<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Hansen&apos;s ink</title><description>悟已往之不谏，知来者之可追。</description><link>https://astro-pure.js.org</link><item><title>基于 Docker 的 MailServer 部署指南 (Gentoo Linux)</title><link>https://astro-pure.js.org/blog/mailserver</link><guid isPermaLink="true">https://astro-pure.js.org/blog/mailserver</guid><description>使用 Docker 容器化部署 Postfix 与 Dovecot 邮件系统</description><pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;核心配置要点与规避事项&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;标准输出重定向 (Syslog 限制)：&lt;/strong&gt; 容器环境通常缺失 &lt;code&gt;syslog&lt;/code&gt; 进程。Postfix 与 Dovecot 的日志必须显式重定向至 &lt;code&gt;/dev/stdout&lt;/code&gt; 或 &lt;code&gt;/dev/stderr&lt;/code&gt;，以确保日志可通过 &lt;code&gt;docker logs&lt;/code&gt; 捕获。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSL 强制依赖检查：&lt;/strong&gt; Dovecot 2.3+ 版本即使在 &lt;code&gt;ssl = no&lt;/code&gt; 模式下，若配置文件中存在未注释的 &lt;code&gt;ssl_cert&lt;/code&gt; 路径，仍会因检查证书文件失败而产生 Fatal 错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LMTP 端口非特权化：&lt;/strong&gt; 为避免 Linux 容器对 1024 以下端口（特权端口）的权限限制或网络策略拦截，建议将内部 LMTP 投递端口由默认的 24 迁移至高位端口（如 2424）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态虚拟用户数据库 (Userdb Static)：&lt;/strong&gt; 在使用 &lt;code&gt;passwd-file&lt;/code&gt; 作为认证源时，&lt;code&gt;userdb&lt;/code&gt; 应配置为 &lt;code&gt;static&lt;/code&gt; 驱动。需显式指定 &lt;code&gt;uid&lt;/code&gt;、&lt;code&gt;gid&lt;/code&gt; 及 &lt;code&gt;mail&lt;/code&gt; 路径格式，防止 Dovecot 自动探测机制在非标准环境下失效。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;部署流程&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;环境说明：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;宿主机：&lt;/strong&gt; Gentoo Linux&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基础镜像：&lt;/strong&gt; Rocky Linux 9&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据根目录：&lt;/strong&gt; &lt;code&gt;/opt/mail_stack&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例域名：&lt;/strong&gt; &lt;code&gt;localhost.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 宿主机环境初始化&lt;/h3&gt;
&lt;p&gt;配置数据持久化目录并统一 UID/GID 映射，确保容器内外文件系统访问权限一致。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;# 停止宿主机冲突服务
/etc/init.d/postfix stop &amp;#x26;&amp;#x26; rc-update del postfix default

# 初始化目录结构
mkdir -p /opt/mail_stack/{vmail,postfix_conf,dovecot_conf,users}

# 创建映射账号 (UID/GID 5000)
groupadd -g 5000 vmail
useradd -u 5000 -g vmail -s /sbin/nologin -d /opt/mail_stack/vmail vmail

# 配置权限
chown -R vmail:vmail /opt/mail_stack/vmail
chmod -R 770 /opt/mail_stack/vmail

# 配置初始用户凭据
cat &amp;#x3C;&amp;#x3C; &apos;EOF&apos; &gt; /opt/mail_stack/users/passwd
guoy@localhost.com:{PLAIN}111111
wanghh@localhost.com:{PLAIN}111111
EOF
chmod 644 /opt/mail_stack/users/passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 提取基础配置文件&lt;/h3&gt;
&lt;p&gt;通过临时容器获取各组件的默认配置文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;docker run --rm -v /opt/mail_stack/postfix_conf:/export rockylinux:9 \
  bash -c &quot;dnf install -y postfix &amp;#x26;&amp;#x26; cp -r /etc/postfix/* /export/&quot;
docker run --rm -v /opt/mail_stack/dovecot_conf:/export rockylinux:9 \
  bash -c &quot;dnf install -y dovecot &amp;#x26;&amp;#x26; cp -r /etc/dovecot/* /export/&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Dovecot 配置优化&lt;/h3&gt;
&lt;p&gt;编辑 &lt;code&gt;/opt/mail_stack/dovecot_conf/&lt;/code&gt; 目录下的配置文件。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;协议声明：&lt;/strong&gt; &lt;code&gt;dovecot.conf&lt;/code&gt; 头部添加 &lt;code&gt;protocols = imap lmtp&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志输出：&lt;/strong&gt; &lt;code&gt;conf.d/10-logging.conf&lt;/code&gt; 设置为标准流输出。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;禁用 SSL：&lt;/strong&gt; 在 &lt;code&gt;conf.d/10-ssl.conf&lt;/code&gt; 中设置 &lt;code&gt;ssl = no&lt;/code&gt; 并注释所有 &lt;code&gt;ssl_cert&lt;/code&gt; 与 &lt;code&gt;ssl_key&lt;/code&gt; 行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LMTP 监听配置：&lt;/strong&gt; 修改 &lt;code&gt;conf.d/10-master.conf&lt;/code&gt;：
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;service lmtp {
  inet_listener lmtp {
    address = 0.0.0.0
    port = 2424
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;认证策略：&lt;/strong&gt; 重写 &lt;code&gt;conf.d/10-auth.conf&lt;/code&gt;：
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;disable_plaintext_auth = no
auth_mechanisms = plain login
passdb {
  driver = passwd-file
  args = scheme=PLAIN username_format=%u /etc/dovecot/users/passwd
}
userdb {
  driver = static
  args = uid=5000 gid=5000 home=/var/vmail/%d/%n mail=maildir:/var/vmail/%d/%n
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. Postfix 路由配置&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;/opt/mail_stack/postfix_conf/main.cf&lt;/code&gt; 中追加或修改以下参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;myhostname = mail.localhost.com
mydomain = localhost.com
myorigin = $mydomain
inet_interfaces = all
inet_protocols = ipv4
mynetworks = 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
virtual_mailbox_domains = localhost.com
virtual_transport = lmtp:inet:dovecot:2424
local_recipient_maps =
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 容器构建与编排&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Dockerfile.postfix:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM rockylinux:9
RUN dnf install -y postfix &amp;#x26;&amp;#x26; dnf clean all
RUN postconf -e &quot;maillog_file = /dev/stdout&quot;
CMD [&quot;postfix&quot;, &quot;start-fg&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Dockerfile.dovecot:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM rockylinux:9
RUN dnf install -y dovecot &amp;#x26;&amp;#x26; dnf clean all
CMD [&quot;dovecot&quot;, &quot;-F&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;docker-compose.yml:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  dovecot:
    build:
      context: .
      dockerfile: Dockerfile.dovecot
    container_name: mail_dovecot
    ports:
      - &quot;143:143&quot;
    volumes:
      - ./dovecot_conf:/etc/dovecot
      - ./vmail:/var/vmail
      - ./users:/etc/dovecot/users
    restart: always

  postfix:
    build:
      context: .
      dockerfile: Dockerfile.postfix
    container_name: mail_postfix
    ports:
      - &quot;25:25&quot;
    volumes:
      - ./postfix_conf:/etc/postfix
    depends_on:
      - dovecot
    restart: always
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 服务验证&lt;/h3&gt;
&lt;p&gt;执行 &lt;code&gt;docker-compose up -d --build&lt;/code&gt; 启动服务。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SMTP 投递测试：&lt;/strong&gt;
使用 &lt;code&gt;telnet&lt;/code&gt; 或 &lt;code&gt;nc&lt;/code&gt; 连接宿主机 25 端口，通过 &lt;code&gt;DATA&lt;/code&gt; 指令提交邮件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件系统验证：&lt;/strong&gt;
检查 &lt;code&gt;/opt/mail_stack/vmail/localhost.com/guoy/new/&lt;/code&gt; 目录。若生成对应的邮件文件，则说明 Postfix (SMTP) 经由 LMTP 成功向 Dovecot 交付邮件。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>企业内部RTL设计与验证的大规模语言模型本地化部署研究报告</title><link>https://astro-pure.js.org/blog/llm-base-in-company</link><guid isPermaLink="true">https://astro-pure.js.org/blog/llm-base-in-company</guid><description>企业内部大模型落地使用可行性分析</description><pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;传统的硬件描述语言（HDL）编写与验证流程正面临严峻挑战，开发周期的压缩与设计质量的提升成为企业生存的核心诉求。近年来，大语言模型（LLM）在软件工程领域的卓越表现，尤其是Cursor和Gemini等工具展示出的语义理解与代码生成能力，为 RTL 设计提供了新的技术路径。然而，芯片设计的核心在于知识产权（IP）的绝对安全，因此，构建一套性能比肩Cursor且能实现物理隔离、本地化部署的RTL辅助设计与验证体系，已成为半导体企业数字化转型的核心课题。&lt;/p&gt;
&lt;h2&gt;1. RTL大模型内核的选型与性能评估&lt;/h2&gt;
&lt;p&gt;构建本地化系统的首要任务是选择合适的基础模型。不同于通用编程模型，RTL设计要求模型对时序并发、时钟域交叉（CDC）、复位逻辑及硬件特有的语法约束有深层理解。&lt;/p&gt;
&lt;h2&gt;1.1 开源编程模型的竞争格局&lt;/h2&gt;
&lt;p&gt;目前的开源模型中，Qwen2.5-Coder、DeepSeek-Coder-V2和Llama 3.1系列处于第一梯队。Qwen2.5-Coder系列在5.5万亿token的多语言语料上进行了预训练，其32B版本在编程能力上被认为已达到GPT-4o的同等水平，支持高达128K的上下文窗口，能够覆盖中型SoC子系统的代码关联需求。DeepSeek-Coder-V2则采用了混合专家（MoE）架构，总参数量虽大，但单次推理激活的参数量（21B）较低，使其在保持高性能的同时显著降低了对计算资源的消耗。&lt;/p&gt;
&lt;p&gt;| &lt;strong&gt;模型名称&lt;/strong&gt;      | &lt;strong&gt;架构类型&lt;/strong&gt; | &lt;strong&gt;训练Token量&lt;/strong&gt; | &lt;strong&gt;支持语言数&lt;/strong&gt; | &lt;strong&gt;上下文长度&lt;/strong&gt; | &lt;strong&gt;核心优势&lt;/strong&gt;                      |
| ----------------- | ------------ | --------------- | -------------- | -------------- | --------------------------------- |
| Qwen2.5-Coder-32B | Dense        | 5.5T            | 40+ 主要语言   | 128k           | 极强的代码修复与逻辑推理性能      |
| DeepSeek-Coder-V2 | MoE          | 10.2T           | 338            | 128k           | 极高的推理效率与广泛的语言支持    |
| Llama 3.1-70B     | Dense        | 15T             | 通用/代码      | 128k           | 生态兼容性最强，推理稳定性高      |
| StarCoder2-15B    | Dense        | 1T              | 600+           | 16k            | 针对库文件与Git提交信息有深度优化 |&lt;/p&gt;
&lt;h2&gt;1.2 硬件专用微调模型的崛起&lt;/h2&gt;
&lt;p&gt;通用模型虽然在语法上表现尚可，但在遵循硬件设计规范（如同步复位优先、避免锁存器推断）方面往往不及经过领域微调的模型。RTLCoder、CodeV和VeriCoder等模型通过在过滤后的高质量Verilog数据集（如VerilogDB）上进行监督微调（SFT），显著提升了功能正确性。
以CodeV为例，该模型采用了“多级总结”策略，利用GPT-3.5对真实世界的RTL代码进行逆向描述生成，从而构建了高质量的“指令-代码”对。实验表明，CodeV在RTLLM基准测试中的表现优于GPT-4，展示了专用微调在解决硬件设计“幻觉”方面的巨大潜力。最新的研究如CodeV-R1，则进一步结合了DeepSeek-R1的推理链能力，通过强化学习（RL）引导模型在生成代码前进行逻辑思考，使其在复杂的协议实现任务中取得了突破。&lt;/p&gt;
&lt;h2&gt;1.3 基准测试与性能比对&lt;/h2&gt;
&lt;p&gt;评估本地模型在RTL任务中的实际效能，主要参考VerilogEval（侧重补全）和RTLLM（侧重合成与验证）。&lt;/p&gt;
&lt;p&gt;| &lt;strong&gt;模型/方法&lt;/strong&gt;      | &lt;strong&gt;VerilogEval-Machine (Pass@1)&lt;/strong&gt; | &lt;strong&gt;RTLLM v1.1 (Pass@1)&lt;/strong&gt; | &lt;strong&gt;资源需求 (4-bit)&lt;/strong&gt; |
| ------------------ | -------------------------------- | ----------------------- | -------------------- |
| GPT-4o (云端基准)  | 67.7%                            | 33.8%                   | N/A                  |
| CodeV-R1-7B (本地) | 68.6%                            | 72.9%                   | ~6GB VRAM            |
| Qwen2.5-Coder-32B  | 66.6%                            | 47.9%                   | ~20GB VRAM           |
| RTL-Coder-6.7B     | 61.2%                            | 36.8%                   | ~6GB VRAM            |
| StarCoder2-15B     | 37.8%                            | 15.5%                   | ~12GB VRAM           |&lt;/p&gt;
&lt;p&gt;数据表明，经过针对性RL训练的小参数模型（如7B量级）在特定硬件指标上已能超越通用巨量模型。这一发现为企业在有限资源下部署高性能AI助理提供了坚实的理论依据。&lt;/p&gt;
&lt;h2&gt;2. 企业级推理引擎后端建设&lt;/h2&gt;
&lt;p&gt;本地部署Cursor类体验的关键不仅在于模型，更在于能够支撑多用户并发请求、低延迟响应的推理后端。&lt;/p&gt;
&lt;h2&gt;2.1 主流推理后端的技术路线比对&lt;/h2&gt;
&lt;p&gt;企业在构建私有服务时，通常在vLLM、TensorRT-LLM和Ollama之间进行选择。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;vLLM&lt;/strong&gt;：是目前高吞吐量生产环境的事实标准。其核心贡献是PagedAttention（分页注意力）技术，该技术模仿操作系统的虚拟内存管理，将KV缓存（Key-Value Cache）切分为固定大小的页面，从而彻底解决了内存碎片化问题。在并发用户数超过10人时，vLLM的吞吐量可达Ollama的10倍以上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TensorRT-LLM&lt;/strong&gt;：作为NVIDIA官方推出的深度优化框架，其通过内核融合（Kernel Fusion）和FP8精度的原生支持，在NVIDIA硬件上能够榨取极致的推理速度。对于延迟极其敏感的实时代码补全任务，TensorRT-LLM相较于通用框架有20%-40%的性能领先。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ollama&lt;/strong&gt;：虽然其并发调度能力较弱，但其“Docker化”的部署体验极大地降低了原型开发难度。对于仅在个人工作站进行尝试的小型团队，Ollama通过集成llama.cpp，在单卡或CPU环境下展现了良好的兼容性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.2 显存需求与硬件拓扑设计&lt;/h2&gt;
&lt;p&gt;RTL任务的模型规模通常分布在7B到32B之间。为了在本地支撑Cursor的流畅感，推理显存的计算必须考虑模型权重、KV缓存和批处理余量。&lt;/p&gt;
&lt;p&gt;| &lt;strong&gt;模型规模&lt;/strong&gt; | &lt;strong&gt;浮点精度 (FP16) 显存&lt;/strong&gt; | &lt;strong&gt;量化精度 (INT4) 显存&lt;/strong&gt; |
| ------------ | ------------------------ | ------------------------ |
| 7B / 8B      | ~14GB                    | ~4-6GB                   |
| 14B / 20B    | ~40GB                    | ~12-16GB                 |
| 32B / 35B    | ~64GB                    | ~20-24GB                 |
| 70B+         | ~140GB                   | ~40-48GB                 |&lt;/p&gt;
&lt;p&gt;对于需要服务多个并发会话的中央服务器，推荐采用多卡张量并行（Tensor Parallelism）架构。研究显示，使用H100 GPU配合vLLM或TensorRT-LLM，在FP8量化下可实现每秒数千个token的产出速率，确保在大型项目搜索或代码重构时，工程师无需长时间等待。&lt;/p&gt;
&lt;h2&gt;3. IDE层级的本地化集成方案&lt;/h2&gt;
&lt;p&gt;Cursor的成功在于其对编辑器上下文的深度集成。要实现这一体验，企业可以采用插件+私有后端的松耦合架构。&lt;/p&gt;
&lt;h2&gt;3.1 Continue.dev：万能本地连接器&lt;/h2&gt;
&lt;p&gt;Continue是一个开源的IDE插件（支持VS Code和JetBrains），被广泛视为 Cursor 的最佳本地化替代品。其允许开发者自定义模型路由，将不同的任务分发给不同的本地模型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多模型协同架构&lt;/strong&gt;：可以在&lt;code&gt;config.json&lt;/code&gt;中配置：将轻量级模型（如Qwen2.5-Coder 1.5B/7B）分配给“tabAutocomplete”通道，以实现毫秒级的代码补全体验；而将推理能力更强的大模型（如Qwen2.5-Coder 32B或DeepSeek-R1 32B）分配给“chat”通道，处理复杂的逻辑问答和代码重构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文增强&lt;/strong&gt;：Continue支持通过&lt;code&gt;@codebase&lt;/code&gt;或&lt;code&gt;@docs&lt;/code&gt;指令引入项目上下文。在本地环境下，插件会自动通过嵌入模型（Embedding Model）为当前工作区建立索引，从而实现跨文件的语义跳转和IP复用分析。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.2 Tabby与代码补全&lt;/h2&gt;
&lt;p&gt;Tabby是一个专为自托管设计的AI编程助手服务器。它集成了模型推理和向量数据库，能够对企业的整个代码仓进行全量索引。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Repository-Level RAG&lt;/strong&gt;：不同于常规的文本切片，Tabby能理解Git仓库的层级结构。当工程师开始编写一个新的模块时，Tabby能检索出内部库中现有的例化（Instantiation）模板和时序逻辑参考，有效减少了重复劳动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;团队管控优势&lt;/strong&gt;：作为中心化的服务端，Tabby便于合规团队进行单一入口审计，确保没有非授权的外部调用，同时可以根据不同部门的权限分发不同的代码索引库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 针对硬件设计的检索增强生成（RAG）深度优化&lt;/h2&gt;
&lt;p&gt;通用的RAG技术在处理RTL代码和硬件规格书（Spec）时常因无法理解层级关系而失效。构建一套“EDA语义感知”的检索系统是本地部署成功的关键。&lt;/p&gt;
&lt;h2&gt;4.1 结构化硬件文档的解析策略&lt;/h2&gt;
&lt;p&gt;硬件文档包含大量表格（如寄存器表、引脚定义）和图表。传统的固定长度切片（Chunking）会撕裂语义。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;层级感知解析&lt;/strong&gt;：建议引入LlamaParse或Unstructured等工具，专门针对PDF中的章节层级（Section 3.1.2）进行树状存储。解析 Liberty（.lib）文件时，应将其转化为结构化的JSON，提取出每个标准单元（Standard Cell）的关键时序与功耗参数（如propagation delay, setup/hold time），以便LLM在进行PPA优化建议时能精准调用数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SDC约束的处理&lt;/strong&gt;：时序约束文件（SDC）具有极强的逻辑关联性。应采用基于正则表达式与语义标签结合的方法，将&lt;code&gt;create_clock&lt;/code&gt;与相关的&lt;code&gt;set_false_path&lt;/code&gt;划归为同一知识单元。实验显示，结构化解析能将复杂查询的召回准确度从31%提升至79%。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4.2 知识图谱（KG）增强检索&lt;/h2&gt;
&lt;p&gt;硬件设计的本质是信号间的连通与反馈，这与图结构高度吻合。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VeriGRAG架构&lt;/strong&gt;：该方案通过提取Verilog代码中的数据路径图（DPG），利用图神经网络（GNN）生成结构化嵌入。当工程师询问“信号A如何影响输出端B”时，系统不再仅仅寻找包含这两个名字的代码片段，而是通过图遍历（Traversal）找回真实的逻辑传播路径，极大地降低了LLM在解释复杂逻辑时的幻觉率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混合检索机制&lt;/strong&gt;：生产环境下可采用“向量搜索 + BM25关键字匹配”的混合模式。向量搜索负责捕捉语义意图（如“握手协议实现”），而BM25则确保诸如&lt;code&gt;inst_reg_0_addr&lt;/code&gt;等具体的工程命名能够被精确命中。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 设计与验证的闭环自动化（Agentic Workflow）&lt;/h2&gt;
&lt;p&gt;Cursor的“效果好”不仅来自补全，还来自其尝试运行和纠错的能力。在本地环境中，这需要将LLM与既有的EDA工具链深度联动。&lt;/p&gt;
&lt;h2&gt;5.1 基于编译器反馈的自修复流&lt;/h2&gt;
&lt;p&gt;LLM生成的Verilog代码常含有微小的语法错误或非合成逻辑。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Verilator/VCS反馈环&lt;/strong&gt;：建立一个多代理（Multi-agent）工作流。第一个Agent根据需求生成初始RTL代码；第二个Agent负责调用本地的Verilator进行静态语法检查；第三个Agent将编译器的错误日志（Log）重新喂回LLM进行修复。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Log2BetterRTL技术&lt;/strong&gt;：研究表明，这种基于日志的闭环反馈能将语法正确率提升约18%，并显著减少Lint违规项（如阻塞/非阻塞赋值混用）。这种迭代修复的过程能在分钟级完成，替代了原本数小时的人工纠错。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5.2 验证套件的高效合成&lt;/h2&gt;
&lt;p&gt;验证工作量通常占整个芯片设计周期的60%-70%。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SVA断言生成&lt;/strong&gt;：利用LLM从自然语言规格书中提取时序逻辑，并自动转换为SystemVerilog Assertions。例如，将“请求必须在2个时钟周期内得到确认”转换为&lt;code&gt;property req_ack_p; @(posedge clk) req |-&gt; ##[1:2] ack; endproperty&lt;/code&gt;。这种方式能将手工编写断言的工作量降低70%以上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;激励生成与覆盖率闭环&lt;/strong&gt;：AI Agent可以根据功能描述自动生成UVM测试序列（Sequence），并根据上一次仿真运行的覆盖率报告（Coverage Report），自动生成能够触发冷门分支（Corner Cases）的随机测试向量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5.3 synopsys eda 能力&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/learnimage-20260326101556187.png&quot; alt=&quot;image-20260326101556187&quot;&gt;
&lt;img src=&quot;https://cdn.willimt.com/learnimage-20260326101626088.png&quot; alt=&quot;image-20260326101626088&quot;&gt;
&lt;img src=&quot;https://cdn.willimt.com/learnimage-20260326101747301.png&quot; alt=&quot;image-20260326101747301&quot;&gt;
&lt;img src=&quot;https://cdn.willimt.com/learnimage-20260326101804207.png&quot; alt=&quot;image-20260326101804207&quot;&gt;
&lt;img src=&quot;https://cdn.willimt.com/learnimage-20260326101829632.png&quot; alt=&quot;image-20260326101829632&quot;&gt;
&lt;img src=&quot;https://cdn.willimt.com/learnimage-20260326101900303.png&quot; alt=&quot;image-20260326101900303&quot;&gt;&lt;/p&gt;
&lt;h2&gt;6. 算力架构计算与显存模型建议&lt;/h2&gt;
&lt;p&gt;在多卡并行环境中，通信带宽往往比单核算力更关键。&lt;/p&gt;
&lt;h2&gt;6.1 并行计算策略&lt;/h2&gt;
&lt;p&gt;当模型参数超过单卡限制时（如部署70B规模的底座模型），必须采用并行技术。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;张量并行（TP）&lt;/strong&gt;：将模型的各层横向切割并分布在多个显卡上。这种方式最适合单节点内的多显卡环境，因为它对GPU间的互联带宽要求极高。建议利用NVLink提供的600GB/s或900GB/s双向带宽。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流水线并行（PP）&lt;/strong&gt;：将模型的不同层纵向分布在不同节点。其带宽要求较低，但会产生计算空泡。在跨服务器集群部署时，这是扩展显存总容量的主要方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6.2 显存资源计算公式&lt;/h2&gt;
&lt;p&gt;为了确保模型在高负载下不发生OOM（内存溢出），需要精准计算显存消耗：
$M_{total} = (P \times Q_{bytes} \times 1.2) + (Batch \times Context \times H_{dim} \times L_{layers} \times 2 / G)$
其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;P：模型参数量（如32B）。&lt;/li&gt;
&lt;li&gt;Q_{bytes}：每参数字节数（4-bit量化约为0.5字节）。&lt;/li&gt;
&lt;li&gt;1.2：框架与激活层冗余系数。&lt;/li&gt;
&lt;li&gt;Batch：并发用户数。&lt;/li&gt;
&lt;li&gt;Context：上下文窗口（如128k）。&lt;/li&gt;
&lt;li&gt;G：GPU卡数。
部分实际测试显示，利用4张A6000（每张显存48G）组成的192G显存池，可以非常从容地运行DeepSeek-Coder-V2（236B量级）的4-bit量化版，并同时支撑20名左右的工程师进行设计任务。
综上所述，通过“强大的开源底座（Qwen/DeepSeek）+ 高并发推理引擎（vLLM）+ 深度IDE集成（Continue）+ 硬件感知RAG”的组合架构，企业完全可以在不牺牲安全性的前提下，获得比肩云端工具的生产力增益。这不仅是开发方式的升级，更是半导体企业在AI时代保护核心竞争力、加速芯片迭代周期的必然选择。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Mailman3 批量初始化用户与邮件列表</title><link>https://astro-pure.js.org/blog/mailserver/mailman3-batch</link><guid isPermaLink="true">https://astro-pure.js.org/blog/mailserver/mailman3-batch</guid><description>记录 Mailman3 中 Web 用户批量创建、邮件列表创建以及成员批量导入的实操过程。</description><pubDate>Fri, 20 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;WEB 端批量注册用户&lt;/h2&gt;
&lt;p&gt;Mailman Web 默认用 &lt;strong&gt;django-allauth&lt;/strong&gt; 做注册/邮箱验证。避免每个用户自己注册并点验证链接，最直接的是在 &lt;code&gt;/opt/mailman/web/settings.py&lt;/code&gt; 里关掉验证。
在 &lt;code&gt;settings.py&lt;/code&gt; 里加入/修改：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 关闭邮箱验证（最关键）
ACCOUNT_EMAIL_VERIFICATION = &quot;none&quot;

# 允许不验证也能登录（一般默认就是 True，但明确写上）
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_AUTHENTICATION_METHOD = &quot;username_email&quot;  # 你的版本若提示 deprecated 可忽略或按新项改

# 可选：如果你不希望开放自助注册，直接关掉注册入口（强烈建议内网这样做）
ACCOUNT_ALLOW_REGISTRATION = False   # 若版本不支持就用 URL/模板层面禁用 /accounts/signup/

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;/opt/mailman/web/scripts&lt;/code&gt; 下创建 &lt;code&gt;create_web_users.py&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/opt/mailman/venv/bin/python
import os
import sys

# === Django bootstrap (must be BEFORE importing Django models) ===
os.environ.setdefault(&quot;DJANGO_SETTINGS_MODULE&quot;, &quot;settings&quot;)
sys.path.insert(0, &quot;/opt/mailman/web&quot;)

import django
django.setup()

# === then import Django/allauth models ===
from django.contrib.auth import get_user_model
from django.db import transaction
from allauth.account.models import EmailAddress

PASSWORD = &quot;111111&quot;

emails = [
    &quot;wanghh@example.com&quot;,
]

User = get_user_model()

created = 0
updated = 0

with transaction.atomic():
    for email in emails:
        email = email.lower().strip()
        username = email.split(&quot;@&quot;)[0]

        user, is_new = User.objects.get_or_create(
            username=username,
            defaults={&quot;email&quot;: email, &quot;is_active&quot;: True},
        )

        user.email = email
        user.is_active = True
        user.set_password(PASSWORD)
        user.save()

        ea, _ = EmailAddress.objects.get_or_create(
            user=user,
            email=email,
            defaults={&quot;primary&quot;: True, &quot;verified&quot;: True},
        )
        ea.primary = True
        ea.verified = True
        ea.save()

        if is_new:
            created += 1
        else:
            updated += 1

print(f&quot;Web users done: created={created}, updated={updated}, total={len(emails)}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后使用虚拟环境的 python 运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/opt/mailman/venv/bin/python create_web_users.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mailman 3 创建 list 并添加订阅者&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;/opt/mailman/scripts&lt;/code&gt; 中创建 create_list.py：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/opt/mailman/venv/bin/python
# -*- coding: utf-8 -*-

&quot;&quot;&quot;
Create-or-update a Mailman3 list and batch-add members from member.txt.
Also ensures default owners are assigned.

Usage:
  /opt/mailman/venv/bin/python create_list.py dev
  /opt/mailman/venv/bin/python create_list.py dev --domain lists.example.com --members-file member.txt

Env (optional):
  MAILMAN_REST_URL   default: http://127.0.0.1:8001/3.1
  MAILMAN_REST_USER  default: restadmin
  MAILMAN_REST_PASS  default: restpass
&quot;&quot;&quot;

import argparse
import os
import sys
import time
from urllib.error import HTTPError

from mailmanclient import Client

DEFAULT_OWNERS = [
    &quot;pengyq@example.com&quot;,
    &quot;wangwy@example.com&quot;,
    &quot;xiaorzh@example.com&quot;,
]


def read_emails(path: str) -&gt; list[str]:
    emails: list[str] = []
    with open(path, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
        for line in f:
            s = line.strip()
            if not s or s.startswith(&quot;#&quot;):
                continue
            emails.append(s)
    # 去重但保持顺序
    seen = set()
    out = []
    for e in emails:
        if e not in seen:
            seen.add(e)
            out.append(e)
    return out


def http_status(e: Exception) -&gt; int | None:
    # urllib.error.HTTPError 有 code 字段
    return getattr(e, &quot;code&quot;, None)


def get_or_create_domain(client: Client, domain: str):
    &quot;&quot;&quot;
    Return domain proxy; create if not exists.
    &quot;&quot;&quot;
    try:
        return client.get_domain(domain)
    except HTTPError as e:
        if http_status(e) == 404:
            try:
                client.create_domain(domain)
            except HTTPError as e2:
                # 可能域已存在：400 Duplicate email host / 409 Conflict
                if http_status(e2) not in (400, 409):
                    raise
            return client.get_domain(domain)
        raise


def robust_get_list(client: Client, list_id: str, retries: int = 3, sleep_s: float = 0.5):
    &quot;&quot;&quot;
    After a create_list 500, the list may still have been created.
    Retry-get to confirm.
    &quot;&quot;&quot;
    last = None
    for _ in range(retries):
        try:
            return client.get_list(list_id)
        except HTTPError as e:
            last = e
            if http_status(e) == 404:
                time.sleep(sleep_s)
                continue
            raise
    # retries exhausted
    if last:
        raise last
    raise RuntimeError(&quot;unexpected&quot;)


def get_or_create_list(client: Client, domain: str, local_part: str):
    &quot;&quot;&quot;
    Return (mlist, created_bool)
    list_id in Mailman REST is typically &quot;&amp;#x3C;local_part&gt;.&amp;#x3C;domain&gt;&quot;.
    &quot;&quot;&quot;
    list_id = f&quot;{local_part}.{domain}&quot;

    # 先查是否存在
    try:
        mlist = client.get_list(list_id)
        return mlist, False
    except HTTPError as e:
        if http_status(e) != 404:
            raise

    # 不存在 -&gt; 创建
    dom = get_or_create_domain(client, domain)

    try:
        mlist = dom.create_list(local_part)
        return mlist, True

    except HTTPError as e:
        # 关键：有时服务端返回 500，但 list 已写入 DB
        if http_status(e) == 500:
            # 再查一次确认是否已创建
            mlist = robust_get_list(client, list_id, retries=5, sleep_s=0.5)
            return mlist, True

        # 已存在/冲突等：再查一次兜底
        if http_status(e) in (400, 409):
            mlist = robust_get_list(client, list_id, retries=3, sleep_s=0.2)
            return mlist, False

        raise


def ensure_owners(mlist, owners: list[str]) -&gt; tuple[int, int]:
    &quot;&quot;&quot;
    Ensure owners are assigned. Returns (added, skipped).
    Uses role API mlist.add_owner(address).
    &quot;&quot;&quot;
    added = 0
    skipped = 0
    for addr in owners:
        try:
            mlist.add_owner(addr)
            added += 1
        except HTTPError as e:
            if http_status(e) in (400, 409):
                skipped += 1
                continue
            raise
    return added, skipped


def add_members(mlist, members: list[str]) -&gt; tuple[int, int]:
    &quot;&quot;&quot;
    Subscribe members in batch. Returns (added, skipped).
    pre_verified/pre_confirmed/pre_approved 可以绕过每人单独确认/验证。
    &quot;&quot;&quot;
    added = 0
    skipped = 0
    for addr in members:
        try:
            mlist.subscribe(
                addr,
                pre_verified=True,
                pre_confirmed=True,
                pre_approved=True,
            )
            added += 1
        except HTTPError as e:
            if http_status(e) in (400, 409):
                skipped += 1
                continue
            raise
    return added, skipped


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument(&quot;listname&quot;, help=&quot;mailing list local-part (e.g. dev)&quot;)
    ap.add_argument(&quot;--domain&quot;, default=&quot;lists.example.com&quot;, help=&quot;mail host/domain (default: lists.example.com)&quot;)
    ap.add_argument(&quot;--members-file&quot;, default=&quot;member.txt&quot;, help=&quot;members file path (default: member.txt)&quot;)
    args = ap.parse_args()

    rest_url = os.getenv(&quot;MAILMAN_REST_URL&quot;, &quot;http://127.0.0.1:8001/3.1&quot;)
    rest_user = os.getenv(&quot;MAILMAN_REST_USER&quot;, &quot;restadmin&quot;)
    rest_pass = os.getenv(&quot;MAILMAN_REST_PASS&quot;, &quot;restpass&quot;)

    client = Client(rest_url, rest_user, rest_pass)

    if not os.path.exists(args.members_file):
        print(f&quot;[FATAL] members file not found: {args.members_file}&quot;, file=sys.stderr)
        sys.exit(2)

    members = read_emails(args.members_file)

    mlist, created = get_or_create_list(client, args.domain, args.listname)
    print(f&quot;[OK] list: {mlist.fqdn_listname} (created={created})&quot;)

    a, s = ensure_owners(mlist, DEFAULT_OWNERS)
    print(f&quot;[OK] owners ensured: added={a}, skipped(existing/dup)={s}&quot;)

    a, s = add_members(mlist, members)
    print(f&quot;[OK] members subscribed: added={a}, skipped(existing/dup)={s}&quot;)

    print(&quot;[DONE]&quot;)


if __name__ == &quot;__main__&quot;:
    main()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过运行虚拟环境中的 python 来执行脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/opt/mailman/venv/bin/python create_list.py &quot;dev&quot;
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Mailman3 安装与部署</title><link>https://astro-pure.js.org/blog/mailserver/mailman3-install</link><guid isPermaLink="true">https://astro-pure.js.org/blog/mailserver/mailman3-install</guid><description>记录 Mailman3 组件架构、部署流程与常用端口配置，帮助快速完成邮件列表服务搭建。</description><pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一、整体架构说明&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;Postfix  --&gt;  Mailman Core  --&gt;  HyperKitty (Archiver)
                  |
                  +--&gt; REST API (8001)
                  |
                  +--&gt; Postorius (Web 管理)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mailman Core&lt;/strong&gt;：负责邮件列表逻辑、投递、归档调度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Postorius&lt;/strong&gt;：列表管理 Web UI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HyperKitty&lt;/strong&gt;：邮件归档与 Web 展示&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mailman-Web&lt;/strong&gt;：Django 项目，包含 Postorius + HyperKitty&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nginx&lt;/strong&gt;：对外 Web 入口（8090）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关键端口规划&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;|      服务      |  端口  |     说明      |
| :----------: | :--: | :---------: |
| Mailman REST | 8001 |    仅本机访问    |
| Mailman Web  | 8000 | Gunicorn 内部 |
|  Nginx Web   | 8090 |    对外访问     |&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;二、Mailman Core 安装&lt;/h2&gt;
&lt;h3&gt;2.1 安装系统依赖（Rocky Linux 10）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo dnf update -y
sudo dnf install -y \
  git gcc make \
  python3 python3-devel python3-pip \
  libffi-devel openssl-devel \
  postfix \
  mariadb-connector-c-devel \
  nginx

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：Mailman Core/ Web 都是 Python 项目；生产环境强烈建议用&lt;strong&gt;独立虚拟环境&lt;/strong&gt;和&lt;strong&gt;专用系统用户&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.2 创建专用用户与目录&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo useradd -r -m -d /opt/mailman -s /sbin/nologin mailman
sudo mkdir -p /opt/mailman/{venv,core,web,var,logs,etc}
sudo chown -R mailman: mailman /opt/mailman
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 建虚拟环境&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo -u mailman python3 -m venv /opt/mailman/venv
sudo -u mailman /opt/mailman/venv/bin/pip install -U pip wheel setuptools
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;clone 仓库再安装&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo -u mailman git clone https://gitlab.com/mailman/mailman.git /opt/mailman/core
sudo -u mailman /opt/mailman/venv/bin/pip install -e /opt/mailman/core
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Mailman Core 的安装与运行结构、命令行（&lt;code&gt;mailman start|stop|shell&lt;/code&gt;）以官方安装文档为基准。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3&gt;2.4 配置 Mailman Core（mailman. Cfg）&lt;/h3&gt;
&lt;p&gt;把主配置放到 &lt;code&gt;/opt/mailman/etc/mailman.cfg&lt;/code&gt;（自建目录更清晰）。
最终配置为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[paths.custom]
var_dir: /opt/mailman/var

[mailman]
layout: custom
site_owner: wanghh@example.com
# Mailman 的 var_dir：队列、数据、生成的 postfix map 等

[mta]
incoming: mailman.mta.postfix.LMTP
outgoing: mailman.mta.deliver.deliver
# Postfix 投递给 Mailman 用 LMTP（推荐 8024 之类高端口）
lmtp_host: 127.0.0.1
lmtp_port: 8024

# Mailman 发信走 Postfix（本机 25）
smtp_host: 127.0.0.1
smtp_port: 25


[database]
# 生产强烈建议用 PostgreSQL
# 这里给 PostgreSQL 的写法示意：
class: mailman.database.postgresql.PostgreSQLDatabase
url: postgresql://mailman:123456@127.0.0.1:5432/mailman

[logging]
config: /opt/mailman/etc/logging.cfg

[rest]
username: restadmin
password: restpass

[archiver.hyperkitty]
class: mailman_hyperkitty.Archiver
enable: yes
configuration: /opt/mailman/etc/hyperkitty.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处应注意 &lt;code&gt;var_dir: /opt/mailman/var&lt;/code&gt; ，设置为本地默认路径，避免路径错误。另外，直接运行 mailman 3 不要在 root 目录下，可能会是程序将当前路径加载进程序，导致权限问题或其他问题。&lt;/p&gt;
&lt;h2&gt;三、Mailman 3 与原有邮件服务器协同工作&lt;/h2&gt;
&lt;p&gt;Postfix / Dovecot 基于虚拟用户使用&lt;/p&gt;
&lt;h3&gt;3.1 整体架构&lt;/h3&gt;
&lt;p&gt;在 &lt;strong&gt;虚拟用户环境&lt;/strong&gt; 下，正确的职责划分是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;外部邮件
   ↓
Postfix
   ├─ 普通邮箱（user@example.com） → virtual_mailbox_maps → Dovecot
   └─ 列表邮箱（list@lists.example.com）
         → transport_maps → lmtp:127.0.0.1:8024 → Mailman Core
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;strong&gt;transport_maps&lt;/strong&gt; 把“列表域 / 列表地址”优先交给 Mailman&lt;/li&gt;
&lt;li&gt;Mailman &lt;strong&gt;自动生成 Postfix maps&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Postfix 只“引用”这些 map，不自己维护列表&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 Mailman 3 生成 Postfix map&lt;/h3&gt;
&lt;p&gt;Mailman Core 在运行后，会在它的 &lt;code&gt;var_dir&lt;/code&gt; 下生成 &lt;strong&gt;Postfix 可用的映射文件&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;在Mailman 侧：启用 Postfix MTA 接口&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[mta]
incoming: mailman.mta.postfix.LMTP
outgoing: mailman.mta.deliver.deliver
# Postfix 投递给 Mailman 用 LMTP（推荐 8024 之类高端口）
lmtp_host: 127.0.0.1
lmtp_port: 8024

# Mailman 发信走 Postfix（本机 25）
smtp_host: 127.0.0.1
smtp_port: 25
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后确认 Mailman 正常运行后，生成了 map：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -l /opt/mailman/var/data/

total 72
-rw-rw----. 1 mailman mailman   349 Feb  3 10:42 postfix_domains
-rw-r-----. 1 mailman mailman 32768 Feb  3 10:42 postfix_domains.lmdb
-rw-rw----. 1 mailman mailman   988 Feb  3 10:42 postfix_lmtp
-rw-r-----. 1 mailman mailman 32768 Feb  3 10:42 postfix_lmtp.lmdb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这个目录不存在，说明 Mailman 还没真正启动成功。
也可以指定生成 map 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo -u mailman -H /opt/mailman/venv/bin/mailman -C /opt/mailman/etc/mailman.cfg aliases
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Postfix 侧：&lt;strong&gt;正确引用 Mailman 生成的 map&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;在虚拟用户场景下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;virtual_mailbox_maps&lt;/code&gt;：只管真实邮箱&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transport_maps&lt;/code&gt;：&lt;strong&gt;优先级更高，用来“劫持”列表地址&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;local_recipient_maps&lt;/code&gt;：要么关闭，要么确保不拒绝列表地址
修改 Postfix  &lt;code&gt;/etc/postfix/main.cf&lt;/code&gt; 推荐配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# for mailman
local_recipient_maps =                                                                 lmdb:/opt/mailman/var/data/postfix_lmtp,
	$alias_maps,
    lmdb:/etc/postfix/virtual-mailbox

relay_domains =                                                                        lmdb:/opt/mailman/var/data/postfix_domains
transport_maps =                                                                       lmdb:/opt/mailman/var/data/postfix_lmtp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Rocky/RHEL 系现在更推荐 &lt;code&gt;lmdb:&lt;/code&gt;，而不是 &lt;code&gt;hash:&lt;/code&gt;（db3/bdb 经常被拆包或默认不带）&lt;/p&gt;
&lt;h4&gt;生成 map&lt;/h4&gt;
&lt;p&gt;Mailman 生成的是 &lt;strong&gt;文本文件&lt;/strong&gt;，Postfix 用时必须 postmap：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo postmap lmdb:/opt/mailman/var/data/postfix_domains
sudo postmap lmdb:/opt/mailman/var/data/postfix_lmtp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证 Postfix transport 是否命中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;postmap -q lists.example.com /opt/mailman/var/data/postfix_domains
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可查看日志：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tail -f /var/log/maillog
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;systemd 管理 Mailman Core&lt;/h4&gt;
&lt;p&gt;创建 systemd unit：&lt;code&gt;/etc/systemd/system/mailman.service&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[Unit]
Description=GNU Mailman Core
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=simple
User=mailman
Group=mailman
WorkingDirectory=/opt/mailman
Environment=HOME=/opt/mailman

# 关键：systemd 直接跟踪 master 进程
ExecStart=/opt/mailman/venv/bin/master -C /opt/mailman/etc/mailman.cfg

# 让 systemd 用 SIGTERM 停止（默认也是 SIGTERM）
ExecStop=/bin/kill -TERM $MAINPID

Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重载并重启：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl daemon-reload
sudo systemctl reset-failed mailman
sudo systemctl restart mailman
sudo systemctl status mailman --no-pager
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、部署 Mailman Web&lt;/h2&gt;
&lt;p&gt;Mailman Web 实际是一个 &lt;strong&gt;Django 项目&lt;/strong&gt;，由三部分组成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;┌──────────────┐
│  Postorius   │  ← 列表管理 UI
├──────────────┤
│  HyperKitty  │  ← 邮件归档 UI
├──────────────┤
│ mailman-web  │  ← Django 项目本体
└──────┬───────┘
       │ REST
       ▼
┌──────────────────┐
│   Mailman Core   │  ← 你已经跑起来了
└──────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.1 安装 web&lt;/h3&gt;
&lt;p&gt;直接复用 &lt;code&gt;/opt/mailman/venv&lt;/code&gt;，这样版本和依赖最干净。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo -u mailman -H /opt/mailman/venv/bin/pip install -U \
  mailman-web \
  postorius \
  hyperkitty
# 验证
sudo -u mailman -H /opt/mailman/venv/bin/pip show mailman-web postorius hyperkitty
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建 mailman-web Django 项目配置&lt;/strong&gt;
1、 创建配置目录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mkdir -p /opt/mailman/web
sudo chown -R mailman:mailman /opt/mailman/web

sudo mkdir -p /opt/mailman/web/logs
sudo chown -R mailman:mailman /opt/mailman/web/logs
sudo chmod 750 /opt/mailman/web/logs

sudo mkdir -p /opt/mailman/var/logs
sudo chown -R mailman:mailman /opt/mailman/var/logs
sudo chmod 750 /opt/mailman/var/logs

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、生成默认 Django 配置  &lt;code&gt;/opt/mailman/web/settings.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Mailman Web configuration (custom location)
from mailman_web.settings.base import *
from mailman_web.settings.mailman import *

# ---- Basic ----
DEBUG = False
ALLOWED_HOSTS = [&quot;192.168.52.46&quot;, &quot;127.0.0.1&quot;, &quot;localhost&quot;]
SECRET_KEY = &quot;f1a3696c-961e-4133-ad1a-2c7fcb5e5722&quot;

# --- static files ---
STATIC_URL = &quot;/static/&quot;
STATIC_ROOT = &quot;/opt/mailman/web/static&quot;

# 如果用 compressor offline，建议保留：
COMPRESS_OFFLINE = True

# ---- Database (建议单独建一个库) ----
DATABASES = {
    &quot;default&quot;: {
        &quot;ENGINE&quot;: &quot;django.db.backends.postgresql&quot;,
        &quot;NAME&quot;: &quot;mailmanweb&quot;,
        &quot;USER&quot;: &quot;mailman&quot;,
        &quot;PASSWORD&quot;: &quot;123456&quot;,
        &quot;HOST&quot;: &quot;127.0.0.1&quot;,
        &quot;PORT&quot;: &quot;5432&quot;,
    }
}

# ---- Connect to Mailman Core REST (与你 mailman.cfg 一致) ----
MAILMAN_REST_API_URL = &quot;http://127.0.0.1:8001/&quot;
MAILMAN_REST_API_VERSION = &quot;3.1&quot;
MAILMAN_REST_API_USER = &quot;restadmin&quot;
MAILMAN_REST_API_PASS = &quot;restpass&quot;


# 归档页面 URL
HYPERKITTY_URL = &quot;http://127.0.0.1/hyperkitty/&quot;
#HYPERKITTY_API_KEY = &quot;59ebff6c-3f00-4991-8125-e3a3b28907c7&quot;

MAILMAN_ARCHIVER_KEY = &quot;59ebff6c-3f00-4991-8125-e3a3b28907c7&quot;

CSRF_TRUSTED_ORIGINS = [
    &quot;http://192.168.52.46:8090&quot;,
]

USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = (&apos;HTTP_X_FORWARDED_PROTO&apos;, &apos;http&apos;)

ACCOUNT_DEFAULT_HTTP_PROTOCOL = &quot;http&quot;
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_HTTPONLY = False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、PostgreSQL 创建 mailmanweb 数据库（如果还没建）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo -u postgres psql -c &quot;CREATE DATABASE mailmanweb OWNER mailman;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4、 初始化 Django（迁移 + 管理员 + 静态文件）
后面所有命令都用同一个环境变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export MAILMAN_WEB_CONFIG=/opt/mailman/web/settings.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;迁移数据库&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo -u mailman -H env MAILMAN_WEB_CONFIG=/opt/mailman/web/settings.py \
  /opt/mailman/venv/bin/mailman-web migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 Web 超级管理员&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo -u mailman -H env MAILMAN_WEB_CONFIG=/opt/mailman/web/settings.py \
  /opt/mailman/venv/bin/mailman-web createsuperuser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;收集静态文件 ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo -u mailman -H \
  PYTHONPATH=/opt/mailman/web \
  DJANGO_SETTINGS_MODULE=settings \
  /opt/mailman/venv/bin/django-admin collectstatic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo -u mailman -H \
  PYTHONPATH=/opt/mailman/web \
  DJANGO_SETTINGS_MODULE=settings \
  /opt/mailman/venv/bin/django-admin runserver 127.0.0.1:8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5、用 gunicorn 跑 mailman-web（systemd）
创建 systemd 服务文件 &lt;code&gt;/etc/systemd/system/mailman-web.service&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[Unit]
Description=Mailman3 Web (Postorius + HyperKitty) via Gunicorn
After=network.target

[Service]
Type=simple
User=mailman
Group=mailman
WorkingDirectory=/opt/mailman/web

Environment=&quot;PYTHONPATH=/opt/mailman/web&quot;
Environment=&quot;DJANGO_SETTINGS_MODULE=settings&quot;

# 如果 settings.py 里还保留了 MAILMAN_WEB_CONFIG 机制，不用也行；
# 但避免混乱，生产建议只用 DJANGO_SETTINGS_MODULE 即可。

ExecStart=/opt/mailman/venv/bin/gunicorn \
  --bind 127.0.0.1:8000 \
  --workers 3 \
  --timeout 120 \
  --access-logfile /opt/mailman/web/logs/gunicorn_access.log \
  --error-logfile /opt/mailman/web/logs/gunicorn_error.log \
  wsgi:application

Restart=on-failure
RestartSec=3

# 让 gunicorn 读到必要路径
UMask=0027

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl daemon-reload
sudo systemctl enable --now mailman-web
systemctl status mailman-web --no-pager
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 Nginx 反向代理&lt;/h3&gt;
&lt;p&gt;创建 &lt;code&gt;/etc/nginx/conf.d/mailman-web.conf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;server {
    listen 8090;
    server_name 192.168.52.46;

    # 静态文件（关键：解决样式简陋）
    location /static/ {
        alias /opt/mailman/web/static/;
        expires 7d;
        add_header Cache-Control &quot;public&quot;;
    }

    # Web 反代（Postorius/HyperKitty）
    location / {
        proxy_pass http://127.0.0.1:8000;
        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;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启 nginx ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo nginx -t
sudo systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4 .3 HyperKitty 与 mailman 互通&lt;/h3&gt;
&lt;p&gt;Mailman 的配置文件需要配置 HyperKitty 信息，&lt;code&gt;/opt/mailman/etc/mailman.cfg&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[archiver.hyperkitty]
class: mailman_hyperkitty.Archiver
enable: yes
configuration: /opt/mailman/etc/hyperkitty.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;/opt/mailman/etc/hyperkitty.cfg&lt;/code&gt; 中内容为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[general]
base_url: http://127.0.0.1:8090/hyperkitty/
api_key: 59ebff6c-3f00-4991-8125-e3a3b28907c7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Django 也需配置  &lt;code&gt;/opt/mailman/web/settings.py&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 归档页面 URL
HYPERKITTY_URL = &quot;http://127.0.0.1/hyperkitty/&quot;
#HYPERKITTY_API_KEY = &quot;59ebff6c-3f00-4991-8125-e3a3b28907c7&quot;

MAILMAN_ARCHIVER_KEY = &quot;59ebff6c-3f00-4991-8125-e3a3b28907c7&quot;
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MySQL 自动备份脚本</title><link>https://astro-pure.js.org/blog/mysql-backup</link><guid isPermaLink="true">https://astro-pure.js.org/blog/mysql-backup</guid><description>使用 Python 编写 MySQL 自动备份脚本，涵盖备份、压缩、日志记录与基础配置管理。</description><pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;1、数据库备份脚本&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
&quot;&quot;&quot;
MySQL数据库自动备份脚本
支持定时备份、压缩、保留策略、日志记录
&quot;&quot;&quot;

import os
import sys
import subprocess
import datetime
import time
import gzip
import shutil
import logging
from logging.handlers import RotatingFileHandler
import json
import argparse
from pathlib import Path

class MySQLBackup:
    def __init__(self, config_file=None):
        # 默认配置
        self.config = {
            &apos;mysql&apos;: {
                &apos;host&apos;: &apos;localhost&apos;,
                &apos;port&apos;: 3306,
                &apos;user&apos;: &apos;root&apos;,
                &apos;password&apos;: &apos;123456&apos;,  # 建议使用配置文件或环境变量
                &apos;database&apos;: &apos;fdmtek-aps&apos;
            },
            &apos;backup&apos;: {
                &apos;backup_dir&apos;: &apos;/data1/backups/mysql&apos;,
                &apos;retention_days&apos;: 30,  # 保留30天
                &apos;compress&apos;: False,
                &apos;compress_level&apos;: 6,  # 压缩级别 1-9
                &apos;timeout&apos;: 300  # mysqldump超时时间（秒）
            },
            &apos;logging&apos;: {
                &apos;log_file&apos;: &apos;/var/log/mysql_backup.log&apos;,
                &apos;log_level&apos;: &apos;INFO&apos;,
                &apos;max_size_mb&apos;: 10,
                &apos;backup_count&apos;: 5
            }
        }

		# 初始化日志
		self.setup_logging()

        # 加载配置文件
        if config_file and os.path.exists(config_file):
            self.load_config(config_file)

        # 检查必要工具
        self.check_prerequisites()

    def load_config(self, config_file):
        &quot;&quot;&quot;加载配置文件&quot;&quot;&quot;
        try:
            with open(config_file, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
                user_config = json.load(f)

            # 深度更新配置
            def deep_update(target, source):
                for key, value in source.items():
                    if key in target and isinstance(target[key], dict) and isinstance(value, dict):
                        deep_update(target[key], value)
                    else:
                        target[key] = value

            deep_update(self.config, user_config)
            self.logger.info(f&quot;已加载配置文件: {config_file}&quot;)
        except Exception as e:
            self.logger.error(f&quot;加载配置文件失败: {e}&quot;)
            raise

    def setup_logging(self):
        &quot;&quot;&quot;设置日志系统&quot;&quot;&quot;
        log_config = self.config[&apos;logging&apos;]

        # 创建日志目录
        log_dir = os.path.dirname(log_config[&apos;log_file&apos;])
        os.makedirs(log_dir, exist_ok=True)

        # 创建logger
        self.logger = logging.getLogger(&apos;MySQLBackup&apos;)
        self.logger.setLevel(getattr(logging, log_config[&apos;log_level&apos;]))

        # 清除已有的handler
        self.logger.handlers.clear()

        # 文件handler（滚动日志）
        file_handler = RotatingFileHandler(
            log_config[&apos;log_file&apos;],
            maxBytes=log_config[&apos;max_size_mb&apos;] * 1024 * 1024,
            backupCount=log_config[&apos;backup_count&apos;],
            encoding=&apos;utf-8&apos;
        )

        # 控制台handler
        console_handler = logging.StreamHandler()

        # 设置格式
        formatter = logging.Formatter(
            &apos;%(asctime)s - %(name)s - %(levelname)s - %(message)s&apos;,
            datefmt=&apos;%Y-%m-%d %H:%M:%S&apos;
        )

        file_handler.setFormatter(formatter)
        console_handler.setFormatter(formatter)

        self.logger.addHandler(file_handler)
        self.logger.addHandler(console_handler)

    def check_prerequisites(self):
        &quot;&quot;&quot;检查必要的工具是否安装&quot;&quot;&quot;
        required_tools = [&apos;mysqldump&apos;, &apos;mysql&apos;]

        for tool in required_tools:
            try:
                subprocess.run([&apos;which&apos;, tool], check=True, capture_output=True)
            except subprocess.CalledProcessError:
                self.logger.error(f&quot;未找到必要工具: {tool}&quot;)
                raise RuntimeError(f&quot;请先安装 {tool}&quot;)

        self.logger.info(&quot;所有必要工具已就绪&quot;)

    def create_backup_dir(self):
        &quot;&quot;&quot;创建备份目录&quot;&quot;&quot;
        backup_dir = self.config[&apos;backup&apos;][&apos;backup_dir&apos;]
        try:
            os.makedirs(backup_dir, exist_ok=True)
            self.logger.debug(f&quot;备份目录: {backup_dir}&quot;)
            return backup_dir
        except Exception as e:
            self.logger.error(f&quot;创建备份目录失败: {e}&quot;)
            raise

    def backup_database(self):
        &quot;&quot;&quot;执行数据库备份&quot;&quot;&quot;
        mysql_config = self.config[&apos;mysql&apos;]
        backup_config = self.config[&apos;backup&apos;]

        # 创建备份目录
        backup_dir = self.create_backup_dir()

        # 生成备份文件名
        timestamp = datetime.datetime.now().strftime(&apos;%Y%m%d_%H%M%S&apos;)
        backup_file = os.path.join(
            backup_dir,
            f&quot;{mysql_config[&apos;database&apos;]}_{timestamp}.sql&quot;
        )

        # 构建mysqldump命令
        cmd = [
            &apos;mysqldump&apos;,
            &apos;--single-transaction&apos;,
            &apos;--routines&apos;,
            &apos;--triggers&apos;,
            &apos;--events&apos;,
            &apos;--add-drop-database&apos;,
            &apos;--databases&apos;, mysql_config[&apos;database&apos;],
            &apos;--host&apos;, mysql_config[&apos;host&apos;],
            &apos;--port&apos;, str(mysql_config[&apos;port&apos;]),
            &apos;--user&apos;, mysql_config[&apos;user&apos;],
            &apos;--result-file&apos;, backup_file
        ]

        # 添加密码（安全方式）
        env = os.environ.copy()
        if mysql_config[&apos;password&apos;]:
            env[&apos;MYSQL_PWD&apos;] = mysql_config[&apos;password&apos;]

        try:
            self.logger.info(f&quot;开始备份数据库: {mysql_config[&apos;database&apos;]}&quot;)
            self.logger.debug(f&quot;备份命令: {&apos; &apos;.join(cmd)}&quot;)

            # 执行备份
            start_time = time.time()
            result = subprocess.run(
                cmd,
                env=env,
                capture_output=True,
                text=True,
                timeout=backup_config[&apos;timeout&apos;]
            )

            elapsed_time = time.time() - start_time

            if result.returncode != 0:
                self.logger.error(f&quot;备份失败: {result.stderr}&quot;)
                return False

            # 检查备份文件
            if not os.path.exists(backup_file):
                self.logger.error(&quot;备份文件未生成&quot;)
                return False

            file_size = os.path.getsize(backup_file)
            self.logger.info(f&quot;备份成功: {backup_file} ({file_size / 1024:.2f} KB)&quot;)
            self.logger.info(f&quot;备份耗时: {elapsed_time:.2f}秒&quot;)

            # 压缩备份文件
            if backup_config[&apos;compress&apos;]:
                backup_file = self.compress_backup(backup_file, backup_config[&apos;compress_level&apos;])

            # 清理旧备份
            self.clean_old_backups(backup_dir)

            return backup_file

        except subprocess.TimeoutExpired:
            self.logger.error(&quot;备份操作超时&quot;)
            return False
        except Exception as e:
            self.logger.error(f&quot;备份过程中发生错误: {e}&quot;)
            return False

    def compress_backup(self, backup_file, compress_level=6):
        &quot;&quot;&quot;压缩备份文件&quot;&quot;&quot;
        try:
            compressed_file = f&quot;{backup_file}.gz&quot;

            self.logger.info(f&quot;开始压缩备份文件: {backup_file}&quot;)

            with open(backup_file, &apos;rb&apos;) as f_in:
                with gzip.open(compressed_file, &apos;wb&apos;, compresslevel=compress_level) as f_out:
                    shutil.copyfileobj(f_in, f_out)

            # 删除原始文件
            os.remove(backup_file)

            # 计算压缩比
            original_size = os.path.getsize(compressed_file)
            self.logger.info(f&quot;压缩完成: {compressed_file}&quot;)

            return compressed_file

        except Exception as e:
            self.logger.error(f&quot;压缩备份文件失败: {e}&quot;)
            return backup_file

    def clean_old_backups(self, backup_dir):
        &quot;&quot;&quot;清理旧的备份文件&quot;&quot;&quot;
        retention_days = self.config[&apos;backup&apos;][&apos;retention_days&apos;]
        cutoff_time = time.time() - (retention_days * 24 * 3600)

        try:
            deleted_files = []
            for item in os.listdir(backup_dir):
                item_path = os.path.join(backup_dir, item)

                # 只处理备份文件
                if not (item.startswith(self.config[&apos;mysql&apos;][&apos;database&apos;]) and
                       (item.endswith(&apos;.sql&apos;) or item.endswith(&apos;.sql.gz&apos;))):
                    continue

                # 检查文件时间
                if os.path.getmtime(item_path) &amp;#x3C; cutoff_time:
                    os.remove(item_path)
                    deleted_files.append(item)

            if deleted_files:
                self.logger.info(f&quot;已清理过期备份文件: {len(deleted_files)}个&quot;)
                self.logger.debug(f&quot;清理的文件: {&apos;, &apos;.join(deleted_files)}&quot;)

        except Exception as e:
            self.logger.error(f&quot;清理旧备份失败: {e}&quot;)

    def test_mysql_connection(self):
        &quot;&quot;&quot;测试MySQL连接&quot;&quot;&quot;
        mysql_config = self.config[&apos;mysql&apos;]

        cmd = [
            &apos;mysql&apos;,
            &apos;--host&apos;, mysql_config[&apos;host&apos;],
            &apos;--port&apos;, str(mysql_config[&apos;port&apos;]),
            &apos;--user&apos;, mysql_config[&apos;user&apos;],
            &apos;--execute&apos;, &apos;SELECT 1&apos;
        ]

        env = os.environ.copy()
        if mysql_config[&apos;password&apos;]:
            env[&apos;MYSQL_PWD&apos;] = mysql_config[&apos;password&apos;]

        try:
            result = subprocess.run(
                cmd,
                env=env,
                capture_output=True,
                text=True
            )

            if result.returncode == 0:
                self.logger.info(&quot;MySQL连接测试成功&quot;)
                return True
            else:
                self.logger.error(f&quot;MySQL连接测试失败: {result.stderr}&quot;)
                return False

        except Exception as e:
            self.logger.error(f&quot;MySQL连接测试异常: {e}&quot;)
            return False

    def list_backups(self):
        &quot;&quot;&quot;列出所有备份文件&quot;&quot;&quot;
        backup_dir = self.config[&apos;backup&apos;][&apos;backup_dir&apos;]

        if not os.path.exists(backup_dir):
            self.logger.warning(&quot;备份目录不存在&quot;)
            return []

        backups = []
        for item in sorted(os.listdir(backup_dir)):
            item_path = os.path.join(backup_dir, item)
            if os.path.isfile(item_path) and item.startswith(self.config[&apos;mysql&apos;][&apos;database&apos;]):
                stat = os.stat(item_path)
                backups.append({
                    &apos;name&apos;: item,
                    &apos;size&apos;: stat.st_size,
                    &apos;mtime&apos;: datetime.datetime.fromtimestamp(stat.st_mtime),
                    &apos;path&apos;: item_path
                })

        return backups

def main():
    parser = argparse.ArgumentParser(description=&apos;MySQL数据库备份工具&apos;)
    parser.add_argument(&apos;--config&apos;, &apos;-c&apos;, default=&apos;config.json&apos;,
                       help=&apos;配置文件路径 (默认: config.json)&apos;)
    parser.add_argument(&apos;--test&apos;, action=&apos;store_true&apos;,
                       help=&apos;测试MySQL连接&apos;)
    parser.add_argument(&apos;--list&apos;, action=&apos;store_true&apos;,
                       help=&apos;列出所有备份文件&apos;)
    parser.add_argument(&apos;--run&apos;, action=&apos;store_true&apos;,
                       help=&apos;立即执行备份&apos;)
    parser.add_argument(&apos;--daemon&apos;, action=&apos;store_true&apos;,
                       help=&apos;以守护进程方式运行，按配置定时备份&apos;)

    args = parser.parse_args()

    try:
        backup = MySQLBackup(args.config)

        if args.test:
            backup.test_mysql_connection()
        elif args.list:
            backups = backup.list_backups()
            if backups:
                print(f&quot;\n找到 {len(backups)} 个备份文件:&quot;)
                print(&quot;-&quot; * 80)
                for b in backups:
                    print(f&quot;{b[&apos;mtime&apos;]:%Y-%m-%d %H:%M:%S} | &quot;
                          f&quot;{b[&apos;size&apos;]/1024:8.1f} KB | {b[&apos;name&apos;]}&quot;)
            else:
                print(&quot;未找到备份文件&quot;)
        elif args.run:
            result = backup.backup_database()
            if result:
                print(f&quot;备份成功: {result}&quot;)
            else:
                print(&quot;备份失败，请查看日志&quot;)
                sys.exit(1)
        elif args.daemon:
            print(&quot;守护进程模式，请使用systemd或cron进行定时执行&quot;)
            # 这里可以添加循环等待逻辑，但建议使用系统定时任务
        else:
            parser.print_help()

    except Exception as e:
        print(f&quot;程序执行失败: {e}&quot;)
        sys.exit(1)

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;2、配置文件 &lt;code&gt;config.json&lt;/code&gt;&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;mysql&quot;: {
    &quot;host&quot;: &quot;localhost&quot;,
    &quot;port&quot;: 3306,
    &quot;user&quot;: &quot;root&quot;,
    &quot;password&quot;: &quot;123456&quot;,
    &quot;database&quot;: &quot;fdmtek-aps&quot;
  },
  &quot;backup&quot;: {
    &quot;backup_dir&quot;: &quot;/data1/backups/mysql&quot;,
    &quot;retention_days&quot;: 30,
    &quot;compress&quot;: true,
    &quot;compress_level&quot;: 6,
    &quot;timeout&quot;: 600
  },
  &quot;logging&quot;: {
    &quot;log_file&quot;: &quot;/var/log/mysql_backup.log&quot;,
    &quot;log_level&quot;: &quot;INFO&quot;,
    &quot;max_size_mb&quot;: 10,
    &quot;backup_count&quot;: 5
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3、验证脚本 &lt;code&gt;verify_backup.py&lt;/code&gt;&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
import subprocess
import sys
import os

def verify_backup(backup_file):
    &quot;&quot;&quot;验证备份文件&quot;&quot;&quot;
    if backup_file.endswith(&apos;.gz&apos;):
        cmd = f&quot;gzip -t {backup_file}&quot;
    else:
        # 检查SQL文件格式
        cmd = f&quot;head -n 10 {backup_file} | grep &apos;MariaDB dump&apos;&quot;

    result = subprocess.run(cmd, shell=True, capture_output=True)
    return result.returncode == 0

if __name__ == &quot;__main__&quot;:
    if len(sys.argv) &gt; 1:
        backup_file = sys.argv[1]
        if verify_backup(backup_file):
            print(f&quot;✓ 备份文件有效: {backup_file}&quot;)
        else:
            print(f&quot;✗ 备份文件损坏: {backup_file}&quot;)
            sys.exit(1)
    else:
        print(&quot;使用方法: python verify_backup.py &amp;#x3C;backup_file&gt;&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;4、使用说明&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 1. 修改config.json中的数据库配置
# 2. 创建备份目录
sudo mkdir -p /data/backups/mysql
sudo chmod 755 /data/backups/mysql

# 3. 测试连接
python3 mysql_backup.py --config config.json --test

# 4. 手动执行一次备份
python3 mysql_backup.py --config config.json --run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务器定时执行脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 编辑crontab
crontab -e

# 添加以下内容，每天凌晨2点执行备份
0 2 * * * /usr/bin/python3 /data1/backups/mysql_backup/mysql_backup.py --config /data1/backups/mysql_backup/config.json --run &gt;&gt; /var/log/mysql_backup_cron.log 2&gt;&amp;#x26;1

# 查看crontab
crontab -l
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Gerrit 使用</title><link>https://astro-pure.js.org/blog/gerrit-use</link><guid isPermaLink="true">https://astro-pure.js.org/blog/gerrit-use</guid><description>Gerrit 使用指南，包含工作流程、登录、克隆仓库与提交审核等操作示例。</description><pubDate>Tue, 12 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;介绍&lt;/h1&gt;
&lt;h3&gt;关于Gerrit&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Gerrit&lt;/strong&gt;，一种开放源代码的代码审查软件，使用网页界面。利用网页浏览器，同一个团队的程序员，可以相互审阅彼此修改后的代码，决定是否能够提交，退回或是继续修改。它使用版本控制系统Git作为底层。&lt;/p&gt;
&lt;h3&gt;定位&lt;/h3&gt;
&lt;p&gt;理论上Git虽然是一个分布式版本管理系统，不需要中心代码库就能相互同步数据。而在实际的操作过程中，为了方便一个团队的多名开发人员通常需要指定一个确定的代码库用于提交和相互同步代码。所以我们的代码管理一般使用如下结构：&lt;img src=&quot;https://cdn.willimt.com/workimage.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;在引入Gerrit代码审核机制后，我们的代码提交和同步的方式发生了变化：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/workimage%201.png&quot; alt=&quot;image 1&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;工作流程&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;首先贡献者的代码通过 git 命令推送到 Gerrit 管理下的 Git 版本库，推送的提交转化为一个一个的代码审核任务，审核任务可以通过 refs/changes/下的引用访问到。代码审核者可以通过 Web 界面查看审核任务、代码变更，通过 Web 界面做出通过代码审核或者打回等决定。测试者也可以通过 refs/changes/引用获取（fetch）修订对其进行测试，如果测试通过就可以将该评审任务设置为校验通过（verified）。最后经过了审核和校验的修订可以通过 Gerrit 界面中提交动作合并到版本库对应的分支中。在 Android 项目的网站的代码贡献流程图更为详细的介绍了 Gerrit 代码审核服务器的工作流程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%202.png&quot; alt=&quot;image 2&quot;&gt;&lt;/p&gt;
&lt;h1&gt;用户登录&lt;/h1&gt;
&lt;p&gt;首先打开主页 &lt;code&gt;http://192.168.51.201:81&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;主页会提示登录：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%203.png&quot; alt=&quot;image 3&quot;&gt;&lt;/p&gt;
&lt;p&gt;其中用户名与密码皆与邮箱账号名相同，如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;username ：wanghh
password : 111111
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;登录后的界面如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%204.png&quot; alt=&quot;image 4&quot;&gt;&lt;/p&gt;
&lt;h1&gt;简单入手&lt;/h1&gt;
&lt;h2&gt;克隆代码仓库&lt;/h2&gt;
&lt;p&gt;进行代码修改、提交审查，显然第一步是要从远程代码库中克隆一份代码。Gerrit服务提供了HTTP和SSH两种服务（使用SSH协议需要配置本机密钥）供大家从远程服务器获取代码，获取方式可在项目基本信息页面查看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%205.png&quot; alt=&quot;image 5&quot;&gt;&lt;/p&gt;
&lt;p&gt;其中 clone 命令有两种类型：&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;1. 命令含 &lt;code&gt;commit-msg hook&lt;/code&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone &quot;http://wanghh%40fdmtek.com@192.168.52.46:8088/a/gerrit-base&quot; \
&amp;#x26;&amp;#x26; (cd &quot;gerrit-base&quot; &amp;#x26;&amp;#x26; mkdir -p `git rev-parse --git-dir`/hooks/ \
&amp;#x26;&amp;#x26; curl -Lo `git rev-parse --git-dir`/hooks/commit-msg http://192.168.52.46:8088/tools/hooks/commit-msg \
&amp;#x26;&amp;#x26; chmod +x `git rev-parse --git-dir`/hooks/commit-msg)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;功能：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;克隆仓库&lt;/strong&gt;：
首先，通过 &lt;code&gt;git clone&lt;/code&gt; 命令从 Gerrit 服务器克隆 &lt;code&gt;gerrit-base&lt;/code&gt; 仓库。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设置 &lt;code&gt;commit-msg hook&lt;/code&gt;&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;进入克隆的 Git 仓库目录 &lt;code&gt;gerrit-base&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;创建一个 Git Hooks 的目录：&lt;code&gt;GIT_DIR/hooks/&lt;/code&gt;。（&lt;code&gt;GIT_DIR&lt;/code&gt; 是 Git 的 &lt;code&gt;.git&lt;/code&gt; 文件夹路径，由 &lt;code&gt;git rev-parse --git-dir&lt;/code&gt; 获取）。&lt;/li&gt;
&lt;li&gt;下载 &lt;code&gt;commit-msg&lt;/code&gt; hook：使用 &lt;code&gt;curl&lt;/code&gt; 从服务器地址 &lt;code&gt;http://192.168.52.46:8088/tools/hooks/commit-msg&lt;/code&gt; 获取文件。&lt;/li&gt;
&lt;li&gt;添加执行权限：对下载的 &lt;code&gt;commit-msg&lt;/code&gt; 文件运行 &lt;code&gt;chmod +x&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;作用：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;commit-msg hook&lt;/code&gt; 是 Gerrit 为 Git 提供的一种工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查提交的消息是否符合 Gerrit 的要求。&lt;/li&gt;
&lt;li&gt;在提交消息中自动添加 &lt;strong&gt;Change-Id&lt;/strong&gt;，这是 Gerrit 用于跟踪代码变更的标识符。&lt;/li&gt;
&lt;li&gt;没有该 hook 时，手动提交可能会报错或不符合 Gerrit 的提交规范。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个命令适用于需要 Gerrit Change-Id 参与代码审查的用户，例如提交代码到 Gerrit 时必须满足其 Change-Id 要求。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;2. 命令不含 &lt;code&gt;commit-msg hook&lt;/code&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone &quot;http://wanghh%40fdmtek.com@192.168.52.46:8088/a/gerrit-base&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;功能&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此命令只是简单地克隆了指定的仓库 &lt;code&gt;gerrit-base&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;克隆后的仓库没有额外设置 &lt;code&gt;commit-msg hook&lt;/code&gt;，意味着使用默认的 Git 操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;作用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;适合仅用于拉取代码或进行简单的本地修改，而不提交代码到 Gerrit。&lt;/li&gt;
&lt;li&gt;如果用户计划把修改提交到 Gerrit，则可能会缺少 Change-Id，因此 Gerrit 后续可能拒绝提交。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;HTTP 使用需要申请 Token 用于验证&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%206.png&quot; alt=&quot;image 6&quot;&gt;&lt;/p&gt;
&lt;p&gt;同时可以顺便验证一下邮箱（&lt;em&gt;重要！&lt;/em&gt;）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%207.png&quot; alt=&quot;image 7&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  git_test git clone &quot;http://wanghh%40fdmtek.com@192.168.52.46:8088/a/gerrit-base&quot; &amp;#x26;&amp;#x26; (cd &quot;gerrit-base&quot; &amp;#x26;&amp;#x26; mkdir -p `git rev-parse --git-dir`/hooks/ &amp;#x26;&amp;#x26; curl -Lo `git rev-parse --git-dir`/hooks/commit-msg http://192.168.52.46:8088/tools/hooks/commit-msg &amp;#x26;&amp;#x26; chmod +x `git rev-parse --git-dir`/hooks/commit-msg)
Cloning into &apos;gerrit-base&apos;...
Password for &apos;http://wanghh%40fdmtek.com@192.168.52.46:8088&apos;:
remote: Counting objects: 2, done
remote: Finding sources: 100% (2/2)
Receiving objects: 100% (2/2), 197 bytes | 197.00 KiB/s, done.
remote: Total 2 (delta 0), reused 0 (delta 0)
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3127  100  3127    0     0  1737k      0 --:--:-- --:--:-- --:--:-- 3053k
➜  git_test cd gerrit-base
➜  gerrit-base git:(master) git status
On branch master
Your branch is up to date with &apos;origin/master&apos;.

nothing to commit, working tree clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仓库 clone 完成之后就可以进行后续的操作：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  gerrit-base git:(master) ✗ git add .
➜  gerrit-base git:(master) ✗ git commit -m &quot;v0.0.1&quot;
[master 8d921fc] v0.0.1
 1 file changed, 1 insertion(+)
 create mode 100644 test.txt
➜  gerrit-base git:(master) git push
Password for &apos;http://wanghh%40fdmtek.com@192.168.52.46:8088&apos;:
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Writing objects: 100% (3/3), 274 bytes | 274.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote: error: branch refs/heads/master:
remote: Push to refs/for/master to create a review, or get &apos;Push&apos; rights to update the branch.
remote: User: wanghh@fdmtek.com
remote: Contact an administrator to fix the permissions
remote: Processing changes: refs: 1, done
To http://192.168.52.46:8088/a/gerrit-base
 ! [remote rejected] master -&gt; master (prohibited by Gerrit: not permitted: update)
error: failed to push some refs to &apos;http://192.168.52.46:8088/a/gerrit-base&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时需要的密码依然是之前生成的 Token，但是可以发现 Push 操作被拒绝了。这是因为仓库中的权限没有开放，并且通常无法直接 push 到 master，需要首先 push 到 gerrit 的分支上，如 &lt;code&gt;refs/for/master&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%208.png&quot; alt=&quot;image 8&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%209.png&quot; alt=&quot;image 9&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时，我们就可以进行提交了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  gerrit-base git:(master) git push origin HEAD:refs/for/master
Password for &apos;http://wanghh%40fdmtek.com@192.168.52.46:8088&apos;:
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Writing objects: 100% (3/3), 274 bytes | 274.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Processing changes: refs: 1, new: 1, done
remote:
remote: SUCCESS
remote:
remote:   http://192.168.52.46:8088/c/gerrit-base/+/1 v0.0.1 [NEW]
remote:
To http://192.168.52.46:8088/a/gerrit-base
 * [new reference]   HEAD -&gt; refs/for/master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;提交成功之后就可以在gerrit中看到变化：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%2010.png&quot; alt=&quot;image 10&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%2011.png&quot; alt=&quot;image 11&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到当前是不满足 submit 条件的。需要进行 Review 与 Verify。&lt;/p&gt;
&lt;h2&gt;审查代码&lt;/h2&gt;
&lt;p&gt;默认情况下项目的所有者、管理员能够指定代码的审核人。指定审核人后，Gerrit会给审核人发送邮件提醒有审核任务。&lt;/p&gt;
&lt;p&gt;审核代码的流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;浏览代码增量，并给出注释;&lt;/li&gt;
&lt;li&gt;浏览完成所有代码，并给出综合评价和评分(-2, -1, 0, 1, 2)。只有打分&gt;2，才能允许merge到目标分支。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个时候，我们想起允哥说有空的时候就可以想起他：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%2012.png&quot; alt=&quot;image 12&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时，电脑另一端的允哥也就收到了干活的消息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/aa7aebc6-34e7-4790-9ed2-e17fca056cfa.png&quot; alt=&quot;aa7aebc6-34e7-4790-9ed2-e17fca056cfa&quot;&gt;&lt;/p&gt;
&lt;p&gt;允哥打开 gerrit 后，也看到新的 commit&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%2013.png&quot; alt=&quot;image 13&quot;&gt;&lt;/p&gt;
&lt;p&gt;随后允哥觉得小伙子写的很不错！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%2014.png&quot; alt=&quot;image 14&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是这时高老爷也过来看了一下，说小伙子菜还得多练啊&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%2015.png&quot; alt=&quot;image 15&quot;&gt;&lt;/p&gt;
&lt;h2&gt;返工修改&lt;/h2&gt;
&lt;p&gt;根据上面Gerrit流程图，当代码审核、确认未通过或合并的过程中出现冲突，这个时候就需要返工修改后再提交。&lt;/p&gt;
&lt;p&gt;返工流程有三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取未通过的代码到本地工作目录；&lt;/li&gt;
&lt;li&gt;修改代码；&lt;/li&gt;
&lt;li&gt;再提交审核。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;修改好代码之后我们再次提交&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  gerrit-base git:(master) ✗ git add .
➜  gerrit-base git:(master) ✗ git commit --amend
[master 2a84938] v0.0.1
 Date: Fri Jan 9 14:14:47 2026 +0800
 1 file changed, 3 insertions(+)
 create mode 100644 test.txt

// push patch set
➜  gerrit-base git:(master) git push origin HEAD:refs/for/master
Password for &apos;http://wanghh%40fdmtek.com@192.168.52.46:8088&apos;:
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Writing objects: 100% (3/3), 301 bytes | 301.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Processing changes: refs: 1, updated: 1, done
remote:
remote: SUCCESS
remote:
remote:   http://192.168.52.46:8088/c/gerrit-base/+/1 v0.0.1
remote:
remote: The following approvals got outdated and were removed:
remote: * Code-Review+1 by guoy@fdmtek.com
remote: * Code-Review+1 by wanghh@fdmtek.com
remote:
remote:
To http://192.168.52.46:8088/a/gerrit-base
 * [new reference]   HEAD -&gt; refs/for/master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后的提交，change-id不会发生变化，而仅仅是patch-id加1。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%2016.png&quot; alt=&quot;image 16&quot;&gt;&lt;/p&gt;
&lt;p&gt;这一次，高老爷也满意了。至此，我们完成了代码的 Review。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;验证修改&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;新提交的修改，如果已经通过代码审核，下一步流程则是代码验证。验证之前需要代码测试人员先将修改获取到本地，然后对代码进行测试。代码测试后，则投票对本次修改做出评价。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%2017.png&quot; alt=&quot;image 17&quot;&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;提交修改&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;当代码成功通过Review和Verified后，此时这段修改后的代码片段就可以合并到主分支里面去了。&lt;/p&gt;
&lt;p&gt;这时候，高老爷动了动小手：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%2018.png&quot; alt=&quot;image 18&quot;&gt;&lt;/p&gt;
&lt;p&gt;代码这时真正被同步到了 master 分支中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/work/image%2019.png&quot; alt=&quot;image 19&quot;&gt;&lt;/p&gt;
&lt;p&gt;查看 &lt;a href=&quot;https://gerrit-documentation.storage.googleapis.com/Documentation/3.13.1/intro-user.html&quot;&gt;官方文档&lt;/a&gt; 了解更多&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;另：&lt;/p&gt;
&lt;p&gt;Gerrit 的 &lt;code&gt;HTTP&lt;/code&gt; 验证通常依赖外部的反向代理来处理用户的认证，客户端通过浏览器发送 HTTP basic authentication 的头部信息来完成验证。因此：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户的登录状态由浏览器缓存的 Basic Authentication 凭据维持，不存储在 Gerrit 中。&lt;/li&gt;
&lt;li&gt;用户点击 &lt;code&gt;Sign out&lt;/code&gt; 后，Gerrit 本身无法清除浏览器上缓存的凭据。&lt;/li&gt;
&lt;li&gt;此外，现代浏览器的 HTTP Basic Auth 无法通过 JS 或页面行为主动触发清理，退出登录变得较为困难。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;**即：*浏览器缓存了 Basic Authentication 凭证，*&lt;em&gt;即使点击了 &lt;code&gt;Sign out&lt;/code&gt;，浏览器依然用之前缓存的凭据重新发送认证请求，导致用户无法登出。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;基于较新的火狐内核支持特定形式的 URL 处理，因此通过反代的配置修改能够清除缓存的 HTTP Basic Authentication 会话，解决此问题。但是基于 chromium 内核的浏览器可能会依然存在这个问题，如果你拒绝使用火狐且坚持需要退出用户登录的话，请自行清理浏览器缓存记录。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Gerrit 安装</title><link>https://astro-pure.js.org/blog/gerrit-install</link><guid isPermaLink="true">https://astro-pure.js.org/blog/gerrit-install</guid><description>Gerrit 安装与配置指南，包含依赖、安装步骤和常见问题处理。</description><pubDate>Mon, 11 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;安装&lt;/h1&gt;
&lt;h2&gt;安装 JDK&lt;/h2&gt;
&lt;p&gt;Gerrit 基于 Java 开发，因此依赖 JAVA 的运行环境，当前最新 3.13 版本要求至少 Java 21。因此需要先安装 JRE。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 创建jdk存放目录
mkdir /usr/local/java

# 将压缩包解压到上述目录当中
tar -zxvf jdk-21_linux-64_bin.tar.gz -C /usr/local/java

# 创建软连接
# 需要先删除系统可能自带的软连接
# 然后重新创建 java 软链接到 /usr/bin/ 目录下
ln -s /usr/local/java/jdk-21.0.5/ /usr/bin/java

# 编辑系统配置文件
em /etc/profile

# 输入以下内容
export JAVA_HOME=/usr/bin/java
export PATH=$JAVA_HOME/bin:$PATH

# 激活配置文件
source /etc/profile

java --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装 Gerrit&lt;/h2&gt;
&lt;p&gt;Gerrit 账号创建并使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo adduser gerrit

sudo su gerrit
cd /home/gerrit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Gerrit 安装&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java -jar gerrit-3.10.1.war init -d /home/gerrit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装时初始参数：&lt;/p&gt;
&lt;p&gt;Auth 方式输入 HTTP，并使用反向代理；&lt;/p&gt;
&lt;p&gt;Plugin 插件都选 y&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;配置用户：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;htpasswd -c /home/gerrit/gerrit/etc/gerrit.password admin     # 创建第一个用户admin，同时会生成一个gerrit.password文件
htpasswd -m /home/gerrit/gerrit/etc/gerrit.password user1     # 在gerrit.password增加用户用 -m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时 &lt;code&gt;gerrit/etc/gerrit.conf&lt;/code&gt; 配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[gerrit]
        basePath = git
        canonicalWebUrl = &amp;#x3C;http://192.168.52.46:8088&gt;
        serverId = 3d9af676-03aa-4cc6-a75c-98d76682443e
[container]
        javaOptions = &quot;-Dflogger.backend_factory=com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance&quot;
        javaOptions = &quot;-Dflogger.logging_context=com.google.gerrit.server.logging.LoggingContext#getInstance&quot;
        user = gerrit
        javaHome = /usr/local/java/jdk-25.0.1
[index]
        type = lucene
[auth]
        type = HTTP
        userNameCaseInsensitive = true
[receive]
        enableSignedPush = false
[sendemail]
        smtpServer = 192.168.52.46
        smtpServerPort = 25
        smtpUser = gerrit
[sshd]
        listenAddress = *:29418
[httpd]
        listenUrl = proxy-http://*:8088/
[cache]
        directory = cache
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;配置反向代理&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;设置 nginx&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;em /etc/nginx/conf.d/gerrit.conf&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;server {
    listen 81;            # 代理监听81端口
    server_name 192.168.52.46;

    auth_basic &quot;Welcome to Gerrit Code Review Site!&quot;;
    auth_basic_user_file /home/gerrit/gerrit/etc/gerrit.password;       # 代理使用 gerrit.password
    location / {
        proxy_pass  &amp;#x3C;http://192.168.52.46:8088&gt;;        # 代理指向gerrit本地服务url,这个url要和gerrit.config中的canonicalWebUrl相同
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header Cache-Control &quot;no-store&quot;;
        proxy_set_header Pragma &quot;no-cache&quot;;
        proxy_set_header Expires &quot;-1&quot;;
    }

    location = /logout {
        return 302 &amp;#x3C;http://logout:logout@192.168.52.46:81/&gt;;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;location = /logout {
        return 302 &amp;#x3C;http://logout:logout@192.168.52.46:81/&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一部分是因为：&lt;/p&gt;
&lt;p&gt;Gerrit 的 &lt;code&gt;HTTP&lt;/code&gt; 验证通常依赖外部的反向代理（如 Nginx、Apache）来处理用户的认证，客户端通过浏览器发送 HTTP basic authentication 的头部信息（&lt;code&gt;Authorization: Basic &amp;#x3C;credentials&gt;&lt;/code&gt;）来完成验证。&lt;/p&gt;
&lt;p&gt;HTTP 验证的特点是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户的登录状态由浏览器缓存的 Basic Authentication 凭据维持，不存储在 Gerrit 中。&lt;/li&gt;
&lt;li&gt;用户点击 &lt;code&gt;Sign out&lt;/code&gt; 后，Gerrit 本身无法清除浏览器上缓存的凭据。&lt;/li&gt;
&lt;li&gt;此外，现代浏览器的 HTTP Basic Auth 无法通过 JS 或页面行为主动触发清理，退出登录变得较为困难。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;**浏览器缓存了 Basic Authentication 凭证，**即使点击了 &lt;code&gt;Sign out&lt;/code&gt;，浏览器依然用之前缓存的凭据重新发送认证请求，导致用户无法登出。&lt;/p&gt;
&lt;p&gt;基于火狐内核的解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器会对包含 &lt;code&gt;logout:logout@&lt;/code&gt; 这样形式的 URL 自动处理，直接向目标服务器发送带有 &lt;code&gt;Authorization&lt;/code&gt; 头的请求。&lt;/li&gt;
&lt;li&gt;因为提供的用户名和密码（&lt;code&gt;logout:logout&lt;/code&gt;）在服务器端被拒绝，导致浏览器认为当前凭据已经失效，从而清除缓存的 HTTP Basic Authentication 会话。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>KLEE 使用</title><link>https://astro-pure.js.org/blog/klee</link><guid isPermaLink="true">https://astro-pure.js.org/blog/klee</guid><description>KLEE 符号执行工具快速上手与运行示例。</description><pubDate>Wed, 11 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;使用&lt;/h1&gt;
&lt;p&gt;klee因为其复杂性，依赖比较多。除了llvm,cmake之类的通用依赖之外，还需要为其编译SAT求解器minisat和SMT求解器stp。因此为了简洁使用，可以通过官方提供的docker来使用，通过以下命令即可使用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker run -ti --name=my_first_klee_container --ulimit=&apos;stack=-1:-1&apos; klee/klee:3.0

docker start -aiklee_container
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;KLEE 在 LLVM 位码上运行。要使用 KLEE 运行程序，首先使用 将其编译为 LLVM 位码&lt;code&gt;clang -emit-llvm&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;clang -I ../klee_build/include -emit-llvm -g -O0 -Xclang -disable-O0-optnone -c test.c -o test.bc
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;运行 KLEE&lt;/h2&gt;
&lt;p&gt;要在位码文件上运行 KLEE，只需执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ klee get_sign.bc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应该看到类似以下输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;KLEE: output directory = &quot;klee-out-0&quot;

KLEE: done: total instructions = 33
KLEE: done: completed paths = 3
KLEE: done: partially completed paths = 0
KLEE: done: generated tests = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ ls klee-last/
assembly.ll      run.istats       test000002.ktest
info             run.stats        test000003.ktest
messages.txt     test000001.ktest warnings.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;KLEE 生成的测试用例&lt;/h2&gt;
&lt;p&gt;KLEE 生成的测试用例被写入名为&lt;code&gt;.ktest&lt;/code&gt;扩展的文件中。这些是二进制文件，可以使用_ktest-tool_实用程序读取。ktest -tool_输出同一对象的不同表示形式，例如 Python 字节字符串（数据）、整数（int）或 ascii 文本（text）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ ktest-tool klee-last/test000001.ktest
ktest file : &apos;klee-last/test000001.ktest&apos;
args       : [&apos;get_sign.bc&apos;]
num objects: 1
object 0: name: &apos;a&apos;
object 0: size: 4
object 0: data: b&apos;\x00\x00\x00\x00&apos;
object 0: hex : 0x00000000
object 0: int : 0
object 0: uint: 0
object 0: text: ....
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;执行测试用例&lt;/h2&gt;
&lt;p&gt;虽然我们可以手动（或借助现有测试基础架构）在程序上运行 KLEE 生成的测试用例，但 KLEE 提供了一个方便的&lt;strong&gt;重放库&lt;/strong&gt;，它只是将对的调用替换&lt;code&gt;klee_make_symbolic&lt;/code&gt;为对函数的调用，该函数将存储在文件中的值分配给我们的输入&lt;code&gt;.ktest&lt;/code&gt;。要使用它，只需将程序与&lt;code&gt;libkleeRuntest&lt;/code&gt;库链接起来，并将环境变量设置&lt;code&gt;KTEST_FILE&lt;/code&gt;为指向所需测试用例的名称。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ export LD_LIBRARY_PATH=path-to-klee-build-dir/lib/:$LD_LIBRARY_PATH
$ gcc -I ../../include -L path-to-klee-build-dir/lib/ get_sign.c -lkleeRuntest
$ KTEST_FILE=klee-last/test000001.ktest ./a.out
$ echo $?
0
$ KTEST_FILE=klee-last/test000002.ktest ./a.out
$ echo $?
1
$ KTEST_FILE=klee-last/test000003.ktest ./a.out
$ echo $?
255
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;KLEE 的符号执行引擎确实支持浮点数（如 float 和 double 类型），但是其处理能力是有限的。KLEE 原生并不完全支持浮点数的符号执行，尤其是在精确度和操作的复杂度上，可能会遇到一些困难。这是因为符号执行主要设计时主要针对整数类型的符号执行进行了优化，而浮点数涉及的数值范围、舍入误差等问题，使得其符号执行变得更加复杂。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KLEE 对浮点数的处理&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;符号执行支持&lt;/strong&gt;：
KLEE 确实能够进行浮点数的符号执行，但在浮点数计算时，其对某些操作（如浮点比较、算术运算等）可能没有与整数运算一样的精确支持。这意味着 KLEE 在遇到浮点数时可能无法像处理整数那样精确地模拟计算过程，特别是在符号约束（symbolic constraints）和符号路径（symbolic path）上。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;浮点数具体化（Concretization）&lt;/strong&gt;：
如果 KLEE 遇到无法精确符号执行的浮点数表达式，它通常会将浮点数的值“具体化”为一个常数（如 0）。例如，如果代码中涉及到浮点数的算术运算或比较，KLEE 可能会将浮点数的值设置为一个默认的数值（通常是 0），以避免符号执行失败。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;浮点数操作的精度问题&lt;/strong&gt;：
浮点数计算涉及舍入误差和精度问题，这对于符号执行来说是一个挑战。KLEE 在浮点数计算时可能无法精确模拟这些操作，尤其是对于复杂的浮点数函数（如三角函数、对数等）来说，符号执行的表现可能不如整数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;浮点数的优化和支持&lt;/strong&gt;：
KLEE 团队已经进行了一些优化，尤其是在浮点数符号执行方面。具体来说，KLEE 使用了一个叫 SymFPU 的浮点数支持库，来改进对浮点数操作的处理。SymFPU 是一个专门用于浮点数符号执行的模块，它能够处理浮点数的符号执行，尤其是在有限的操作和函数支持上有所改进。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>软件测试</title><link>https://astro-pure.js.org/blog/software-test</link><guid isPermaLink="true">https://astro-pure.js.org/blog/software-test</guid><description>单元测试与覆盖率指南，介绍 gcov、lcov、gcovr 的使用与报告生成示例。</description><pubDate>Wed, 04 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;单元测试&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Gcov&lt;/code&gt;是一个测试覆盖程序，是集成在&lt;code&gt;GCC&lt;/code&gt;中的，随&lt;code&gt;GCC&lt;/code&gt;一起发布。&lt;/p&gt;
&lt;h3&gt;基本概念&lt;/h3&gt;
&lt;h4&gt;基本块BB&lt;/h4&gt;
&lt;p&gt;基本块指一段程序的第一条语句被执行过一次后，这段程序中的每一跳语句都需要执行一次，称为基本块，因此基本块中的所有语句的执行次数是相同的，一般由多个顺序执行语句后边跟一个跳转语句组成&lt;/p&gt;
&lt;h4&gt;跳转ARC&lt;/h4&gt;
&lt;p&gt;从一个&lt;code&gt;BB&lt;/code&gt;到另外一个&lt;code&gt;BB&lt;/code&gt;的跳转叫做一个&lt;code&gt;ARC&lt;/code&gt;,要想知道程序中的每个语句和分支的执行次数，就必须知道每个&lt;code&gt;BB&lt;/code&gt;和&lt;code&gt;ARC&lt;/code&gt;的执行次数&lt;/p&gt;
&lt;h4&gt;程序流图&lt;/h4&gt;
&lt;p&gt;如果把&lt;code&gt;BB&lt;/code&gt;作为一个节点，这样一个函数中的所有&lt;code&gt;BB&lt;/code&gt;就构成了一个有向图，要想知道程序中的每个语句和分支的执行次数，就必须知道每个&lt;code&gt;BB&lt;/code&gt;和&lt;code&gt;ARC&lt;/code&gt;的执行次数，根据图论可以知道有向图中&lt;code&gt;BB&lt;/code&gt;的入度和出度是相同的，所以只要知道了部分的&lt;code&gt;BB&lt;/code&gt;或者&lt;code&gt;ARC&lt;/code&gt;大小，就可以推断所有的大小，这里选择由&lt;code&gt;ARC&lt;/code&gt;的执行次数来推断&lt;code&gt;BB&lt;/code&gt;的执行次数，所以对部分&lt;code&gt;ARC&lt;/code&gt;插桩，只要满足可以统计出来所有的&lt;code&gt;BB&lt;/code&gt;和&lt;code&gt;ARC&lt;/code&gt;的执行次数即可&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;原理&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;测试程序首先进行编译预处理，生成汇编文件，并完成插桩，插桩的过程中会向源文件的末尾插入一个静态数组，数组的大小就是这个源文件中桩点的个数，数组的值就是桩点的执行次数，每个桩点插入3~4条汇编语句，直接插入生成的&lt;code&gt;*.s&lt;/code&gt;文件中，最后汇编文件经过汇编生成目标文件，在程序运行过程中桩点负责收集程序的执行信息&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;h3&gt;编译&lt;/h3&gt;
&lt;p&gt;测试代码如下： &lt;code&gt;say.c&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

int say(char *what) {
    printf(&quot;------ %s\n&quot;, what);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;main.c:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
​
extern int say(const char *);
​
int main(int argc, const char *argv[]) {

 if (argv[1]) {
	 say(&quot;hello&quot;);
 } else {
	 say(&quot;bye&quot;);
 }
 return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加&lt;code&gt;-fprofile-arcs -ftest-coverage -fPIC&lt;/code&gt;编译参数编译程序，生成可执行程序和&lt;code&gt;*.gcno&lt;/code&gt;文件，里面记录了行信息和程序流图信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ gcc -fprofile-arcs -ftest-coverage -fPIC -O0 say.c main.c

$ ls
a.out  main.c  main.gcno  say.c  say.gcno
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数据收集&lt;/h3&gt;
&lt;p&gt;运行可执行文件，生成&lt;code&gt;*.gcda&lt;/code&gt;在默认生成在相应&lt;code&gt;*.o&lt;/code&gt;文件目录，里面记录了&lt;code&gt;*.c&lt;/code&gt;文件中程序的执行情况，包括跳变次数等:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt; $ ./a.out
 ------ bye
 ​
 $ ls
 a.out  main.c  main.gcda  main.gcno  say.c  say.gcda  say.gcno
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;报告生成&lt;/h3&gt;
&lt;p&gt;针对某一个文件的执行情况，可以通过如下命令生成报告，并创建&lt;code&gt;*.gcov&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ gcov -a main.c
File &apos;main.c&apos;
Lines executed:80.00% of 5
Creating &apos;main.c.gcov&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常用选项，更多可参考&lt;a href=&quot;https://gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html#Invoking-Gcov&quot;&gt;Invoking gcov&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; -b：分支覆盖
 -a：所有基本块覆盖
 -f：函数覆盖
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;注意事项&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在编译时不要加优化选项，否则代码会发生变化，无法准确定位&lt;/li&gt;
&lt;li&gt;代码中复杂的宏，比如宏展开后是循环或者其他控制结构，可以用内联函数来代替，因为&lt;code&gt;gcov&lt;/code&gt;只统计宏调用出现的那一行&lt;/li&gt;
&lt;li&gt;代码每一行最好只有一条语句&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*.gcno&lt;/code&gt;与&lt;code&gt;*.gcda&lt;/code&gt;需要匹配，两个文件是有时间戳来记录是不是匹配的&lt;/li&gt;
&lt;li&gt;若是编译动态库，需要在链接时&lt;code&gt;-lgcov&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;图形化展示&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;gcov&lt;/code&gt;生成的报告分散在各个源码文件所对应的&lt;code&gt;*.gcov&lt;/code&gt;文件中，难以汇总分析，并且可视化效果较差，所以需要转化成可视图形化报告，有&lt;code&gt;lcov&lt;/code&gt;或&lt;code&gt;gcovr&lt;/code&gt;两个工具可以完成，两者功能基本相同，&lt;code&gt;gcovr&lt;/code&gt;，是一个用&lt;code&gt;Python&lt;/code&gt;编写的开源软件，大小只有几十KB.&lt;/p&gt;
&lt;h4&gt;列表形式&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;代码覆盖率&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ gcovr -r .
------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: .
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
main.c                                         5       4    80%   15
say.c                                          3       3   100%
------------------------------------------------------------------------------
TOTAL                                          8       7    87%
------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报告展示程序运行后覆盖了&lt;code&gt;80%&lt;/code&gt;的代码&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;分支覆盖率&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ gcovr -b -r .
------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: .
------------------------------------------------------------------------------
File                                    Branches   Taken  Cover   Missing
------------------------------------------------------------------------------
main.c                                         2       1    50%   14
say.c                                          0       0    --%
------------------------------------------------------------------------------
TOTAL                                          2       1    50%
------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报告展示了在&lt;code&gt;main.c&lt;/code&gt;中有一个分支没有执行到&lt;/p&gt;
&lt;h4&gt;HTML文件形式&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ gcovr -r . --html -o xxx.html
$ ls
a.out  main.c  main.gcda  main.gcno  say.c  say.gcda  say.gcno  xxx.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现添加&lt;code&gt;--html&lt;/code&gt;参数后，可以生成&lt;code&gt;html&lt;/code&gt;文件，用浏览器打开，如下图：
&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/gcovr_xxx.png&quot; alt=&quot;gcovr_xxx.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;还可以添加&lt;code&gt;--html-details&lt;/code&gt;选项，为每个代码文件单独生成&lt;code&gt;html&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ gcovr -r . --html --html-details -o xxx.html
$ ls
a.out  main.c  main.gcda  main.gcno  say.c  say.gcda  say.gcno  xxx.html  xxx.main.c.html  xxx.say.c.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现多了&lt;code&gt;xxx.main.c.html&lt;/code&gt;和&lt;code&gt;xxx.say.c.html&lt;/code&gt;，用浏览器打开&lt;code&gt;xxx.html&lt;/code&gt;，如下图：
&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/gcovr_xxx_detail.png&quot; alt=&quot;gcovr_xxx_detail.png&quot;&gt;
文件名较之前带上了下划线，单击文件名，可以看到具体的代码覆盖情况，如下图：
&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/gcovr_xxx_main.png&quot; alt=&quot;gcovr_xxx_main.png|612&quot;&gt;&lt;/p&gt;
&lt;h1&gt;测试样例生成&lt;/h1&gt;
&lt;p&gt;当前的大型商用代码测试软件包括了测试样例自动生成，调研了老牌的测试软件Parasoft C/C++ test与国产新推出的测试工具wings。&lt;/p&gt;
&lt;p&gt;| 对比方面      | wings                          | C/C++ test                |
| --------- | ------------------------------ | ------------------------- |
| 测试样例生成灵活性 | 可以根据需求，修改赋值次数                  | 基于Google Test框架，一次性生成固定组值 |
| 测试类型      | 见下方表格                          | 见下方表格                     |
| 生成方式      | 基于xml的值填充                      | 基于Google Test框架的测试样例代码    |
| 应用范围      | 国产替代，使用较少                      | 市场占比极大                    |
| 获取难易      | 使用依赖于厂家的数据库格式，需要厂家提供转换插件，获取难度大 | 有破解版，可用                   |&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20250222144740738.png&quot; alt=&quot;image-20250222144740738|221&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>VSCode 配置 TeX/LaTeX 开发环境</title><link>https://astro-pure.js.org/blog/tex-vscode</link><guid isPermaLink="true">https://astro-pure.js.org/blog/tex-vscode</guid><description>记录在 VSCode 中配置 TeX Live 与 LaTeX Workshop 的过程，包括安装校验、插件配置与常见设置。</description><pubDate>Thu, 27 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一、检查TEX&lt;/h2&gt;
&lt;p&gt;通过命令： &lt;code&gt;tex -version&lt;/code&gt;  来检查当前用户目录下TexLive是否可用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(base) [wanghh@platform_fat01 tex_test]$ tex -version
TeX 3.141592653 (TeX Live 2025)
kpathsea version 6.4.1
Copyright 2025 D.E. Knuth.
There is NO warranty.  Redistribution of this software is
covered by the terms of both the TeX copyright and
the Lesser GNU General Public License.
For more information about these matters, see the file
named COPYING and the TeX source.
Primary author of TeX: D.E. Knuth.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or
检查软件路径是否有效&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(base) [wanghh@platform_fat01 tex_test]$ which tex
/usr/local/texlive/2025/bin/x86_64-linux/tex
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、VSCode安装 Latex Workshop插件&lt;/h2&gt;
&lt;p&gt;插件存放地址为 &lt;code&gt;/home/wanghh/Public/vsx&lt;/code&gt;
插件安装后重启VSCode，随后新建并打开一个.tex文件，latex插件即可在左侧栏出现。插件使用比较简单，可自行尝试。&lt;/p&gt;
&lt;p&gt;使用插件编辑latex前，需要配置一下VSCode的环境，打开setting.json后，添加下列配置即可：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;	&quot;latex-workshop.latex.autoBuild.run&quot;: &quot;never&quot;,
    &quot;latex-workshop.showContextMenu&quot;: true,
    &quot;latex-workshop.intellisense.package.enabled&quot;: true,
    &quot;latex-workshop.message.error.show&quot;: false,
    &quot;latex-workshop.message.warning.show&quot;: false,
    &quot;latex-workshop.latex.tools&quot;: [
        {
            &quot;name&quot;: &quot;xelatex&quot;,
            &quot;command&quot;: &quot;xelatex&quot;,
            &quot;args&quot;: [
                &quot;-synctex=1&quot;,
                &quot;-interaction=nonstopmode&quot;,
                &quot;-file-line-error&quot;,
                &quot;%DOCFILE%&quot;
            ]
        },
        {
            &quot;name&quot;: &quot;pdflatex&quot;,
            &quot;command&quot;: &quot;pdflatex&quot;,
            &quot;args&quot;: [
                &quot;-synctex=1&quot;,
                &quot;-interaction=nonstopmode&quot;,
                &quot;-file-line-error&quot;,
                &quot;%DOCFILE%&quot;
            ]
        },
        {
            &quot;name&quot;: &quot;latexmk&quot;,
            &quot;command&quot;: &quot;latexmk&quot;,
            &quot;args&quot;: [
                &quot;-synctex=1&quot;,
                &quot;-interaction=nonstopmode&quot;,
                &quot;-file-line-error&quot;,
                &quot;-pdf&quot;,
                &quot;-outdir=%OUTDIR%&quot;,
                &quot;%DOCFILE%&quot;
            ]
        },
        {
            &quot;name&quot;: &quot;bibtex&quot;,
            &quot;command&quot;: &quot;bibtex&quot;,
            &quot;args&quot;: [
                &quot;%DOCFILE%&quot;
            ]
        }
    ],
    &quot;latex-workshop.latex.recipes&quot;: [
        {
            &quot;name&quot;: &quot;XeLaTeX&quot;,
            &quot;tools&quot;: [
                &quot;xelatex&quot;
            ]
        },
        {
            &quot;name&quot;: &quot;PDFLaTeX&quot;,
            &quot;tools&quot;: [
                &quot;pdflatex&quot;
            ]
        },
        {
            &quot;name&quot;: &quot;BibTeX&quot;,
            &quot;tools&quot;: [
                &quot;bibtex&quot;
            ]
        },
        {
            &quot;name&quot;: &quot;LaTeXmk&quot;,
            &quot;tools&quot;: [
                &quot;latexmk&quot;
            ]
        },
        {
            &quot;name&quot;: &quot;xelatex -&gt; bibtex -&gt; xelatex*2&quot;,
            &quot;tools&quot;: [
                &quot;xelatex&quot;,
                &quot;bibtex&quot;,
                &quot;xelatex&quot;,
                &quot;xelatex&quot;
            ]
        },
        {
            &quot;name&quot;: &quot;pdflatex -&gt; bibtex -&gt; pdflatex*2&quot;,
            &quot;tools&quot;: [
                &quot;pdflatex&quot;,
                &quot;bibtex&quot;,
                &quot;pdflatex&quot;,
                &quot;pdflatex&quot;
            ]
        },
    ],
    &quot;latex-workshop.latex.clean.fileTypes&quot;: [
        &quot;*.aux&quot;,
        &quot;*.bbl&quot;,
        &quot;*.blg&quot;,
        &quot;*.idx&quot;,
        &quot;*.ind&quot;,
        &quot;*.lof&quot;,
        &quot;*.lot&quot;,
        &quot;*.out&quot;,
        &quot;*.toc&quot;,
        &quot;*.acn&quot;,
        &quot;*.acr&quot;,
        &quot;*.alg&quot;,
        &quot;*.glg&quot;,
        &quot;*.glo&quot;,
        &quot;*.gls&quot;,
        &quot;*.ist&quot;,
        &quot;*.fls&quot;,
        &quot;*.log&quot;,
        &quot;*.fdb_latexmk&quot;
    ],
    &quot;latex-workshop.latex.autoClean.run&quot;: &quot;onFailed&quot;,
    &quot;latex-workshop.latex.recipe.default&quot;: &quot;lastUsed&quot;,
    &quot;latex-workshop.view.pdf.internal.synctex.keybinding&quot;: &quot;double-click&quot;,
    &quot;latex-workshop.view.pdf.viewer&quot;: &quot;tab&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上个人配置也可自行更改。&lt;/p&gt;
&lt;p&gt;编译选项以XeLaTeX为主（其针对中文支持较好）&lt;/p&gt;
&lt;h2&gt;三、中文字体使用说明&lt;/h2&gt;
&lt;p&gt;XeLaTeX本身对于中文的支持较为友好，其次TEX自带了一些中文字体。
通过 &lt;code&gt;fc-list :lang=zh&lt;/code&gt;命令可以查看当前已安装的中文字体
&lt;img src=&quot;https://cdn.willimt.com/work/20260423230740.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;其中&lt;code&gt;/usr/local/texlive/2025/texmf-dist/fonts/&lt;/code&gt;路径下为TEX自带的字体， &lt;code&gt;/usr/share/fonts/custom/&lt;/code&gt;路径下为我们安装的字体
主要包括以下几类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% Noto Serif CJK SC
% Source Han Serif SC
% AR PL KaitiM GB
% Source Han Sans SC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体字体细分（黑体、粗体、细体等）可根据需求修改。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Docker 使用</title><link>https://astro-pure.js.org/blog/docker-use</link><guid isPermaLink="true">https://astro-pure.js.org/blog/docker-use</guid><description>离线安装与常用操作指南，包含 Docker 与 docker-compose 的安装步骤与配置示例。</description><pubDate>Mon, 24 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;离线安装Docker, docker-compose&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一、安装docker&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1、下载docker 安装包&lt;/p&gt;
&lt;p&gt;https://download.docker.com/linux/static/stable/x86_64/&lt;/p&gt;
&lt;p&gt;选择23.0.0，与其他服务器保持一致。&lt;/p&gt;
&lt;p&gt;2、上传至服务器并解压&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;tar -zxvf docker-23.0.0.tgz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、复制刚解压的docker/目录下所有文件到/usr/bin 目录下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;cp -rf docker/* /usr/bin/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4、创建docker.service文件&lt;/p&gt;
&lt;p&gt;进入到 /usr/lib/systemd/system/ 目录下，编辑创建docker.service文件，用于管理docker服务，复制黏贴如下内容即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;vi /usr/lib/systemd/system/docker.service
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;[Unit]
Description=Docker Application Container Engine
Documentation=http://docs.docker.com
After=network.target docker.socket
[Service]
Type=notify
EnvironmentFile=-/run/flannel/docker
WorkingDirectory=/usr/local/bin
ExecStart=/usr/bin/dockerd \
                -H tcp://0.0.0.0:4243 \
                -H unix:///var/run/docker.sock \
                --selinux-enabled=false \
                --log-opt max-size=100m
ExecReload=/bin/kill -s HUP $MAINPID
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
TimeoutStartSec=0
Delegate=yes
KillMode=process
Restart=on-failure
[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5、重新加载daemon-reload&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;6、设置开机启动&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;systemctl enable docker.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;7、启动docker并查看版本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;systemctl start docker
docker version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;二、安装 docker-compose&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1、下载docker-compose 文件&lt;/p&gt;
&lt;p&gt;https://github.com/docker/compose/releases/tag/v2.18.1&lt;/p&gt;
&lt;p&gt;此处选择linux_x86_64版本&lt;/p&gt;
&lt;p&gt;2、上传至服务器并复制到指定目录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;# 复制到 /usr/local/bin
mv docker-compose-linux-x86_64 /usr/local/bin/docker-compose
# 设置可执行
chmod +x /usr/local/bin/docker-compose
# 创建软链
ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、确定是否成功&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;docker-compose version
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;docker 使用&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;# 进入容器
docker exec -it redis bash
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;# 打包镜像
docker build -t vue-app .

# 基于compose构建容器
docker-compose up -d
# 删除compose构建的容器
docker-compose down

# 重启相关容器
docker-compose restart redis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;docker ps -a&lt;/code&gt; 来检查 Redis 容器是否正在运行或已经停止，并查看其退出状态代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;docker ps -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;查看容器的日志&lt;/strong&gt; 比如查看 Redis 容器的日志以了解启动失败的具体原因&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;docker logs redis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;单独命令创建容器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;docker run -d -p 8080:8080 flowable
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;MySQL 的用户权限&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;确保 &lt;code&gt;root&lt;/code&gt; 用户或其他用户允许从外部访问。通过以下步骤检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;进入 MySQL 容器：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;docker exec -it mysql bash
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;登录 MySQL：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;mysql -u root -p
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保 MySQL 用户可以从任意主机（即 &lt;code&gt;%&lt;/code&gt;）进行访问：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;GRANT ALL PRIVILEGES ON *.* TO &apos;root&apos;@&apos;%&apos;;
FLUSH PRIVILEGES;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>QEMU 外设建模模板</title><link>https://astro-pure.js.org/blog/simulator/qemu-device</link><guid isPermaLink="true">https://astro-pure.js.org/blog/simulator/qemu-device</guid><description>结合示例讲解 QEMU 设备模型的核心结构与实现流程，包括寄存器、MMIO、IRQ 与定时器等关键要素。</description><pubDate>Tue, 28 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;QEMU 自定义外设模板&lt;/h1&gt;
&lt;p&gt;此模板为开发 QEMU 中的外设提供了指南。它包含了详细的解释和代码注释。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 引入必要的头文件&lt;/h2&gt;
&lt;p&gt;这些头文件是大多数 QEMU 设备模型所必需的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &quot;qemu/osdep.h&quot;             // OS 兼容层
#include &quot;hw/irq.h&quot;                 // 中断请求 (IRQ) 处理
#include &quot;hw/qdev-properties.h&quot;     // QEMU 设备属性管理
#include &quot;migration/vmstate.h&quot;      // 虚拟机迁移支持
#include &quot;qemu/log.h&quot;               // QEMU 日志功能
#include &quot;qemu/module.h&quot;            // QEMU 模块初始化
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;2. 定义设备状态结构体&lt;/h2&gt;
&lt;p&gt;定义一个结构体来保存自定义设备的内部状态。这包括设备所需的寄存器和状态标志。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;typedef struct CustomDeviceState {
    SysBusDevice parent_obj;   // 系统总线设备的基础结构
    uint32_t reg1;             // 示例寄存器 1
    uint32_t reg2;             // 示例寄存器 2
    uint64_t tick_offset;      // 时间或时钟偏移
    bool enable;               // 设备启用标志
    QEMUTimer *timer;          // 定时器，用于计划事件
    qemu_irq irq;              // 中断请求
    MemoryRegion iomem;        // 内存映射 I/O 区域
} CustomDeviceState;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;3. 设备初始化与重置&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;reset()&lt;/strong&gt;: 重置设备的内部状态和寄存器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;init()&lt;/strong&gt;: 初始化设备，包括内存和中断连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static void custom_device_reset(DeviceState *dev)
{
    CustomDeviceState *s = CUSTOM_DEVICE(dev);
    s-&gt;reg1 = 0;               // 将寄存器 1 重置为 0
    s-&gt;reg2 = 0;               // 将寄存器 2 重置为 0
    s-&gt;enable = true;          // 默认启用设备
}

static void custom_device_init(Object *obj)
{
    CustomDeviceState *s = CUSTOM_DEVICE(obj);

    // 初始化中断处理（GPIO 输出用于中断信号）
    qdev_init_gpio_out(DEVICE(s), &amp;#x26;s-&gt;irq, 1);

    // 初始化内存映射 I/O（MMIO）区域
    memory_region_init_io(&amp;#x26;s-&gt;iomem, obj, &amp;#x26;custom_device_ops, s, &quot;CustomDevice&quot;, 0x100);

    // 将 MMIO 区域和中断连接到系统总线
    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &amp;#x26;s-&gt;iomem);
    sysbus_init_irq(SYS_BUS_DEVICE(obj), &amp;#x26;s-&gt;irq);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;4. 实现读写函数&lt;/h2&gt;
&lt;p&gt;这些函数定义了设备如何处理对寄存器的读写请求。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 读取 MMIO 区域的函数
static uint64_t custom_device_read(void *opaque, hwaddr offset, unsigned size)
{
    CustomDeviceState *s = opaque;  // 访问设备状态
    switch (offset) {
        case 0x00:
            return s-&gt;reg1;         // 返回 reg1 的值
        case 0x04:
            return s-&gt;reg2;         // 返回 reg2 的值
        default:
            return 0;               // 无效的偏移，返回 0
    }
}

// 写入 MMIO 区域的函数
static void custom_device_write(void *opaque, hwaddr offset, uint64_t val, unsigned size)
{
    CustomDeviceState *s = opaque;  // 访问设备状态
    switch (offset) {
        case 0x00:
            s-&gt;reg1 = val;          // 将值写入 reg1
            break;
        case 0x04:
            s-&gt;reg2 = val;          // 将值写入 reg2
            break;
        default:
            break;                  // 无效的偏移，不执行操作
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;5. 定义 MemoryRegion 操作&lt;/h2&gt;
&lt;p&gt;此部分将读写函数绑定到设备的内存区域。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static const MemoryRegionOps custom_device_ops = {
    .read = custom_device_read,    // 绑定读取函数
    .write = custom_device_write,  // 绑定写入函数
    .endianness = DEVICE_NATIVE_ENDIAN,  // 使用本地字节序
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;6. 支持虚拟机迁移（可选）&lt;/h2&gt;
&lt;p&gt;如果设备需要支持虚拟机迁移，开发者需要定义哪些字段应被保存和恢复。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static const VMStateDescription vmstate_custom_device = {
    .name = &quot;custom_device&quot;,       // VM 状态名称
    .version_id = 1,               // 状态版本 ID
    .minimum_version_id = 1,       // 最小兼容版本 ID
    .fields = (VMStateField[]) {
        VMSTATE_UINT32(reg1, CustomDeviceState),  // 保存/恢复 reg1
        VMSTATE_UINT32(reg2, CustomDeviceState),  // 保存/恢复 reg2
        VMSTATE_END_OF_LIST()                     // 状态字段列表结束
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;7. 实现并注册设备&lt;/h2&gt;
&lt;p&gt;此函数实现设备的逻辑。在这里，我们初始化定时器并连接中断。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static void custom_device_realize(DeviceState *dev, Error **errp)
{
    CustomDeviceState *s = CUSTOM_DEVICE(dev);

    // 创建一个新的定时器，该定时器将在 custom_device_interrupt 函数中触发
    s-&gt;timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, custom_device_interrupt, s);

    // 获取并连接中断线路
    qemu_irq plic_irq = qdev_get_gpio_in(DEVICE(plic), 7);
    qdev_connect_gpio_out(DEVICE(s), 0, plic_irq);
}

// 初始化设备类
static void custom_device_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    dc-&gt;reset = custom_device_reset;   // 绑定重置函数
    dc-&gt;vmsd = &amp;#x26;vmstate_custom_device; // 绑定 VM 状态描述
    dc-&gt;realize = custom_device_realize; // 绑定实现函数
}

// 注册设备类型
static const TypeInfo custom_device_info = {
    .name = TYPE_CUSTOM_DEVICE,            // 设备名称
    .parent = TYPE_SYS_BUS_DEVICE,         // 父类
    .instance_size = sizeof(CustomDeviceState),  // 实例大小
    .instance_init = custom_device_init,   // 实例初始化函数
    .class_init = custom_device_class_init // 类初始化函数
};

// 将设备类型注册到 QEMU
static void custom_device_register_types(void)
{
    type_register_static(&amp;#x26;custom_device_info);  // 注册设备类型
}

// 在 QEMU 初始化时调用注册函数
type_init(custom_device_register_types);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;通过遵循此模板，开发者可以在 QEMU 中实现支持 MMIO、中断和可选迁移功能的自定义外设。你可以根据具体的设备需求修改此模板。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Nextcloud + OnlyOffice 安装与使用</title><link>https://astro-pure.js.org/blog/nextcloud</link><guid isPermaLink="true">https://astro-pure.js.org/blog/nextcloud</guid><description>Nextcloud 离线部署与 Docker/Docker-Compose 配置示例，包含 onlyoffice 集成说明。</description><pubDate>Sat, 18 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;安装nextcloud&lt;/strong&gt;&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run -d --name nextcloud --restart=always -v /home/docker/workspace/nextcloud:/var/www/html -e TZ=&quot;Asia/Shanghai&quot; -p 8080:80 nextcloud
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;安装onlyoffice服务&lt;/strong&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#安装 onlyoffice
docker run -d --restart=always --name onlyoffice -p 9002:80 -p 9001:443 onlyoffice/documentserver
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;使用docker-compose.yml&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;version: &apos;3&apos;

services:
  nextcloud:
    image: nextcloud
    container_name: nextcloud
    restart: always
    ports:
      - &quot;8080:80&quot;
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /docker/nextcloud:/var/www/html

  onlyoffice:
    image: onlyoffice/documentserver
    container_name: onlyoffice
    restart: always
    ports:
      - &quot;9002:80&quot;
      - &quot;9001:443&quot;
    volumes:
      - /docker/onlyoffice/data:/var/www/onlyoffice/Data
			# - /home/docker/workspace/onlyoffice/logs:/var/log/onlyoffice  # 日志文件
		  # - /home/docker/workspace/onlyoffice/lib:/var/lib/onlyoffice   # 数据库和应用配置
		  # - /home/docker/workspace/onlyoffice/cache:/var/cache/onlyoffice  # 缓存文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;获取onlyoffice秘钥（用于后面nextcloud的onlyoffice插件配置）&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;进入onlyoffice容器内&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;## 进入onlyoffice容器
docker exec -it onlyoffice bash

## 查看秘钥
cat /etc/onlyoffice/documentserver/local.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后将离线下载的nextcloud app的压缩包传入容器中/var/www/html/apps目录里面（即挂载数据卷的apps目录）后解压即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tar -zxvf onlyoffice.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;onlyoffice添加字体&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 将字体文件传入容器中
cp ~/方正书宋简体.ttf onlyoffice:/usr/share/fonts/truetype/custom/
# 进入容器
docker exec -it onlyoffice bash
# 执行脚本，随后刷新浏览器缓存即可
bash /usr/bin/documentserver-generate-allfonts.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;nextcloud优化&lt;/h2&gt;
&lt;h3&gt;配置缓存 APCu + redis&lt;/h3&gt;
&lt;p&gt;APCu : 配置&lt;code&gt;config.php&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&apos;memcache.local&apos;  =&gt;  &apos;\\OC\\Memcache\\APCu&apos; ,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec -it nextcloud bash

cat /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;redis ：配置&lt;code&gt;config.php&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;  &apos;memcache.locking&apos; =&gt; &apos;\\OC\\Memcache\\Redis&apos;,
  &apos;memcache.distributed&apos; =&gt; &apos;\\OC\\Memcache\\Redis&apos;,
  &apos;redis&apos; =&gt; array(
     &apos;host&apos; =&gt; &apos;49.233.160.34&apos;,
     &apos;port&apos; =&gt; 16379,
  ),
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用http2&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进入 Nextcloud 容器&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec -it nextcloud bash
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启用 &lt;code&gt;http2&lt;/code&gt; 模块&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;a2enmod http2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;后台任务&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;选择第三种corn&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在宿主机上确保 Cron 服务正在运行，使用以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;systemctl status cron
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以手动测试这条命令，看看是否正常运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec -u www-data nextcloud php -f /var/www/html/cron.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查是否已经安装：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rpm -qa | grep cronie
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动并启用 &lt;code&gt;cron&lt;/code&gt; 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl start crond
sudo systemctl enable crond
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;在宿主机的 Crontab 中添加任务&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;crontab -e&lt;/code&gt; 编辑宿主机的 cron 配置文件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;添加如下行，定期在容器内执行 &lt;code&gt;cron.php&lt;/code&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*/5 * * * * docker exec -u www-data nextcloud php -f /var/www/html/cron.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令每 5 分钟在容器内以 &lt;code&gt;www-data&lt;/code&gt; 用户身份执行 Nextcloud 的 &lt;code&gt;cron.php&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;修改php运行内存&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec -it nextcloud bash

sed -i &apos;s/memory_limit=.*/memory_limit=1024M/&apos; /usr/local/etc/php/conf.d/nextcloud.ini
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Spike 模拟器基础</title><link>https://astro-pure.js.org/blog/simulator/spike-base</link><guid isPermaLink="true">https://astro-pure.js.org/blog/simulator/spike-base</guid><description>Spike（RISC-V 参考模拟器）机制与运行原理说明</description><pubDate>Sun, 15 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;Spike机制&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Spike 仿真器模拟 RISC-V 处理器的指令执行，而宿主机是运行仿真器的物理计算机。&lt;code&gt;tohost&lt;/code&gt; 和 &lt;code&gt;fromhost&lt;/code&gt; 寄存器的通信机制允许目标系统（仿真中的 RISC-V 处理器）与宿主机之间进行数据交换，主要作用包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输入/输出（I/O）处理&lt;/strong&gt;：例如，当仿真程序需要与外部设备（如控制台、磁盘等）交互时，这些通信机制允许目标系统通过宿主机的资源来执行这些操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统调用（Syscalls）&lt;/strong&gt;：当仿真程序执行系统调用（如文件操作、内存分配等）时，&lt;code&gt;tohost&lt;/code&gt; 和 &lt;code&gt;fromhost&lt;/code&gt; 机制将系统调用的请求从目标系统传递到宿主机，由宿主机模拟相应的系统调用，并返回结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常与中断处理&lt;/strong&gt;：仿真过程中，宿主机负责处理一些来自目标系统的异常情况或中断信号，通过 &lt;code&gt;tohost&lt;/code&gt; 发送中断请求，宿主机可以模拟响应这些事件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;&lt;code&gt;tohost&lt;/code&gt; 和 &lt;code&gt;fromhost&lt;/code&gt; 寄存器的工作机制：&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tohost&lt;/code&gt; 寄存器&lt;/strong&gt;：目标系统将数据写入 &lt;code&gt;tohost&lt;/code&gt; 寄存器，通常用于发送命令或请求给宿主机。例如，仿真程序可能请求宿主机进行某种I/O操作或触发系统调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fromhost&lt;/code&gt; 寄存器&lt;/strong&gt;：宿主机通过 &lt;code&gt;fromhost&lt;/code&gt; 寄存器将响应数据传递回目标系统，例如返回系统调用的结果或提供输入数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;通信机制未启用时&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;tohost_addr&lt;/code&gt; 为零，通信未启用。在这种情况下，仿真器会简单地执行目标系统的指令而不与宿主机进行任何交互。这意味着仿真器处于一个纯粹的“裸机模式”（bare-metal mode），运行的是不需要宿主机协助的程序。&lt;/li&gt;
&lt;li&gt;在这个模式下，仿真器通过不断调用 &lt;code&gt;idle()&lt;/code&gt; 函数执行指令，直到接收到退出信号。在这种情况下，仿真程序无法与外部系统（宿主机）进行交互，所以适合用于一些不依赖 I/O 的测试程序或无操作系统的裸机程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;Spike的运行&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Spike的程序启动逻辑位于&lt;code&gt;spike.cc&lt;/code&gt;文件的主函数（&lt;code&gt;main()&lt;/code&gt;）中。包括以下关键部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数解析&lt;/strong&gt;：解析输入参数并根据这些参数设置仿真器的配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;初始化内存和处理器&lt;/strong&gt;：根据配置初始化仿真的内存和处理器核心。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加载程序&lt;/strong&gt;：将内核和初始RAM盘加载到仿真内存中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启动仿真&lt;/strong&gt;：通过&lt;code&gt;sim_t::run()&lt;/code&gt;进入主仿真循环，开始指令的执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过&lt;code&gt;sim_t::run()&lt;/code&gt;调用&lt;code&gt;htif_t::run()&lt;/code&gt;的主运行循环（它是仿真器和宿主机之间的接口主循环。&lt;code&gt;htif_t::run()&lt;/code&gt; 会不断调用 &lt;code&gt;sim_t::idle()&lt;/code&gt; 函数来推进仿真的进程。）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; int htif_t::run()
 {
   start();
 
   auto enq_func = [](std::queue&amp;#x3C;reg_t&gt;* q, uint64_t x) { q-&gt;push(x); };
   std::queue&amp;#x3C;reg_t&gt; fromhost_queue;
   std::function&amp;#x3C;void(reg_t)&gt; fromhost_callback =
     std::bind(enq_func, &amp;#x26;fromhost_queue, std::placeholders::_1);
 
   if (tohost_addr == 0) { // 裸机模式运行
     while (!signal_exit)
       idle();  // 从这里进入，在 sim_t::idle() 函数中，仿真器会执行指令并让时间前进。
   }
   ......
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sim_t::idle()&lt;/code&gt; 负责推进仿真器的时间状态，通常通过让仿真中的处理器核心执行指令。它可以处理中断、定时器事件等，模拟系统的实际行为。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; void sim_t::idle()
 {
   if (done())
     return;
 
   if (debug || ctrlc_pressed)
     interactive();
   else
     step(INTERLEAVE); // 默认  static const size_t INTERLEAVE = 5000;
 
   if (remote_bitbang)
     remote_bitbang-&gt;tick();
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;step(INTERLEAVE)&lt;/code&gt;&lt;/strong&gt;：&lt;code&gt;idle()&lt;/code&gt; 函数调用 &lt;code&gt;sim_t::step()&lt;/code&gt; 函数来推进仿真器的执行。&lt;code&gt;INTERLEAVE&lt;/code&gt; 是仿真器每次执行的指令数量，它是一个预定义的值或根据仿真配置设定。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sim_t::step()&lt;/code&gt; 函数是仿真器的主执行循环，它遍历每个处理器核心，并调用每个核心的 &lt;code&gt;processor_t::step()&lt;/code&gt; 函数来执行指令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; void sim_t::step(size_t n)
 {
   for (size_t i = 0, steps = 0; i &amp;#x3C; n; i += steps) // 这个循环会持续执行，直到仿真器核心已经执行了 n 条指令。
   {
     steps = std::min(n - i, INTERLEAVE - current_step); // steps 变量决定每次当前处理器核心将执行的指令数
     procs[current_proc]-&gt;step(steps); // procs[current_proc]-&gt;step(steps)：这一行代码调用当前处理器核心的 step() 函数，执行steps条指令。
 
     current_step += steps;
     if (current_step == INTERLEAVE) // 当 current_step 达到 INTERLEAVE 时，仿真器将 current_step 重置为 0，并切换到下一个处理器核心
     {
       current_step = 0;
       procs[current_proc]-&gt;get_mmu()-&gt;yield_load_reservation();
       if (++current_proc == procs.size()) { // 如果所有核心都轮流执行了一次（即 current_proc == procs.size()），仿真器会调用设备的 tick() 函数更新其状态。rtc_ticks 是计算出来的虚拟时钟周期，表示在执行的指令数之间设备所经历的虚拟时间
         current_proc = 0;
         reg_t rtc_ticks = INTERLEAVE / INSNS_PER_RTC_TICK;
         for (auto &amp;#x26;dev : devices) dev-&gt;tick(rtc_ticks);
       }
     }
   }
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;processor_t::step(size_t n)&lt;/code&gt; 是 Spike 中用于实现指令取指（fetch）、解码（decode）、执行（execute）循环的核心函数。它负责让每个处理器核心（hart）执行 &lt;code&gt;n&lt;/code&gt; 条指令，同时处理调试模式、陷阱、触发器和中断等情况。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;调试模式检查&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; if (!state.debug_mode) {
   if (halt_request == HR_REGULAR) {
     enter_debug_mode(DCSR_CAUSE_DEBUGINT);
   } else if (halt_request == HR_GROUP) {
     enter_debug_mode(DCSR_CAUSE_GROUP);
   } else if (state.dcsr-&gt;halt) {
     enter_debug_mode(DCSR_CAUSE_HALT);
   }
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在执行指令之前，Spike 会检查是否存在调试模式请求。如果有调试中断或调试模式标志被设置，处理器会进入调试模式。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;主执行循环&lt;/strong&gt;&lt;/h3&gt;
&lt;h3&gt;&lt;strong&gt;基本状态初始化&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; while (n &gt; 0) {
     size_t instret = 0;
     reg_t pc = state.pc;
     mmu_t* _mmu = mmu;
     state.prv_changed = false;
     state.v_changed = false;
     ......
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;instret&lt;/code&gt; 记录已经执行的指令数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pc&lt;/code&gt; 是当前的程序计数器，指向当前要执行的指令的地址。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_mmu&lt;/code&gt; 指向内存管理单元（MMU），用于加载和存储指令。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;state.prv_changed&lt;/code&gt; 和 &lt;code&gt;state.v_changed&lt;/code&gt; 用于跟踪特权级（privilege level）和矢量化（vectorization）的变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;取指、解码和执行&lt;/strong&gt;&lt;/h3&gt;
&lt;h3&gt;&lt;strong&gt;慢速路径（slow path）&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; if (unlikely(slow_path())) {
   while (instret &amp;#x3C; n) {
     // 执行单条指令
     insn_fetch_t fetch = mmu-&gt;load_insn(pc);
     pc = execute_insn_logged(this, pc, fetch);
     advance_pc();
   }
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;慢速路径&lt;/strong&gt;：如果进入了“慢速路径”（即需要特殊处理的情况，如单步调试、触发器检测等），每条指令都会通过 &lt;code&gt;mmu-&gt;load_insn(pc)&lt;/code&gt; 从内存中加载，并通过 &lt;code&gt;execute_insn_logged()&lt;/code&gt; 执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单步执行&lt;/strong&gt;：在慢速路径下，指令会逐条取出、解码并执行，同时处理调试触发器和中断。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;快速路径（fast path）&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; else while (instret &amp;#x3C; n) {
   for (auto ic_entry = _mmu-&gt;access_icache(pc); ; ) {
     auto fetch = ic_entry-&gt;data;
     pc = execute_insn_fast(this, pc, fetch);
     ic_entry = ic_entry-&gt;next;
     if (unlikely(ic_entry-&gt;tag != pc))
       break;
     if (unlikely(instret + 1 == n))
       break;
     instret++;
     state.pc = pc;
   }
   advance_pc();
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;快速路径&lt;/strong&gt;：在不需要特殊处理的情况下，Spike 使用指令缓存（icache）加速执行流程。处理器从缓存中读取指令，并通过 &lt;code&gt;execute_insn_fast()&lt;/code&gt; 执行指令。如果缓存匹配，则直接执行下一个指令块，减少频繁的内存访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳转指令&lt;/strong&gt;：如果遇到分支跳转或者缓存不匹配的情况，指令缓存中的执行流会中断，并重新从新地址获取指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;中断和陷阱处理&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在指令执行过程中，可能会发生各种异常、中断或者调试陷阱：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;中断处理&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; take_pending_interrupt();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;take_pending_interrupt()&lt;/code&gt;&lt;/strong&gt;：检查并处理待处理的中断。如果检测到中断，将触发中断处理流程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;陷阱处理&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;catch (trap_t&amp;#x26; t) {
  take_trap(t, pc);
  n = instret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果在指令执行过程中发生陷阱（如非法指令、内存访问异常等），Spike 会捕获陷阱，并调用 &lt;code&gt;take_trap()&lt;/code&gt; 处理陷阱。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;触发器匹配&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;catch (triggers::matched_t&amp;#x26; t) {
  take_trigger_action(t.action, t.address, pc, t.gva);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果检测到触发器匹配事件，Spike 会执行相应的触发器操作（如中断、陷阱等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;等待中断&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;catch (wait_for_interrupt_t&amp;#x26; t) {
  n = ++instret;
  in_wfi = true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;当执行 &lt;code&gt;WFI&lt;/code&gt;（等待中断）指令时，处理器会进入等待状态，并等待中断发生。此时会退出循环，并标记为 &lt;code&gt;in_wfi&lt;/code&gt; 状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;指令计数和周期计数&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;state.minstret-&gt;bump(instret);
state.mcycle-&gt;bump(instret);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Spike 模拟了处理器的性能计数器。在每次指令执行后，&lt;code&gt;minstret&lt;/code&gt;（已完成指令计数器）和 &lt;code&gt;mcycle&lt;/code&gt;（周期计数器）会根据执行的指令数量进行递增。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>QEMU tcg中间码优化与后端机制</title><link>https://astro-pure.js.org/blog/simulator/qemu-tcg</link><guid isPermaLink="true">https://astro-pure.js.org/blog/simulator/qemu-tcg</guid><description>介绍 QEMU TCG 的核心机制，包括 Guest 指令到 IR 再到 Host 指令的翻译过程与关键数据结构。</description><pubDate>Mon, 02 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;TCG 基本逻辑&lt;/h2&gt;
&lt;p&gt;qemu tcg 的基本翻译思路是把 guest 指令先翻译成中间码(IR)，然后再把 IR 翻译成 host 指令。guest-&gt;IR-&gt;host 这种三段式实现的好处是把前端翻译，优化和后端翻译拆开了，降低了开发的难度。&lt;/p&gt;
&lt;p&gt;IR 指令的理解是比较直白的，qemu 定义了一套 IR 的指令，具体的定义在 tcg/README 里说明,在一个 tb 里，qemu 前端翻译得到的 IR 被串联到一个链表里，中间码优化和后端翻译都靠这个链表得到 IR，中间码优化时，需要改动 IR 时(比如，删掉不可达的 IR)，对这个链表做操作就好。&lt;/p&gt;
&lt;p&gt;中间码不只是定义了对应的指令，也有寄存器的定义，它形成了一个独立的逻辑空间，在 IR 这一层，可以认为都在中间码相关的寄存器上做计算的。IR 这一层定义了几个寄存器类型，它们分别是：global, local temp, normal temp, fixed, const, ebb&lt;/p&gt;
&lt;p&gt;一般 guest 的 gpr 也被定义为 IR 这一层的 global 寄存器，中间码做计算的时候，会用到一些临时变量，这些临时变量就保存在 local temp 或者是 normal temp 这样的寄存器里，计算的时候要用到一些常量时，需要定义一个 TCG 寄存器，创建一个常量并把它赋给 TCG 寄存器。&lt;/p&gt;
&lt;p&gt;riscv 下 global 寄存器一般如下定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/* target/riscv/translate.c */
riscv_translate_init
  [...]
  +-&gt; cpu_gpr[i] = tcg_global_mem_new(cpu_env,
  		    offsetof(CPURISCVState, gpr[i]), riscv_int_regnames[i]);
        /* 在TCGContext里分配对应的空间，并且设定这个寄存器是TEMP_GLOBAL */
    +-&gt; tcg_global_mem_new_internal(..., reg, offset, name);
  [...]
  +-&gt; cpu_pc = tcg_global_mem_new(cpu_env, offsetof(CPURISCVState, pc), &quot;pc&quot;);
  [...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里分配了对应的 TCG 寄存器，返回值是这些寄存器存储地址相对 tcg_ctx 的偏移。注意这里得到的是 global 寄存器的描述结构，类型是 TCGTemp，而 global 寄存器实际存储在 CPURISCVState 内具体定义的地方，TCGTemp 内通过 mem_base 和 mem_offset 指向具体存储地址。&lt;/p&gt;
&lt;p&gt;实际上，所有 TCG 寄存器的分配都是在 TCGContext 里分配了对应的存储空间，并且配置上相关参数，这些参数和 IR 一起交给后端做 IR 优化和后端翻译，后端使用 TCGContext 的地址和具体寄存器的偏移可以找见具体的 TCG 寄存器。&lt;/p&gt;
&lt;p&gt;normal temp 只在一个 BB 中有效，local temp 在一个 TB 中有效。fixed 要结合 host 寄存器分配来看，首先 IR 中分配的这些寄存器都是虚拟的寄存器，IR 翻译到 host 指令都要给虚拟寄存器分配对应的 host 物理寄存器，当一个 TCG 寄存器有 TEMP_FIXED 标记表示在后端翻译时把这个虚拟寄存器固定映射到一个 host 物理寄存器上，一般 fixed 寄存器都是翻译执行时经常要用到的参数。&lt;/p&gt;
&lt;h2&gt;中间码优化&lt;/h2&gt;
&lt;p&gt;前端翻译得到的 IR 可能会有优化的空间存在，所以 qemu 在进行后端翻译之前会先做中间码
优化，优化以一个 TB 为单位，优化的输入就是一个 TB 对应的 IR 和用到的 TCG 寄存器。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/* tcg/tcg.c */
tcg_gen_code
  +-&gt; tcg_optimize(s)
    +-&gt; done = fold_add(&amp;#x26;ctx, op);

  +-&gt; reachable_code_pass(s);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tcg_optimize 是做着一些常量的检查，进而做指令优化(折叠常量表达式), 我们取其中的一个 case，比如 fold_add 具体看下，大概知道下这里是在干什么。可以看到这个 case 检测 add_32/64 这个 IR 的两个操作数是不是常量，如果是常量，那么在这里直接把常量相加后的结果放到一个常量类型 TCG 寄存器，然后把之前的 add_32/64 改成一条 mov 指令。&lt;/p&gt;
&lt;p&gt;从名字就可以看出 reachable_code_pass 应该做的是一些死代码的删除，这里检测到运行不到的 IR 就直接从 IR 链表里把他们删掉。&lt;/p&gt;
&lt;p&gt;中间码优化的输出还是 IR 链表和相关的 TCG 寄存器，可见我们也可以把这两个函数注释掉，从而把中间码优化关掉。可以看出，中间码优化和编译器 IR 优化的逻辑是类似的。&lt;/p&gt;
&lt;h2&gt;tcg 后端功能&lt;/h2&gt;
&lt;p&gt;qemu 用 tcg 模拟 guest 指令执行，qemu 把 guest 指令先翻译成中间码，然后再把中间码翻译成 host 指令，host 指令可以最终在 host cpu 上执行，这样就完成了翻译。&lt;/p&gt;
&lt;p&gt;此部分关注的是后端翻译模型，也就是中间码翻译成 host 指令的过程。中间码是一套完整的指令集定义，使用中间码可以完整的表述 guest 指令的行为，看一个小例子对这种描述会有更直观的感受。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;addi            sp,sp,-32                  &amp;#x3C;-- guest汇编
sd              s0,24(sp)

add_i64 x2/sp,x2/sp,$0xffffffffffffffe0    &amp;#x3C;-- 中间码
add_i64 tmp4,x2/sp,$0x18
qemu_st_i64 x8/s0,tmp4,leq,0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上 guest 那条 store 指令，它被翻译成了两条中间码，第一条 add_i64 是用来计算 sd 要 store 的地址，计算出的地址保存在 tmp4 这个虚拟寄存器里，第二条中间码把 s0 的值 store 到 tmp4 描述的内存上，qemu 用中间码和虚拟寄存器完整的表述 guest 的逻辑。这里 qemu_st_i64 这个中间码表示一个 store 操作，store 的数据和地址都用虚拟寄存器描述，所以在 qemu_st_i64 之前要用 add_i64 先计算出 store 的地址，并保存在虚拟寄存器里。&lt;/p&gt;
&lt;p&gt;qemu 中，其它的 guest 指令也是这样先翻译成中间码和虚拟寄存器的表示，后端翻译基于中间码和虚拟寄存器进行。上面的中间码表述中，x2/sp 和 x8/s0 还是 guest 上寄存器的名字，但是逻辑上 guest 上的寄存器都已经映射到 qemu 虚拟寄存器，所以中间码指令中的所有寄存器都是 qemu 的虚拟寄存器。&lt;/p&gt;
&lt;p&gt;qemu 模拟的 guest cpu 系统说到底就是 host 内存里表示的 guest cpu 的软件结构体的状态以及 guest 内存的状态，qemu 中间码已经完整的描述了 guest 状态改变的激励，拿上面 addi 和 sd guest 指令的模拟为例，模拟 addi 的中间码是 addi_64 x2/sp,x2/sp,$0xffffffffffffffe0 ，表示要把 guest 的 sp 加上-32，sd 的中间码表示要把 guest sp + 24 指向的地址上的值改成 s0 的值。我们拿到如上中间码或者 guest 指令，甚至可以直接写 c 代码去完成模拟。qemu 为了追求效率把中间码翻译成 host 指令来完成模拟。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;add_i64 x2/sp,x2/sp,$0xffffffffffffffe0
add_i64 tmp4,x2/sp,$0x18
qemu_st_i64 x8/s0,tmp4,leq,0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这几条中间码只是表意，实际真正更新 guest cpu 的数据结构和 guest 地址还需要 host 指令完成，所以实际翻译后的 host 指令可能是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;ldr      x20, [x19, #0x10]    把guest cpu中的sp load到host的x20寄存器
sub      x20, x20, #0x20      使用host sub指令完成guest sp的计算
str      x20, [x19, #0x10]    更新guest cpu中sp的值
add      x21, x20, #0x18      使用host add指令计算store的地址，并保存到host的x21寄存器
ldr      x22, [x19, #0x40]    把guest cpu中的s0 load到host的x22寄存器
str      x22, [x21, xzr]      使用host str指令更新guest地址上的值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;qemu 的后端翻译就是完成如上功能，总结起来就是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;分配 host 物理寄存器；&lt;/li&gt;
&lt;li&gt;生成 host 指令；&lt;/li&gt;
&lt;li&gt;host 和 guest 之间的状态同步。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;分配 host 物理寄存器&lt;/h2&gt;
&lt;p&gt;虚拟寄存器和 host 物理寄存器是两个独立的概念，虚拟寄存器可能会很多，而物理寄存器的个数是有限的，虚拟寄存器有自己的生命周期，虚拟寄存器生命周期结束后，它所使用的物理寄存器就可以给其它虚拟寄存器使用。因为 host 物理寄存器数目有限，就有可能出现 host 物理寄存器不够分的情况，这时候就需要把已经分配但是目前还没有用到的 host 物理寄存器的值保存到内存，这样就可以腾出 host 物理寄存器来使用。&lt;/p&gt;
&lt;p&gt;qemu 在处理 host 物理寄存器分配的时候，分了两步处理，第一步先确定虚拟寄存器的生命周期，一般叫做寄存器活性分析，第二步根据虚拟寄存器活性分析的结果具体分配物理寄存器。&lt;/p&gt;
&lt;p&gt;针对一段中间码，qemu 对其做逆序遍历，依此确定虚拟寄存器的生命周期。如果一个虚拟寄存器后续还中间码使用，那它还是 live 的，如果后面没有中间码用了，它就 dead 了。&lt;/p&gt;
&lt;p&gt;所以，一个虚拟寄存器 dead 与否是和具体中间码一起看的，一个虚拟寄存器可能在前几个中间码中是 live 的(虽然这几个中间码并没有使用这个虚拟寄存器)，最后一个使用它的中间码后这个虚拟寄存器就 dead 了。qemu 里只要记录虚拟寄存器被引用时的状态就好。&lt;/p&gt;
&lt;h2&gt;生成 host 指令以及状态同步&lt;/h2&gt;
&lt;p&gt;我们把状态同步和 host 指令生成放到一起看，因为所谓状态同步也要生成 host 指令进行。&lt;/p&gt;
&lt;p&gt;对于中间码的输入虚拟寄存器，需要先判断这个输入寄存器的值是保存在内存上，还是已经保存在 host 物理寄存器上了，如果还在内存上，qemu 就要分配 host 物理寄存器，然后插入 host 上的 load 指令把内存上的值 load 到 host 物理寄存器上，如果虚拟寄存器的值已经在 host 物理寄存器上，那么它直接就可以参与计算。对于中间码的输出虚拟寄存器，qemu 需要为它分配 host 物理寄存器。&lt;/p&gt;
&lt;p&gt;中间码的输入和输出寄存器都有着落了，qemu 就可以尝试把中间码翻译成 host 指令。这个翻译可能直接就可以翻译成一条 host 指令，也可能需要再插入几条 host 指令调整下。&lt;/p&gt;
&lt;p&gt;guest 指令对应的中间码执行完后，需要把 guest 指令的输出同步回 guest CPU 数据结构，所以 qemu 在这里还需要插入 host store 指令把数据刷回 guest CPU。qemu 在寄存器活性分析的时候会把需要做同步的虚拟寄存器打上 sync 的标记，生成 host 指令的时候遇见 sync 标记就可以直接插入 host 指令做同步。&lt;/p&gt;
&lt;p&gt;并不需要每个 guest 指令执行完都要把信息刷回 guest CPU 数据结构，虽然 guest CPU 的信息是定义在 guest CPU 数据结构中的，但是我们是模拟 guest CPU，只要不破坏模拟的逻辑，host 物理寄存器上的值就可以先不刷回 guest CPU 数据结构。那什么时候需要刷回 guest CPU，整个 TB 执行完时，虚拟寄存器需要被同步回 guest CPU，当中间码可能导致 guest CPU 异常时，需要做同步，因为触发异常后，guest CPU 跳转到异常处理地址，并且向软件报告异常处理的上下文，其中 guest CPU 的通用寄存器就都是从 guest CPU 数据结构获取。&lt;/p&gt;
&lt;h2&gt;加入 BB 的概念&lt;/h2&gt;
&lt;p&gt;上面讲的寄存器分配和状态同步其实还不完整，qemu 的一个翻译块(TB)里是可以存在跳转中间码的，在有跳转中间码的情况下，上面逆序遍历确定虚拟寄存器活性的办法就会有问题。为此 qemu 中在 TB 的基础上又引入了 Basic Block(BB)的概念，简单讲在一个 BB 内中间码都是顺序执行的，这样如上的逻辑在 BB 内还是成立的。所以，在 BB 的结尾就要 dead 全部虚拟寄存器，并且把 guest CPU 对应的虚拟寄存器向 guest CPU 数据结构做同步。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Spike 运行机制详解</title><link>https://astro-pure.js.org/blog/simulator/spike-run</link><guid isPermaLink="true">https://astro-pure.js.org/blog/simulator/spike-run</guid><description>Spike 指令获取与运行流程详解，包含 ICache、MMU 加载与 refill 机制示例代码。</description><pubDate>Sun, 18 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;指令获取&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Spike 使用 MMU（内存管理单元）来加载指令。具体来说，指令获取的过程通过 &lt;code&gt;mmu-&gt;load_insn(pc)&lt;/code&gt; 函数完成，该函数负责根据程序计数器的地址从内存中读取一条指令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; struct insn_fetch_t
 {
   insn_func_t func;
   insn_t insn;
 };
 
 struct icache_entry_t {
   reg_t tag;
   struct icache_entry_t* next;
   insn_fetch_t data;
 };
 
 insn_fetch_t fetch = mmu-&gt;load_insn(pc);
     -&gt; inline insn_fetch_t load_insn(reg_t addr)
        {
            icache_entry_t entry;
            return refill_icache(addr, &amp;#x26;entry)-&gt;data;
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;mmu-&gt;load_insn(pc)&lt;/code&gt;&lt;/strong&gt;：根据当前程序计数器（&lt;code&gt;pc&lt;/code&gt;）的值，MMU 从内存中读取指令。这是一个指令获取函数，它返回一个 &lt;code&gt;insn_fetch_t&lt;/code&gt; 结构，其中包含了要执行的指令及其相关的元数据。&lt;/p&gt;
&lt;p&gt;Spike 提供了一个指令缓存（ICache）来加速指令获取的过程。缓存中的指令可以避免频繁从内存中读取相同的指令，这提高了仿真的性能。&lt;/p&gt;
&lt;p&gt;在快速路径中，Spike 会从指令缓存中直接获取指令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; auto ic_entry = _mmu-&gt;access_icache(pc);
     -&gt; inline icache_entry_t* access_icache(reg_t addr)
       {
         icache_entry_t* entry = &amp;#x26;icache[icache_index(addr)];
         if (likely(entry-&gt;tag == addr))
           return entry;
         return refill_icache(addr, entry);
       }
 auto fetch = ic_entry-&gt;data;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果指令缓存未命中或者在慢速路径中，Spike 会直接从内存中加载指令。通过 &lt;code&gt;mmu-&gt;load_insn()&lt;/code&gt; 函数完成，它负责从内存地址中读取原始的指令二进制数据。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;读取指令&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;refill_icache&lt;/code&gt; 是 Spike 仿真器中用于填充指令缓存（ICache）的函数。它从内存中读取指令，并将指令及其解码结果缓存起来，以便后续访问时可以从缓存中快速获取指令，而不必再次进行内存访问。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; inline icache_entry_t* refill_icache(reg_t addr, icache_entry_t* entry)
   {
     if (matched_trigger)
       throw *matched_trigger;
 
     auto tlb_entry = translate_insn_addr(addr); // 将虚拟地址（addr）转换为物理地址
     // 从内存中读取指令的前两个字节（16位），并将其存入 insn 变量。
     insn_bits_t insn = from_le(*(uint16_t*)(tlb_entry.host_offset + addr));
     int length = insn_length(insn); // 确定该指令的长度
     // 根据 length 的值，Spike 从内存中获取剩余的指令字节，并组装成完整的指令
     if (likely(length == 4)) {
       insn |= (insn_bits_t)from_le(*(const uint16_t*)translate_insn_addr_to_host(addr + 2)) &amp;#x3C;&amp;#x3C; 16;
     } else if (length == 2) {
       // entire instruction already fetched
     } else if (length == 6) {
       insn |= (insn_bits_t)from_le(*(const uint16_t*)translate_insn_addr_to_host(addr + 2)) &amp;#x3C;&amp;#x3C; 16;
       insn |= (insn_bits_t)from_le(*(const uint16_t*)translate_insn_addr_to_host(addr + 4)) &amp;#x3C;&amp;#x3C; 32;
     } else {
       static_assert(sizeof(insn_bits_t) == 8, &quot;insn_bits_t must be uint64_t&quot;);
       insn |= (insn_bits_t)from_le(*(const uint16_t*)translate_insn_addr_to_host(addr + 2)) &amp;#x3C;&amp;#x3C; 16;
       insn |= (insn_bits_t)from_le(*(const uint16_t*)translate_insn_addr_to_host(addr + 4)) &amp;#x3C;&amp;#x3C; 32;
       insn |= (insn_bits_t)from_le(*(const uint16_t*)translate_insn_addr_to_host(addr + 6)) &amp;#x3C;&amp;#x3C; 48;
     }
 
     insn_fetch_t fetch = {proc-&gt;decode_insn(insn), insn}; // 指令解码
     entry-&gt;tag = addr; // 将缓存条目中的 tag 更新为当前指令的地址（addr），以便后续可以根据地址快速找到这条缓存条目。
     entry-&gt;next = &amp;#x26;icache[icache_index(addr + length)]; // 将 next 指针设置为下一个缓存条目地址
     entry-&gt;data = fetch; // 将 fetch（包含解码后的指令和原始指令）存入 entry-&gt;data 中。
     // 跟踪与异常处理
     reg_t paddr = tlb_entry.target_offset + addr;;
     if (tracer.interested_in_range(paddr, paddr + 1, FETCH)) {
       entry-&gt;tag = -1;
       tracer.trace(paddr, length, FETCH);
     }
     return entry;
   }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;refill_icache&lt;/code&gt; 的主要功能是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;地址转换（TLB）。&lt;/li&gt;
&lt;li&gt;读取不同长度的指令。&lt;/li&gt;
&lt;li&gt;解码指令。&lt;/li&gt;
&lt;li&gt;将指令存入缓存。&lt;/li&gt;
&lt;li&gt;监控触发器或调试器的状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;指令解码&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;processor_t::decode_insn(insn_t insn)&lt;/code&gt; 是 Spike 中用于将二进制指令解码为对应的操作函数的核心函数。它会通过查找一个哈希表来快速获取指令对应的处理函数（&lt;code&gt;insn_func_t&lt;/code&gt;），如果哈希表未命中，则会通过线性搜索找到相应的指令描述符，并将其缓存起来以供后续查找。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;decode_insn&lt;/code&gt; 的主要功能是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从二进制指令中提取出操作码。&lt;/li&gt;
&lt;li&gt;查找指令对应的操作函数。&lt;/li&gt;
&lt;li&gt;通过哈希表缓存机制加速指令解码。&lt;/li&gt;
&lt;li&gt;处理自定义指令集和扩展。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;通过哈希表查找指令&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; size_t idx = insn.bits() % OPCODE_CACHE_SIZE;
 auto [hit, desc] = opcode_cache[idx].lookup(insn.bits());
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;insn.bits()&lt;/code&gt;&lt;/strong&gt;：从指令对象中提取出指令的二进制表示。&lt;code&gt;bits()&lt;/code&gt; 返回指令的位表示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;idx = insn.bits() % OPCODE_CACHE_SIZE&lt;/code&gt;&lt;/strong&gt;：使用指令的位表示对 &lt;code&gt;OPCODE_CACHE_SIZE&lt;/code&gt; 取模，计算哈希表中的索引 &lt;code&gt;idx&lt;/code&gt;。这个索引指向缓存中的某个槽位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;opcode_cache[idx].lookup(insn.bits())&lt;/code&gt;&lt;/strong&gt;：在哈希表（&lt;code&gt;opcode_cache&lt;/code&gt;）中查找是否存在该指令。如果存在（&lt;code&gt;hit&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;），则直接返回指令描述符 &lt;code&gt;desc&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;处理自定义指令和标准指令&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;如果哈希表中没有找到对应的指令（&lt;code&gt;hit&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt;），Spike 将使用线性搜索方法在自定义指令和标准指令列表中寻找匹配的指令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; if (unlikely(!hit)) {
   // fall back to linear search
   auto matching = [insn_bits = insn.bits()](const insn_desc_t &amp;#x26;d) {
     return (insn_bits &amp;#x26; d.mask) == d.match;
   };
   auto p = std::find_if(custom_instructions.begin(),
                         custom_instructions.end(), matching);
   if (p == custom_instructions.end()) {
     p = std::find_if(instructions.begin(), instructions.end(), matching);
     assert(p != instructions.end());
   }
   desc = &amp;#x26;*p;
   opcode_cache[idx].replace(insn.bits(), desc);
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;线性搜索&lt;/strong&gt;：如果缓存未命中，Spike 会在 &lt;code&gt;custom_instructions&lt;/code&gt;（自定义指令集）中线性搜索。&lt;code&gt;matching&lt;/code&gt; 是一个匿名函数，它通过指令的掩码（&lt;code&gt;mask&lt;/code&gt;）和匹配值（&lt;code&gt;match&lt;/code&gt;）来匹配指令。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::find_if&lt;/code&gt;&lt;/strong&gt;：标准库函数 &lt;code&gt;find_if&lt;/code&gt; 在列表中寻找第一个满足条件的指令描述符。如果没有在自定义指令集中找到，Spike 将搜索标准的 &lt;code&gt;instructions&lt;/code&gt; 集合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存更新&lt;/strong&gt;：一旦找到匹配的指令编码，Spike 将该描述符插入哈希表的缓存中，以加速后续的解码过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;指令什么时候被添加到队列中？&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;processor_t::processor_t&lt;/code&gt;初始化函数中&lt;code&gt;processor_t::register_base_instructions()&lt;/code&gt; 函数就是 Spike 仿真器中将指令添加到 &lt;code&gt;instructions&lt;/code&gt; 容器中的地方。这也是标准 RISC-V 指令集被注册的关键函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;   #define DEFINE_INSN(name) \
     if (!name##_overlapping) \
       register_base_insn((insn_desc_t) { \
         name##_match, \
         name##_mask, \
         fast_rv32i_##name, \
         fast_rv64i_##name, \
         fast_rv32e_##name, \
         fast_rv64e_##name, \
         logged_rv32i_##name, \
         logged_rv64i_##name, \
         logged_rv32e_##name, \
         logged_rv64e_##name});
   #include &quot;insn_list.h&quot;
   #undef DEFINE_INSN
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;DEFINE_INSN&lt;/code&gt; 宏&lt;/strong&gt;：声明与每条指令相关的各种处理函数。每条指令可以有多个变种（例如针对 RV32I、RV64I、RV32E、RV64E 等不同的指令集变体），每个变种都有对应的快速路径（&lt;code&gt;fast&lt;/code&gt;）和日志记录（&lt;code&gt;logged&lt;/code&gt;）版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; DEFINE_INSN(add)
 DEFINE_INSN(addi)
 DEFINE_INSN(addiw)
 DEFINE_INSN(addw)
 DEFINE_INSN(and)
 DEFINE_INSN(andi)
 DEFINE_INSN(auipc)
 DEFINE_INSN(beq)
 DEFINE_INSN(bge)
 DEFINE_INSN(bgeu)
 DEFINE_INSN(blt)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;insn_list.h&lt;/code&gt;&lt;/strong&gt;：该文件包含所有标准 RISC-V 指令的列表。通过包含这个文件，Spike 为每条指令声明了其相关的处理函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; static uint32_t addi(unsigned int dest, unsigned int src, uint16_t imm) __attribute__ ((unused));
 static uint32_t addi(unsigned int dest, unsigned int src, uint16_t imm)
 {
   return (bits(imm, 11, 0) &amp;#x3C;&amp;#x3C; 20) |
     (src &amp;#x3C;&amp;#x3C; 15) |
     (dest &amp;#x3C;&amp;#x3C; 7) |
     MATCH_ADDI;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后会调用&lt;code&gt;processor_t::register_insn&lt;/code&gt;函数，将指令添加入指令队列中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void processor_t::register_insn(insn_desc_t desc, bool is_custom) {
  assert(desc.fast_rv32i &amp;#x26;&amp;#x26; desc.fast_rv64i &amp;#x26;&amp;#x26; desc.fast_rv32e &amp;#x26;&amp;#x26; desc.fast_rv64e &amp;#x26;&amp;#x26;
         desc.logged_rv32i &amp;#x26;&amp;#x26; desc.logged_rv64i &amp;#x26;&amp;#x26; desc.logged_rv32e &amp;#x26;&amp;#x26; desc.logged_rv64e);

  if (is_custom)
    custom_instructions.push_back(desc);
  else
    instructions.push_back(desc);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;获得指令执行函数&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;desc-&gt;func(xlen, rve, log_commits_enabled);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;desc-&gt;func&lt;/code&gt;&lt;/strong&gt;：指令描述符包含了对应指令的处理函数（&lt;code&gt;func&lt;/code&gt;），这是一个指令处理函数（&lt;code&gt;insn_func_t&lt;/code&gt; 类型）。根据指令描述符，Spike 返回这个指令处理函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;insn_func_t func(int xlen, bool rve, bool logged) const
  {
    if (logged)
      if (rve)
        return xlen == 64 ? logged_rv64e : logged_rv32e;
      else
        return xlen == 64 ? logged_rv64i : logged_rv32i;
    else
      if (rve)
        return xlen == 64 ? fast_rv64e : fast_rv32e;
      else
        return xlen == 64 ? fast_rv64i : fast_rv32i;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时获得的指令执行函数形式类似于&lt;code&gt;fast_rv32i_add&lt;/code&gt;,实际上是没有这个函数，而是会通过宏定义的方式来实现指令执行的逻辑。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;指令执行&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;获得指令的执行函数后Spike就会直接调用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;static inline reg_t execute_insn_fast(processor_t* p, reg_t pc, insn_fetch_t fetch) {
  return fetch.func(p, fetch.insn, pc);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spike的指令逻辑是在ISA（指令集架构）模块中定义的，具体是在&lt;code&gt;riscv/insns/&lt;/code&gt;目录下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每条指令都有一个对应的C++文件。例如，&lt;code&gt;add&lt;/code&gt;指令的编码格式在&lt;code&gt;riscv/insns/add.h&lt;/code&gt;文件中定义。Spike使用宏和位运算来解析和生成指令的二进制编码。&lt;/li&gt;
&lt;li&gt;指令的解析和执行逻辑是由仿真器内部的解码器来实现的。指令的二进制编码由RISC-V ISA的标准规定，在Spike中，这些编码由解码器解析并映射到相应的指令处理函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Spike 使用宏定义的方式来实现指令执行的逻辑。这种方式可以让多条指令共享相似的结构逻辑，并简化重复性代码的编写。例如，&lt;code&gt;add.h&lt;/code&gt; 文件中的宏实现可能被多个变种（例如 RV32I 和 RV64I）使用，而不需要为每个变种编写单独的函数。&lt;/p&gt;
&lt;p&gt;在构建时，Spike 的编译器会将这些宏展开到实际的指令处理逻辑中。具体的执行流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当仿真器解码到 &lt;code&gt;add&lt;/code&gt; 指令时，会使用指令表中的描述符（如&lt;code&gt;fast_rv32i_add&lt;/code&gt;）找到 &lt;code&gt;add.h&lt;/code&gt; 文件中的处理逻辑。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add.h&lt;/code&gt; 中的宏会被展开为相应的操作，仿真器将使用这些操作执行加法指令。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// add.h
WRITE_RD(RS1 &amp;#x26; RS2);
|
||-&gt; // riscv/decode_macros.h
    #define RS1 READ_REG(insn.rs1())
    #define RS2 READ_REG(insn.rs2())
||-&gt; // riscv/decode_macros.h
    #define WRITE_RD(value) WRITE_REG(insn.rd(), value)
    #define WRITE_REG(reg, value) ({ \
        reg_t wdata = (value); /* value may have side effects */ \
        if (DECODE_MACRO_USAGE_LOGGED) STATE.log_reg_write[(reg) &amp;#x3C;&amp;#x3C; 4] = {wdata, 0}; \
        CHECK_REG(reg); \
        STATE.XPR.write(reg, wdata); \
      })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上就是Spike在运行时指令获取和指令执行的大体流程。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Qemu机制与自定义指令添加指南</title><link>https://astro-pure.js.org/blog/simulator/qemu-base</link><guid isPermaLink="true">https://astro-pure.js.org/blog/simulator/qemu-base</guid><description>梳理 QEMU 的基础架构，重点介绍 KVM 与 TCG 的关系、指令翻译流程及 RISC-V 相关实现路径。</description><pubDate>Tue, 13 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;qemu 基础逻辑&lt;/h2&gt;
&lt;p&gt;qemu 虚拟机提供两种 CPU 实现的方式，一种是基于中间码的实现，一种是基于 KVM 的实现。
&lt;img src=&quot;https://cdn.willimt.com/work/20260423225639.png&quot; alt=&quot;20240625124715&quot;&gt;
第一种方式一般被叫做叫 tcg(tiny code generator)，这种方式的基本思路是用纯软件的方式把 target CPU 的指令先翻译成中间码，然后再把中间码翻译成 host CPU 的指令，通常把 target CPU 指令翻译成中间码的过程叫整个过程的前端，中间码翻译成 host CPU 的过程对应的叫做后端。给 qemu 增加一个新 CPU 的模型需要既增加前端也增加后端，如果要模拟整个系统，还要增加基础设备以及 user mode 的支持。如果目的是在一个成熟的平台上验证另一个新的 CPU，比如在 x86 机器上跑 riscv 的虚拟机，验证 riscv 的逻辑，只需要加上 riscv 指令到中间码这个前端支持就可以了，因为中间码到 x86 的后端已经存在；如果目的是在一台 riscv 的机器上模拟 x86 架构，就需要添加中间码到 riscv 的后端支持。&lt;/p&gt;
&lt;p&gt;KVM（Kernel Virtual Machine）是 Linux 的一个内核驱动模块，它能够让 Linux 主机成为一个 Hypervisor（虚拟机监控器）。QEMU 虚拟机是一个纯软件的实现，可以在没有 KVM 模块的情况下独立运行，但是性能会较低一些。QEMU 有整套的虚拟机实现，包括处理器虚拟化、内存虚拟化以及 I/O 设备的虚拟化。QEMU 默认使用纯软件模拟来模拟 CPU 的执行。当启用 KVM 时，QEMU 将 CPU 的执行转交给 KVM 来处理。在硬件支持的情况下，KVM 将直接使用主机 CPU 的硬件虚拟化扩展来执行虚拟机中的指令，从而极大地提升性能。简单来说，KVM 和 QEMU 相辅相成，QEMU 通过 KVM 达到了硬件虚拟化的速度，而 KVM 则通过 QEMU 来模拟设备。
我们暂时只关注第一种方式。riscv 体系相关的前端的代码在：target/riscv/，后端的代码在：tcg/riscv/，基础外设和 machine 的代码在 hw/riscv/。&lt;/p&gt;
&lt;h2&gt;qemu tcg 前端解码逻辑&lt;/h2&gt;
&lt;p&gt;把 target cpu 指令翻译成 host cpu 指令有两种方式，一种是使用 helper 函数，一种是使用 TCG 函数的方式。通常是使用 TCG 函数来操作数据、翻译指令，只有在某些 TCG 操作不方便或者无法模拟 CPU 操作时才会使用 helper 函数。&lt;/p&gt;
&lt;p&gt;如果把逻辑拉高一层来看，所谓 target CPU 的运行，实际上是根据 target CPU 指令流去不断的改变 target CPU 数据结构里的数据状态。因为实际的代码要运行到 host CPU 上，所以 target 代码要被翻译成 host 代码，才可以执行，通过执行模拟改变 target CPU 的数据状态。qemu 为了解耦把 target CPU 代码先翻译成中间码，翻译成的中间码的语义也就是改变 target CPU 数据状态的一组==描述语句==，所以 target CPU 状态参数会被当做入参传入中间码描述语句。这组中间码是改变 CPU 状态的抽象的描述，有些 CPU 上的状态不好抽象成一般的描述就用 helper 函数的方式补充，所以 helper 函数也是改变 target CPU 状态的描述。&lt;/p&gt;
&lt;p&gt;如果要用 tcg 的方式，就需要使用 tcg_gen_xxx 的函数组织逻辑描述 target CPU 指令对 target CPU 状态的改变。一些公共的代码会自动生成的，qemu 里使用 decode tree 的方式自动生成这一部分代码。&lt;/p&gt;
&lt;p&gt;riscv 的指令描述在 target/riscv/insn16.decode、insn32.decode 里（包括指令编码、参数位置、入参结构体等），qemu 编译的时候会解析.decode 文件，使用脚本(scripts/decodetree.py)生成对应的定义和函数，生成的文件放在 qemu/build/libqemu-riscv64-softmmu.fa.p/decode-insn32.c.inc，decode-insn16.c.inc 里。这些文件生成的 trans_xxx 函数只是声明，具体功能需要自己实现，riscv 的这部分实现是放在了在 target/riscv/insn_trans/*里。生成的文件里有两个很大的解码函数 decode-insn32.c.inc 和 decode-insn16.c.inc，qemu 把 target CPU 指令翻译成中间码的时候就需要调用上面两个解码函数，通过查找解码树来调用对应的翻译函数。&lt;/p&gt;
&lt;p&gt;现在用 riscv 架构下 user mode 的代码来看看上层具体调用关系。qemu 提供 system mode 和 user mode 的模拟方式，其中 system mode 会完整模拟整个系统，一个完整的 OS 可以运行在这个模拟的系统上，user mode 只是支持加载一个 target CPU 构架的用户态程序来跑，对于一般指令使用 tcg 的方式翻译执行，对于用户态程序里的系统调用，user mode 代码里模拟实现了==系统调用==的过程。linux user mode 的代码在 qemu/linux-user/*，具体的调用过程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/* qemu/linux-user/main.c */
main
  +-&gt; cpu_loop
    +-&gt; cpu_exec
      +-&gt; tb_gen_code
      |     /* qemu/target/riscv/trannslate.c */
      | +-&gt; gen_intermediate_code
      | | +-&gt; translator_loop(&amp;#x26;riscv_tr_ops, xxx)
      | |       /* riscv_tr_translate_insn */
      | |   +-&gt; ops-&gt;translator_insn
      | |     +-&gt; decode_ops
      | |       +-&gt; decode_insn16
      | |       +-&gt; decode_insn32
      | +-&gt; tcg_gen_code
      |   +-&gt; tcg_out_xxx
      +-&gt; cpu_loop_exec_tb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;gen_intermediate_code 是前端的解码函数，把 target CPU 的指令翻译成 tcg 中间码。tcg_gen_code 是后端，把中间码翻译成 host CPU 上的指令，其中 tcg_out_xxx 的一组函数做具体的翻译工作。
下面展开其中的各个细节：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;tcg 整个翻译流程构架分析&lt;/li&gt;
&lt;li&gt;decode tree 的语法&lt;/li&gt;
&lt;li&gt;tcg trans_xxx 函数的语法&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;tcg 翻译流程&lt;/h3&gt;
&lt;p&gt;整个 tcg 前后端的翻译流程按指令块的粒度来搞，收集一个指令块翻译成中间码，然后把中间码翻译成 host CPU 指令，整个过程动态执行。为了加速翻译，qemu 把翻译成的 host CPU 指令块做了缓存，tcg 前端解码的时候，先在缓存里找，如果找见就直接执行。&lt;/p&gt;
&lt;h3&gt;decode tree 语法&lt;/h3&gt;
&lt;p&gt;CPU 指令编码通常是按组划分的，因此可以用 decode 去描述这些固定的结构，然后 qemu 根据这些指令定义，使用一个脚本(scripts/decodetree.py)在编译的时候生成解码函数的框架。
decode tree 里定义了几个描述：field，argument，format，pattern，group。CPU 在解码的时候总要把指令中的特性 field 中的数据取出作为入参(寄存器编号，立即数，操作码等)，field 描述一个指令编码中特定的域段，根据描述可以生成取对应域段的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;+---------------------------+---------------------------------------------+
| Input                     | Generated code                              |
+===========================+=============================================+
| %disp   0:s16             | sextract(i, 0, 16)                          |
+---------------------------+---------------------------------------------+
| %imm9   16:6 10:3         | extract(i, 16, 6) &amp;#x3C;&amp;#x3C; 3 | extract(i, 10, 3)  |
+---------------------------+---------------------------------------------+
| %disp12 0:s1 1:1 2:10     | sextract(i, 0, 1) &amp;#x3C;&amp;#x3C; 11 |                   |
|                           |    extract(i, 1, 1) &amp;#x3C;&amp;#x3C; 10 |                 |
|                           |    extract(i, 2, 10)                        |
+---------------------------+---------------------------------------------+
| %shimm8 5:s8 13:1         | expand_shimm8(sextract(i, 5, 8) &amp;#x3C;&amp;#x3C; 1 |      |
|   !function=expand_shimm8 |               extract(i, 13, 1))            |
+---------------------------+---------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个数据，比如一个立即数，可能是多个域段拼成的，所以就有相应的移位操作，再比如有些立即数是编码域段的数值取出来后再进过简单运算得到的，field 定义中带的函数就可以完成这样的计算。
argument 用来定义数据结构，比如，riscv insn32.decode 里定义的: &amp;#x26;b imm rs2 rs1，编译后的 decode-insn32.c.inc 里生成的数据结构如下，这个结构会作为 trans_xxx 函数的入参。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;typedef struct {
    int imm;
    int rs2;
    int rs1;
} arg_b;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;format 定义指令的格式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;@r       .......   ..... ..... ... ..... ....... &amp;#x26;r                %rs2 %rs1 %rd
@i       ............    ..... ... ..... ....... &amp;#x26;i      imm=%imm_i     %rs1 %rd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如上面就是对一个 32bit 指令编码的描述，. 表示一个 0 或者 1 的 bit 位，描述里可以用 field、之前定义的 filed 的引用、argument 的引用，field 的引用还可以赋值。field 可以用来匹配，argument 用来生成 trans_xxx 函数的入参。
pattern 用来定义具体指令。比如 riscv32 里的 lui 指令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;lui      ....................       ..... 0110111 @u
@u       ....................      ..... ....... &amp;#x26;u      imm=%imm_u          %rd
&amp;#x26;u    imm rd
%imm_u    12:s20                 !function=ex_shift_12
%rd        7:5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面把相关的 format、argument、field 的定义也列了出来。可以看到 lui 的操作码是 0110111，这个指令的格式定义是@u，这个格式定义使用的参数定义是&amp;#x26;u，&amp;#x26;u 就是 trans_lui 函数入参结构体里的变量的定义，其中定义的变量名字是 imm、rd，这个 imm 实际的格式是%imm_i, 它是一个在指令编码 31-12bit 定义立即数，要把 31-12bit 的数值左移 12bit 得到最终结果，rd 实际的格式是%rd，是一个在指令编码 11-7bit 定义的 rd 寄存器的标号。可以看到 riscv 里对应的 trans 函数的实现如下，在编译时，脚本只生成一个空函数，函数内容需要前端实现者编写。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static bool trans_lui(DisasContext *ctx, arg_lui *a)
{
    if (a-&gt;rd != 0) {
        tcg_gen_movi_tl(cpu_gpr[a-&gt;rd], a-&gt;imm);
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;trans_xxx 函数的逻辑&lt;/h3&gt;
&lt;p&gt;trans_xxxx 函数的作用是生成中间码指令。以 riscv 的 add 指令为例，如下是 trans_rvi.c.inc 里 add 指令的模拟。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static bool trans_add(DisasContext *ctx, arg_add *a)
{
    return gen_arith(ctx, a, EXT_NONE, tcg_gen_add_tl, tcg_gen_add2_tl);
}
static bool gen_arith(DisasContext *ctx, arg_r *a, DisasExtend ext,
                      void (*func)(TCGv, TCGv, TCGv),
                      void (*f128)(TCGv, TCGv, TCGv, TCGv, TCGv, TCGv))
{
    TCGv dest = dest_gpr(ctx, a-&gt;rd);
    TCGv src1 = get_gpr(ctx, a-&gt;rs1, ext);
    TCGv src2 = get_gpr(ctx, a-&gt;rs2, ext);
    if (get_ol(ctx) &amp;#x3C; MXL_RV128) {
        func(dest, src1, src2);
        gen_set_gpr(ctx, a-&gt;rd, dest);
    } else {
        if (f128 == NULL) {
            return false;
        }
        TCGv src1h = get_gprh(ctx, a-&gt;rs1);
        TCGv src2h = get_gprh(ctx, a-&gt;rs2);
        TCGv desth = dest_gprh(ctx, a-&gt;rd);
        f128(dest, desth, src1, src1h, src2, src2h);
        gen_set_gpr128(ctx, a-&gt;rd, dest, desth);
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tcg_gen_add_i32 可以看作为 tcg_gen_add_tl 的函数入参，riscv 的 add 指令从 target CPU 的 rs1，rs2 两个寄存器里取两个加数，相加后放到 rd 寄存器里。这里在 TCG 体系中的操作从直观上看应该是已经操作完成了，但是实际上这里的操作只是保存了这一条指令的操作语义。tcg_gen_add_i32 的实现为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void tcg_gen_add_i32(TCGv_i32 ret, TCGv_i32 arg1, TCGv_i32 arg2)
{
    tcg_gen_op3_i32(INDEX_op_add_i32, ret, arg1, arg2);
}
tcg_gen_add_i32(TCGv_i32 ret, TCGv_i32 arg1, TCGv_i32 arg2)|tcg_gen_add_tl
  -&gt; tcg_gen_op3_i32(TCGOpcode opc, TCGv_i32 a1, TCGv_i32 a2, TCGv_i32 a3)
    -&gt; tcg_gen_op3(TCGOpcode opc, TCGArg a1, TCGArg a2, TCGArg a3)
        -&gt; TCGOp *op = tcg_emit_op(opc, 3);
    	-&gt; op-&gt;args[0] = a1;
    	-&gt; op-&gt;args[1] = a2;
    	-&gt; op-&gt;args[2] = a3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到最后生成的指令把数据挂到了一个链表里，后面的后端解码会把这些指令翻译成 host 指令。&lt;/p&gt;
&lt;h4&gt;TCG 体系的数据结构&lt;/h4&gt;
&lt;p&gt;这里需要简单介绍一下 TCG 体系的数据结构的定义。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// tcg/tcg.h
// TCG 的核心数据结构之一，它包含了 TCG 状态的所有全局信息
struct TCGContext {
    /* ... other fields ... */
    TCGOp *ops;               // 当前正在生成的 TCG 操作
    TCGTemp *temps;           // 临时变量数组
    int nb_temps;             // 临时变量数量
    TCGLabel *labels;         // 标签数组
    int nb_labels;            // 标签数量
    int code_gen_buffer_size; // 代码生成缓冲区大小
    uint8_t *code_gen_buffer; // 代码生成缓冲区
    /* ... other fields ... */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// tcg/tcg.h
// 表示一个 TCG 操作，通常是翻译后的目标指令
typedef struct TCGOp {
    TCGOpcode opc;           // 操作码
    TCGArg args[TCG_MAX_OP_ARGS]; // 操作的参数
} TCGOp;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// tcg/tcg.h
// 表示一个 TCG 临时变量
struct TCGTemp {
    TCGType type;            // 临时变量类型
    int val_type;            // 值类型
    int reg;                 // 寄存器编号
    int mem_reg;             // 内存寄存器编号
    tcg_target_long val;     // 临时变量的值
    /* ... other fields ... */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// tcg/tcg.h
// 表示一个标签，用于标记代码生成中的位置
typedef struct TCGLabel {
    tcg_insn_unit *label_ptr; // 标签指针，指向生成代码的位置
} TCGLabel;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// tcg/tcg-opc.h
// 一个枚举类型，表示 TCG 支持的所有操作码
typedef enum TCGOpcode {
    INDEX_op_add_i32,
    INDEX_op_sub_i32,
    /* ... other opcodes ... */
} TCGOpcode;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么 TCG 创建的变量存在哪里？TCGv cpu_gpr[reg_num]是一个全局变量，它如何索引到 target CPU 的寄存器？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;get_gpr(ctx, a-&gt;rs2, ext) &amp;#x3C;==&gt; return cpu_gpr[reg_num]
// cpu_gpr[i] 会在每个TB块CPUStatus初始化时创建
riscv_translate_init
	-&gt;cpu_gpr[i] = tcg_global_mem_new(tcg_env, offsetof(CPURISCVState, gpr[i]), riscv_int_regnames[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先 tcg_temp_new 分配的空间是在 TCGContext tcg_ctx 里的，所谓创建一个这样的 TCGv 就是在 tcg_ctx 里用去一个 TCGTemp。cpu_gpr[reg_num]可以索引到 target CPU 寄存器的基本逻辑就是只要在前端和后端约定好描述 target CPU 的软件结构，cpu_gpr[reg_num]描述的就是相关寄存器在这个软件结构里的位置。然后 tcg_env 在 tcg_context_init(unsigned max_cpus)里初始化，得到的是 tcg_ctx 里 TCGTemp temps 的地址。
tcg_global_mem_new 在 tcg_ctx 里从 TCGTemp temps 上分配空间，返回空间在 tcg_ctx 上的相对地址。这样 cpu_gpr[reg_name]就可以作为标记在前端和后端之间建立连接。
后端的代码直接把中间码翻译成 host 指令，中间码中的 TCGv 直接映射到 host CPU 的寄存器上，从逻辑上讲，应该是翻译得到的 host 代码修改中间码对应 TCGv 对应的内存才对。这里的逻辑是 qemu 在生成的中间码中以及 TB 执行后做了 host 寄存器到 target CPU 描述内存之间的同步。&lt;/p&gt;
&lt;h2&gt;指令添加流程&lt;/h2&gt;
&lt;p&gt;通过以上针对 TCG 前端的分析，这里会通过一个例子来展示如何添加 RISC-V 自定义指令，以 SADD 饱和加法指令为例。
首先需要在 insn32.decode 文件中定义指令编码，因为操作数格式与 r 类指令相同，因此不需要添加新的 Argument。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;sadd     0000000 .....    ..... 000 ..... 0001011 @r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 decodetree.py 脚本生成后会得到 sadd 指令的翻译函数与入参结构体(qemu/build/libqemu-riscv32-softmmu.fa.p/decode-insn32.c.inc)：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;typedef struct {
    int rd;
    int rs1;
    int rs2;
} arg_r;
typedef arg_r arg_sadd;
static bool trans_sadd(DisasContext *ctx, arg_sadd *a);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，sadd 指令的解析也被添加到 decode tree 中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;case 0x0000000b:
        /* ........ ........ ........ .0001011 */
        switch (insn &amp;#x26; 0xf8007000u) {
        case 0x00000000:
            /* 00000... ........ .000.... .0001011 */
            decode_insn32_extract_r(ctx, &amp;#x26;u.f_r, insn);
            switch ((insn &gt;&gt; 25) &amp;#x26; 0x3) {
            case 0x0:
                /* 0000000. ........ .000.... .0001011 */
                /* ../target/riscv/insn32.decode:187 */
                if (trans_sadd(ctx, &amp;#x26;u.f_r)) return true;
                break;
            }
            break;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体的 trans_sadd()函数功能需要我们在翻译文件中自己添加实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;target/riscv/insn_trans/trans_rvi.c.inc
static bool trans_sadd(DisasContext *ctx, arg_sadd *a)
{
    return gen_sadd(ctx, a, EXT_NONE);
}
static bool gen_sadd(DisasContext *ctx, arg_r *a, DisasExtend ext)
{
    TCGv rd = dest_gpr(ctx, a-&gt;rd);
    TCGv rs1 = get_gpr(ctx, a-&gt;rs1, ext);
    TCGv rs2 = get_gpr(ctx, a-&gt;rs2, ext);
    TCGv temp = tcg_temp_new_i32();
    TCGv max_val = tcg_constant_i32(0x7fffffff);
    TCGv min_val = tcg_constant_i32(0x80000000);
    TCGv zero = tcg_constant_i32(0);
    // temp = rs1 + rs2
    tcg_gen_add_i32(temp, rs1, rs2);
    // checkout overflow = (rs1 &gt; 0 &amp;#x26;&amp;#x26; rs2 &gt; 0 &amp;#x26;&amp;#x26; temp &amp;#x3C; 0)
    TCGv_i32 overflow = tcg_temp_new_i32();
    TCGv_i32 temp_cond = tcg_temp_new_i32();
    tcg_gen_setcond_i32(TCG_COND_GT, overflow, rs1, zero);
    tcg_gen_setcond_i32(TCG_COND_GT, temp_cond, rs2, zero);
    tcg_gen_and_i32(overflow, overflow, temp_cond);
    tcg_gen_setcond_i32(TCG_COND_LT, temp_cond, temp, zero);
    tcg_gen_and_i32(overflow, overflow, temp_cond);
    // checkout underflow = (rs1 &amp;#x3C; 0 &amp;#x26;&amp;#x26; rs2 &amp;#x3C; 0 &amp;#x26;&amp;#x26; temp &gt;= 0)
    TCGv_i32 underflow = tcg_temp_new_i32();
    tcg_gen_setcond_i32(TCG_COND_LT, underflow, rs1, zero);
    tcg_gen_setcond_i32(TCG_COND_LT, temp_cond, rs2, zero);
    tcg_gen_and_i32(underflow, underflow, temp_cond);
    tcg_gen_setcond_i32(TCG_COND_GE, temp_cond, temp, zero);
    tcg_gen_and_i32(underflow, underflow, temp_cond);
    /// if overflow, result = INT32_MAX
    tcg_gen_movcond_i32(TCG_COND_NE, rd, overflow, zero, max_val, temp);
    // if underflow, result = INT32_MIN
    tcg_gen_movcond_i32(TCG_COND_NE, rd, underflow, zero, min_val, temp);
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总体的添加流程简述如上。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Conda 使用</title><link>https://astro-pure.js.org/blog/conda</link><guid isPermaLink="true">https://astro-pure.js.org/blog/conda</guid><description>Conda 环境与包管理指南，包含 Miniconda 安装、环境创建与常用命令示例。</description><pubDate>Sat, 27 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;背景：在云服务器通过 conda 的虚拟环境安装 qemu 的依赖环境，将环境打包迁移部署到内网&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;strong&gt;Miniconda 介绍&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Conda 是用于管理依赖包和虚拟环境的工具（常常被用来作为 Python 环境管理和包管理的工具，但实际也是非常好用的 C++/C 包管理工具）。Anaconda 是一个完整的带有 Conda 工具的软件包，包含了 conda、python 等 180 多个科学包及其依赖项。它主要用于科学计算和数据分析，并提供了包管理与环境管理的功能。Anaconda 的安装包比较大，因为它预装了大量的科学计算工具和包。Miniconda 是 Anaconda 的&lt;strong&gt;轻量版&lt;/strong&gt;，只包含 conda 和 Python，但没有包含 Anaconda 中捆绑的科学计算和数据分析用的包。
Miniconda 的安装与换源暂且不表。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Miniconda 使用&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;创建并激活 Conda 虚拟环境&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt; conda create -n qemu_env python=3.12
 conda activate qemu_env
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;添加 Conda-Forge 频道&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt; conda config --add channels conda-forge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;conda-forge&lt;/code&gt; 是一个广泛使用的社区驱动的 Conda 包管理器通道（channel）。它由开源社区维护，包含了大量高质量的开源软件包和库。与官方的 Conda 通道相比，&lt;code&gt;conda-forge&lt;/code&gt; 通道通常会包含更多的包，更新速度也更快。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;安装所需要的包&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt; conda search xxx
 conda install xxx
 # or
 conda install -c conda-forge xxx # -c 参数用于指定要安装软件包的通道（channel）
 # or
 pip install xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;查看已安装的包（库）版本&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt; ninja --version
 &gt; 1.10.2
 pkg-config --modversion glib-2.0
 &gt; 2.78.0
 #or
 ls $CONDA_PREFIX/lib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要确保安装的目录在虚拟环境下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt; which pkg-config
 &gt; /root/miniconda3/envs/qemu_env/bin/pkg-config
 which pip
 &gt; /root/miniconda3/envs/qemu_env/bin/pip
 export PATH=$CONDA_PREFIX/bin:$PATH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照以上步骤安装的软件包或者库文件会安装在当前虚拟环境下。（确认是否进入创建的虚拟环境，而不是 base 环境）&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;虚拟环境打包及迁移&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;以下介绍仅适用于同系统下虚拟环境迁移，不同系统之间迁移情况会略有不同。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;打包虚拟环境&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;定位虚拟环境路径&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; conda info --envs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打包虚拟环境目录
&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20240830172801823.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt; tar -czvf qemu_env.tar.gz -C /path/to/conda/envs/qemu_env ./
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成一个包含虚拟环境的压缩文件 &lt;code&gt;qemu_env.tar.gz&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;迁移虚拟环境&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;内网服务器上解压并导入打包好的 Conda 环境&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt; tar -xzvf qemu_env.tar.gz -C /path/to/conda/envs/qemu_env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;💡
需要提前在内网服务器安装好 Miniconda，但是不需要提前创建新的虚拟环境&lt;/p&gt;
&lt;p&gt;激活环境&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;conda activate qemu_env
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;验证环境完整性&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在服务器上激活环境后，确保所有包和依赖都存在&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;conda list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;文件权限&lt;/strong&gt;: 确保迁移后的文件拥有正确的权限，特别是在不同用户之间迁移时，可能需要修改文件权限以确保虚拟环境能够正常使用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;外部依赖&lt;/strong&gt;: 如果虚拟环境中依赖系统库，需要确认这些库在当前服务器上也已安装。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;解决版本冲突&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;可能迁移成功后虚拟环境中有的包的版本与外部包的版本不一致，而当前所使用的包指向的是外部的包。这个问题通常与环境变量的优先级、路径设置以及多个版本的库共存有关。
e.g. 在 Conda 环境中安装了 &lt;code&gt;glib-2.78.0&lt;/code&gt;，但是 &lt;code&gt;pkg-config&lt;/code&gt; 仍然指向系统的 &lt;code&gt;glib-2.56.4&lt;/code&gt;，则可能是因为环境变量或路径配置导致了这个冲突。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;确认 &lt;code&gt;pkg-config&lt;/code&gt; 使用的是 Conda 环境的版本&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;**激活虚拟环境：**确保 Conda 虚拟环境 &lt;code&gt;qemu_env&lt;/code&gt; 已激活&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查 &lt;code&gt;pkg-config&lt;/code&gt; 的路径：&lt;/strong&gt; 确保当前的 &lt;code&gt;pkg-config&lt;/code&gt; 来自 Conda 环境&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证 &lt;code&gt;pkg-config&lt;/code&gt; 中的包版本&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;确保 Conda 环境中的 &lt;code&gt;pkg-config&lt;/code&gt; 优先级&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export PATH=$CONDA_PREFIX/bin:$PATH
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;设置链接库优先级&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在某些应用程序编译过程中，可能仍然会链接到系统的 &lt;code&gt;glib&lt;/code&gt;，此时可以检查链接器的配置。确保编译过程中使用的 &lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt; 和链接器标志指向 Conda 环境中的库文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export LD_LIBRARY_PATH=$CONDA_PREFIX/lib:$LD_LIBRARY_PATH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上为 Conda 虚拟环境的简单使用&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Docker 学习</title><link>https://astro-pure.js.org/blog/docker</link><guid isPermaLink="true">https://astro-pure.js.org/blog/docker</guid><description>Docker 基础知识学习笔记，包括 Docker 架构、安装、基本操作及 Dockerfile 自定义镜像。</description><pubDate>Thu, 02 Mar 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;1. 初识 Docker&lt;/h1&gt;
&lt;h2&gt;1.1. 什么是 Docker&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;分布式系统中，依赖的组件非常多，不同组件之间部署时往往会产生一些冲突。&lt;/li&gt;
&lt;li&gt;在数百上千台服务中重复部署，环境不一定一致，会遇到各种问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.1.1. 应用部署的环境问题&lt;/h3&gt;
&lt;p&gt;大型项目组件较多，运行环境也较为复杂，部署时会碰到一些问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;依赖关系复杂，容易出现兼容性问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开发、测试、生产环境有差异&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/algorithm/image-20230320131848119.png&quot; alt=&quot;Docker&quot;&gt;&lt;/p&gt;
&lt;p&gt;例如一个项目中，部署时需要依赖于 node.js、Redis、RabbitMQ、MySQL 等，这些服务部署时所需要的函数库、依赖项各不相同，甚至会有冲突。给部署带来了极大的困难。&lt;/p&gt;
&lt;h3&gt;1.1.2. Docker 解决依赖兼容问题&lt;/h3&gt;
&lt;p&gt;Docker 为了解决依赖的兼容问题的，采用了两个手段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;将应用的 Libs（函数库）、Deps（依赖）、配置与应用一起打包&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将每个应用放到一个隔离&lt;strong&gt;容器&lt;/strong&gt;去运行，避免互相干扰&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/algorithm/image-20230320131951012.png&quot; alt=&quot;image-20230320131951012&quot;&gt;&lt;/p&gt;
&lt;p&gt;这样打包好的应用包中，既包含应用本身，也保护应用所需要的 Libs、Deps，无需再操作系统上安装这些，自然就不存在不同应用之间的兼容问题了。&lt;/p&gt;
&lt;h3&gt;1.1.3. Docker 解决操作系统环境差异&lt;/h3&gt;
&lt;p&gt;要解决不同操作系统环境差异问题，必须先了解操作系统结构。以一个 Ubuntu 操作系统为例，结构如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/algorithm/image-20210731143401460.png&quot; alt=&quot;image-20210731143401460&quot;&gt;&lt;/p&gt;
&lt;p&gt;结构包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算机硬件：例如 CPU、内存、磁盘等&lt;/li&gt;
&lt;li&gt;系统内核：所有 Linux 发行版的内核都是 Linux，例如 CentOS、Ubuntu、Fedora 等。内核可以与计算机硬件交互，对外提供&lt;strong&gt;内核指令&lt;/strong&gt;，用于操作计算机硬件。&lt;/li&gt;
&lt;li&gt;系统应用：操作系统本身提供的应用、函数库。这些函数库是对内核指令的封装，使用更加方便。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应用与计算机交互的流程如下：&lt;/p&gt;
&lt;p&gt;1）应用调用操作系统应用（函数库），实现各种功能&lt;/p&gt;
&lt;p&gt;2）系统函数库是对内核指令集的封装，会调用内核指令&lt;/p&gt;
&lt;p&gt;3）内核指令操作计算机硬件&lt;/p&gt;
&lt;p&gt;Ubuntu 和 CentOS 都是基于 Linux 内核，无非是系统应用不同，提供的函数库有差异：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/algorithm/image-20210731144304990.png&quot; alt=&quot;image-20210731144304990&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时，如果将一个 Ubuntu 版本的 MySQL 应用安装到 CentOS 系统，MySQL 在调用 Ubuntu 函数库时，会发现找不到或者不匹配，就会报错了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/algorithm/image-20210731144458680.png&quot; alt=&quot;image-20210731144458680&quot;&gt;&lt;/p&gt;
&lt;p&gt;Docker 如何解决不同系统环境的问题？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker 将用户程序与所需要调用的系统 (比如 Ubuntu) 函数库一起打包&lt;/li&gt;
&lt;li&gt;Docker 运行到不同操作系统时，直接基于打包的函数库，借助于操作系统的 Linux 内核来运行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/algorithm/image-20210731144820638.png&quot; alt=&quot;image-20210731144820638&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1.1.4. 小结&lt;/h3&gt;
&lt;p&gt;Docker 如何解决大型项目依赖关系复杂，不同组件依赖的兼容性问题？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker 允许开发中将应用、依赖、函数库、配置一起&lt;strong&gt;打包&lt;/strong&gt;，形成可移植镜像&lt;/li&gt;
&lt;li&gt;Docker 应用运行在容器中，使用沙箱机制，相互&lt;strong&gt;隔离&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Docker 如何解决开发、测试、生产环境有差异的问题？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker 镜像中包含完整运行环境，包括系统函数库，仅依赖系统的 Linux 内核，因此可以在任意 Linux 操作系统上运行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Docker 是一个快速交付应用、运行应用的技术，具备下列优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以将程序及其依赖、运行环境一起打包为一个镜像，可以迁移到任意 Linux 操作系统&lt;/li&gt;
&lt;li&gt;运行时利用沙箱机制形成隔离容器，各个应用互不干扰&lt;/li&gt;
&lt;li&gt;启动、移除都可以通过一行命令完成，方便快捷&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.2. Docker 和虚拟机的区别&lt;/h2&gt;
&lt;p&gt;Docker 可以让一个应用在任何操作系统中非常方便的运行。而以前我们接触的虚拟机，也能在一个操作系统中，运行另外一个操作系统，保护系统中的任何应用。&lt;/p&gt;
&lt;p&gt;两者有什么差异呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;虚拟机&lt;/strong&gt;（virtual machine）是在操作系统中&lt;strong&gt;模拟&lt;/strong&gt;硬件设备，然后运行另一个操作系统，比如在 Windows 系统里面运行 Ubuntu 系统，这样就可以运行任意的 Ubuntu 应用了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docker&lt;/strong&gt; 仅仅是封装函数库，并没有模拟完整的操作系统，如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/algorithm/image-20210731145914960.png&quot; alt=&quot;image-20210731145914960&quot;&gt;&lt;/p&gt;
&lt;p&gt;对比来看：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20210731152243765.png&quot; alt=&quot;image-20210731152243765&quot;&gt;&lt;/p&gt;
&lt;p&gt;小结：&lt;/p&gt;
&lt;p&gt;Docker 和虚拟机的差异：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker 是一个系统进程；虚拟机是在操作系统中的操作系统&lt;/li&gt;
&lt;li&gt;Docker 体积小、启动速度快、性能好；虚拟机体积大、启动速度慢、性能一般&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.3. Docker 架构&lt;/h2&gt;
&lt;h3&gt;1.3.1. 镜像和容器&lt;/h3&gt;
&lt;p&gt;Docker 中有几个重要的概念：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;镜像（Image）&lt;/strong&gt;：Docker 将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起，称为镜像。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;容器（Container）&lt;/strong&gt;：镜像中的应用程序运行后形成的进程就是&lt;strong&gt;容器&lt;/strong&gt;，只是 Docker 会给容器进程做隔离，对外不可见。&lt;/p&gt;
&lt;p&gt;一切应用最终都是代码组成，都是硬盘中的一个个的字节形成的&lt;strong&gt;文件&lt;/strong&gt;。只有运行时，才会加载到内存，形成进程。&lt;/p&gt;
&lt;p&gt;而&lt;strong&gt;镜像&lt;/strong&gt;，就是把一个应用在硬盘上的文件、及其运行环境、部分系统函数库文件一起打包形成的文件包。这个文件包是只读的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;容器&lt;/strong&gt;呢，就是将这些文件中编写的程序、函数加载到内存中允许，形成进程，只不过要隔离起来。因此一个镜像可以启动多次，形成多个容器进程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20210731153059464.png&quot; alt=&quot;image-20210731153059464&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1.3.2. DockerHub&lt;/h3&gt;
&lt;p&gt;开源应用程序非常多，打包这些应用往往是重复的劳动。为了避免这些重复劳动，人们就会将自己打包的应用镜像，例如 Redis、MySQL 镜像放到网络上，共享使用，就像 GitHub 的代码共享一样。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DockerHub：DockerHub 是一个官方的 Docker 镜像的托管平台。这样的平台称为 Docker Registry。&lt;/li&gt;
&lt;li&gt;国内也有类似于 DockerHub 的公开服务，比如&lt;a href=&quot;https://c.163yun.com/hub&quot;&gt;网易云镜像服务&lt;/a&gt;、&lt;a href=&quot;https://cr.console.aliyun.com/&quot;&gt;阿里云镜像库&lt;/a&gt;等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们一方面可以将自己的镜像共享到 DockerHub，另一方面也可以从 DockerHub 拉取镜像：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20210731153743354.png&quot; alt=&quot;image-20210731153743354&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1.3.3. Docker 架构&lt;/h3&gt;
&lt;p&gt;我们要使用 Docker 来操作镜像、容器，就必须要安装 Docker。&lt;/p&gt;
&lt;p&gt;Docker 是一个 CS 架构 of 程序，由两部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务端 (server)：Docker 守护进程，负责处理 Docker 指令，管理镜像、容器等&lt;/li&gt;
&lt;li&gt;客户端 (client)：通过命令或 RestAPI 向 Docker 服务端发送指令。可以在本地或远程向服务端发送指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20210731154257653.png&quot; alt=&quot;image-20210731154257653&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1.3.4. 小结&lt;/h3&gt;
&lt;p&gt;镜像：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将应用程序及其依赖、环境、配置打包在一起&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;容器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;镜像运行起来就是容器，一个镜像可以运行多个容器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Docker 结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务端：接收命令或远程请求，操作镜像或容器&lt;/li&gt;
&lt;li&gt;客户端：发送命令或者请求到 Docker 服务端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DockerHub：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个镜像托管的服务器，类似的还有阿里云镜像服务，统称为 DockerRegistry&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.4. 安装 Docker&lt;/h2&gt;
&lt;p&gt;企业部署一般都是采用 Linux 操作系统，而其中又数 CentOS 发行版占比最多，因此常在 CentOS 下安装 Docker。&lt;/p&gt;
&lt;p&gt;Docker 分为 CE 和 EE 两大版本。CE 即社区版（免费，支持周期 7 个月），EE 即企业版，强调安全，付费使用，支持周期 24 个月。&lt;/p&gt;
&lt;p&gt;Docker CE 分为 &lt;code&gt;stable&lt;/code&gt; &lt;code&gt;test&lt;/code&gt; 和 &lt;code&gt;nightly&lt;/code&gt; 三个更新频道。&lt;/p&gt;
&lt;p&gt;官方网站上有各种环境下的&lt;a href=&quot;https://docs.docker.com/install/&quot;&gt;安装指南&lt;/a&gt;，这里主要介绍 Docker CE 在 CentOS 上的安装。&lt;/p&gt;
&lt;h3&gt;1.4.1. 卸载（可选）&lt;/h3&gt;
&lt;p&gt;如果之前安装过旧版本的 Docker，可以使用下面命令卸载：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-selinux \
                  docker-engine-selinux \
                  docker-engine \
                  docker-ce
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.4.2. 安装 Docker&lt;/h3&gt;
&lt;p&gt;首先需要大家虚拟机联网，安装 yum 工具&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;yum install -y yum-utils \
           device-mapper-persistent-data \
           lvm2 --skip-broken
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后更新本地镜像源：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 设置 docker 镜像源
yum-config-manager \
    --add-repo \
    https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

sed -i &apos;s/download.docker.com/mirrors.aliyun.com\/docker-ce/g&apos; /etc/yum.repos.d/docker-ce.repo

yum makecache fast
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后输入命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;yum install -y docker-ce
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.4.3. 启动 Docker&lt;/h3&gt;
&lt;p&gt;Docker 应用需要用到各种端口，逐一去修改防火墙设置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# 关闭
systemctl stop firewalld
# 禁止开机启动防火墙
systemctl disable firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过命令启动 Docker：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;systemctl start docker  # 启动 docker 服务
systemctl stop docker  # 停止 docker 服务
systemctl restart docker  # 重启 docker 服务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后输入命令，可以查看 Docker 版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20210418154704436.png&quot; alt=&quot;image-20210418154704436&quot;&gt;&lt;/p&gt;
&lt;h1&gt;2. Docker 的基本操作&lt;/h1&gt;
&lt;h2&gt;2.1. 镜像操作&lt;/h2&gt;
&lt;h3&gt;2.1.1. 镜像名称&lt;/h3&gt;
&lt;p&gt;首先来看下镜像的名称组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;镜名称一般分两部分组成：[repository]:[tag]。&lt;/li&gt;
&lt;li&gt;在没有指定 tag 时，默认是 latest，代表最新版本的镜像&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20210731155141362.png&quot; alt=&quot;image-20210731155141362&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2.1.2. 镜像命令&lt;/h3&gt;
&lt;p&gt;常见的镜像操作命令如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20210731155649535.png&quot; alt=&quot;image-20210731155649535&quot;&gt;&lt;/p&gt;
&lt;h2&gt;2.2. 容器操作&lt;/h2&gt;
&lt;h3&gt;2.2.1. 容器相关命令&lt;/h3&gt;
&lt;p&gt;容器操作的命令如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20210731161950495.png&quot; alt=&quot;image-20210731161950495&quot;&gt;&lt;/p&gt;
&lt;h2&gt;2.3. 数据卷（容器数据管理）&lt;/h2&gt;
&lt;h3&gt;2.3.1. 什么是数据卷&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;数据卷（volume）&lt;/strong&gt; 是一个虚拟目录，指向宿主机文件系统中的某个目录。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20210731173541846.png&quot; alt=&quot;image-20210731173541846&quot;&gt;&lt;/p&gt;
&lt;p&gt;一旦完成数据卷挂载，对容器的一切操作都会作用在数据卷对应的宿主机目录了。&lt;/p&gt;
&lt;h1&gt;3. Dockerfile 自定义镜像&lt;/h1&gt;
&lt;h2&gt;3.1. 镜像结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20210731175806273.png&quot; alt=&quot;image-20210731175806273&quot;&gt;&lt;/p&gt;
&lt;h2&gt;3.2. Dockerfile 语法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20210731180321133.png&quot; alt=&quot;image-20210731180321133&quot;&gt;&lt;/p&gt;
&lt;h1&gt;4. Docker-Compose&lt;/h1&gt;
&lt;p&gt;Docker Compose 可以基于 Compose 文件帮我们快速的部署分布式应用，而无需手动一个个创建和运行容器！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.willimt.com/uPic/learning/image-20210731180921742.png&quot; alt=&quot;image-20210731180921742&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>