<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>郭金锋</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://chillywall.github.io/ChillyBlog/</id>
  <link href="https://chillywall.github.io/ChillyBlog/" rel="alternate"/>
  <link href="https://chillywall.github.io/ChillyBlog/rss.xml" rel="self"/>
  <rights>All rights reserved 2026, 郭金锋</rights>
  <subtitle>渐吾身于尘垢兮，维余生之趋光</subtitle>
  <title>郭金锋的博客</title>
  <updated>2026-04-10T05:07:21.884Z</updated>
  <entry>
    <author>
      <name>郭金锋</name>
    </author>
    <category term="AI" scheme="https://chillywall.github.io/ChillyBlog/categories/AI/"/>
    <category term="Deployment" scheme="https://chillywall.github.io/ChillyBlog/categories/AI/Deployment/"/>
    <category term="TensorRT" scheme="https://chillywall.github.io/ChillyBlog/tags/TensorRT/"/>
    <content>
      <![CDATA[<p>本文介绍如何使用TensorRT部署一个简单的用于Cifar100分类任务的CNN模型。</p><span id="more"></span><p>本文项目源码在 <a href="https://github.com/ChillyWall/TensorRT-Deployment-Practices.git">这里</a>，所使用的模型为我自己训练的小型CNN模型，导出的ONNX格式模型和训练代码在 <a href="https://github.com/ChillyWall/CIFAR100-CNN-Classification-Practice.git">这里</a>。</p><h2 id="PyTorch-导出-ONNX-格式"><a href="#PyTorch-导出-ONNX-格式" class="headerlink" title="PyTorch 导出 ONNX 格式"></a>PyTorch 导出 ONNX 格式</h2><p>PyTorch 支持将模型导出到 ONNX 格式，便于后面使用 TensorRT 进行加速。支持动态 batch size，方便批处理。方法如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">export_onnx</span>(<span class="hljs-params">model, checkpoint</span>):<br>    model = CIFAR100_VGG()<br>    load_model_params(model, checkpoint)<br>    model.<span class="hljs-built_in">eval</span>()<br><br>    dummy_input = torch.randn(<span class="hljs-number">1</span>, <span class="hljs-number">3</span>, <span class="hljs-number">32</span>, <span class="hljs-number">32</span>)<br>    torch.onnx.export(<br>        model,<br>        dummy_input,<br>        <span class="hljs-string">&quot;cifar100_vgg.onnx&quot;</span>,<br>        verbose=<span class="hljs-literal">True</span>,<br>        input_names=[<span class="hljs-string">&quot;input&quot;</span>],  <span class="hljs-comment"># 输入节点名称</span><br>        output_names=[<span class="hljs-string">&quot;output&quot;</span>],  <span class="hljs-comment"># 输出节点名称</span><br>        opset_version=<span class="hljs-number">13</span>,  <span class="hljs-comment"># 算子集版本</span><br>        dynamic_axes=&#123;<br>            <span class="hljs-string">&quot;input&quot;</span>: &#123;<span class="hljs-number">0</span>: <span class="hljs-string">&quot;batch_size&quot;</span>&#125;,  <span class="hljs-comment"># 第0维 = 动态 batch</span><br>            <span class="hljs-string">&quot;output&quot;</span>: &#123;<span class="hljs-number">0</span>: <span class="hljs-string">&quot;batch_size&quot;</span>&#125;,  <span class="hljs-comment"># 输出第0维也跟着动态</span><br>        &#125;,<br>    )<br><br><br><span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">&quot;__main__&quot;</span>:<br>    model = CIFAR100_VGG()<br>    export_onnx(model, <span class="hljs-string">&quot;./checkpoints/model_best.pth&quot;</span>)<br></code></pre></td></tr></table></figure><h2 id="TensorRT-编译-ONNX-模型"><a href="#TensorRT-编译-ONNX-模型" class="headerlink" title="TensorRT 编译 ONNX 模型"></a>TensorRT 编译 ONNX 模型</h2><p>之后是使用 TensorRT 编译导出的 ONNX 模型。TensorRT 编译主要有三种方法，分别是通 过 trtexec 命令和通过 TensorRT 的 Python 和 C++的 API。</p><p>trtexec 命令最为简单快速，适应简单模型。而 Python API 相对要更方便一些，支持更多细致的配置。但是如果有自定义插件等更复杂的要求，还是需要 C++ API，其提供最多的特性和最高的性能。</p><p>这里进行的是简单的 CNN 模型，于是直接使用 trtexec 命令进行编译。命令大致如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">trtexec --onnx=cifar100_vgg.onnx --saveEngine=cifar100_vgg.engine --fp16<br></code></pre></td></tr></table></figure><p>在训练的过程中往往使用 32 位浮点型作为计算的数据结构，而在工程实践中，则常常使用 16 位，这在精度影响不大的情况下获得更高的速度。这便是量化。</p><p>量化的本质是：用更低比特（更少位数）的数据类型来近似表示高精度的浮点数据。除了 fp32 和 fp16，有时为了极致速度会使用 int8，这还会牵扯到“校准”的过程，精度也容易 出现明显下降。</p><p>这里我们使用常用的 fp16 来加速。</p><h3 id="模型性能简单评估"><a href="#模型性能简单评估" class="headerlink" title="模型性能简单评估"></a>模型性能简单评估</h3><p>可以使用 trtexec 提供的功能来简单评估模型的性能。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">trtexec --loadEngine=cifar100_vgg.engine<br></code></pre></td></tr></table></figure><p>输出的信息中有延迟和吞吐量等信息。吞吐量表示每秒可以处理多少个请求(queries per second&#x2F;qps)。</p><h2 id="使用-TensorRT-C-API-进行模型构建"><a href="#使用-TensorRT-C-API-进行模型构建" class="headerlink" title="使用 TensorRT C++ API 进行模型构建"></a>使用 TensorRT C++ API 进行模型构建</h2><p>目前计划写一个简单的 TensorRT C++ API 的封装工具，用于部署简单的模型，封装构建， 推理，预处理等过程，便于之后的学习。</p><p>目前已完成构建部分的代码，目前该模块实现了从 .engine 文件中直接加载模型和先将模 型从 onnx 格式转化再加载的功能。同时对 Nvidia 相关库中的指针进行了简单的 RAII 封 装，使用 unique<sub>ptr</sub> 保证保证资源被正确释放并自定了删除器。</p><p>后续会尝试通过 C++ API 进行推理，并使用 OpenCV 做基础的预处理。这些都完善之后就 是想办法将其集成到 ROS 中，在 Gazebo 仿真环境下进行基础的视觉任务。</p><h3 id="使用-TensorRT-C-API-构建模型"><a href="#使用-TensorRT-C-API-构建模型" class="headerlink" title="使用 TensorRT C++ API 构建模型"></a>使用 TensorRT C++ API 构建模型</h3><p>封装了编译模型和加载模型的接口。在编译模型后将其保存为 <code>.engine</code> 文件，编译模型函数接 受一个回调函数，用于配置构建器等。也可以从 <code>.engine</code> 文件加载模型。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// ModelBuilder.h</span><br><span class="hljs-keyword">using</span> TRTBuildConfigFun =<br>    std::function&lt;<span class="hljs-built_in">void</span>(nvinfer1::IBuilderConfig*, nvinfer1::INetworkDefinition*,<br>                       nvinfer1::IBuilder*)&gt;;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">TRTModelBuilder</span> &#123;<br><span class="hljs-keyword">public</span>:<br>    <span class="hljs-built_in">TRTModelBuilder</span>(nvinfer1::ILogger&amp; logger) : <span class="hljs-built_in">m_logger</span>(logger) &#123;&#125;<br><br>    <span class="hljs-comment">// 从本地 .engine (Plan) 文件加载</span><br>    <span class="hljs-function">TRTPtr&lt;nvinfer1::ICudaEngine&gt; <span class="hljs-title">loadFromPlan</span><span class="hljs-params">(<span class="hljs-type">const</span> std::string&amp; enginePath)</span></span>;<br><br>    <span class="hljs-comment">// 从 ONNX 编译并保存为 .engine</span><br>    <span class="hljs-function">TRTPtr&lt;nvinfer1::ICudaEngine&gt;</span><br><span class="hljs-function">    <span class="hljs-title">buildFromOnnx</span><span class="hljs-params">(<span class="hljs-type">const</span> std::string&amp; onnxPath, <span class="hljs-type">const</span> std::string&amp; enginePath,</span></span><br><span class="hljs-params"><span class="hljs-function">                  TRTBuildConfigFun configFun = <span class="hljs-literal">nullptr</span>)</span></span>;<br><br><span class="hljs-keyword">private</span>:<br>    nvinfer1::ILogger&amp; m_logger;<br>&#125;;<br><br><span class="hljs-comment">// ModelBuilder.cpp</span><br><span class="hljs-function">TRTPtr&lt;nvinfer1::ICudaEngine&gt;</span><br><span class="hljs-function"><span class="hljs-title">TRTModelBuilder::loadFromPlan</span><span class="hljs-params">(<span class="hljs-type">const</span> std::string&amp; enginePath)</span> </span>&#123;<br>    <span class="hljs-function">std::ifstream <span class="hljs-title">file</span><span class="hljs-params">(enginePath, std::ios::binary)</span></span>;<br>    <span class="hljs-keyword">if</span> (!file.<span class="hljs-built_in">good</span>())<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">nullptr</span>;<br><br>    file.<span class="hljs-built_in">seekg</span>(<span class="hljs-number">0</span>, std::ios::end);<br>    <span class="hljs-type">size_t</span> size = file.<span class="hljs-built_in">tellg</span>();<br>    file.<span class="hljs-built_in">seekg</span>(<span class="hljs-number">0</span>, std::ios::beg);<br><br>    <span class="hljs-function">std::vector&lt;<span class="hljs-type">char</span>&gt; <span class="hljs-title">data</span><span class="hljs-params">(size)</span></span>;<br>    file.<span class="hljs-built_in">read</span>(data.<span class="hljs-built_in">data</span>(), size);<br><br>    <span class="hljs-keyword">auto</span> runtime =<br>        <span class="hljs-built_in">TRTPtr</span>&lt;nvinfer1::IRuntime&gt;(nvinfer1::<span class="hljs-built_in">createInferRuntime</span>(m_logger));<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">TRTPtr</span>&lt;nvinfer1::ICudaEngine&gt;(<br>        runtime-&gt;<span class="hljs-built_in">deserializeCudaEngine</span>(data.<span class="hljs-built_in">data</span>(), size));<br>&#125;<br><br><span class="hljs-function">TRTPtr&lt;nvinfer1::ICudaEngine&gt;</span><br><span class="hljs-function"><span class="hljs-title">TRTModelBuilder::buildFromOnnx</span><span class="hljs-params">(<span class="hljs-type">const</span> std::string&amp; onnxPath,</span></span><br><span class="hljs-params"><span class="hljs-function">                               <span class="hljs-type">const</span> std::string&amp; enginePath,</span></span><br><span class="hljs-params"><span class="hljs-function">                               TRTBuildConfigFun configFun)</span> </span>&#123;<br>    <span class="hljs-keyword">auto</span> builder =<br>        <span class="hljs-built_in">TRTPtr</span>&lt;nvinfer1::IBuilder&gt;(nvinfer1::<span class="hljs-built_in">createInferBuilder</span>(m_logger));<br>    <span class="hljs-keyword">auto</span> network =<br>        <span class="hljs-built_in">TRTPtr</span>&lt;nvinfer1::INetworkDefinition&gt;(builder-&gt;<span class="hljs-built_in">createNetworkV2</span>(<span class="hljs-number">0U</span>));<br>    <span class="hljs-keyword">auto</span> config =<br>        <span class="hljs-built_in">TRTPtr</span>&lt;nvinfer1::IBuilderConfig&gt;(builder-&gt;<span class="hljs-built_in">createBuilderConfig</span>());<br>    <span class="hljs-keyword">auto</span> parser = <span class="hljs-built_in">TRTPtr</span>&lt;nvonnxparser::IParser&gt;(<br>        nvonnxparser::<span class="hljs-built_in">createParser</span>(*network, m_logger));<br><br>    <span class="hljs-comment">// 如果解析失败，说明 onnx 模型有问题</span><br>    <span class="hljs-keyword">if</span> (!parser-&gt;<span class="hljs-built_in">parseFromFile</span>(<br>            onnxPath.<span class="hljs-built_in">c_str</span>(),<br>            <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">int</span>&gt;(nvinfer1::ILogger::Severity::kWARNING))) &#123;<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">nullptr</span>;<br>    &#125;<br><br>    <span class="hljs-keyword">if</span> (configFun) &#123;<br>        <span class="hljs-comment">// 调用配置函数</span><br>        <span class="hljs-built_in">configFun</span>(config.<span class="hljs-built_in">get</span>(), network.<span class="hljs-built_in">get</span>(), builder.<span class="hljs-built_in">get</span>());<br>    &#125;<br><br>    <span class="hljs-comment">// 编译模型</span><br>    <span class="hljs-keyword">auto</span> plan = <span class="hljs-built_in">TRTPtr</span>&lt;nvinfer1::IHostMemory&gt;(<br>        builder-&gt;<span class="hljs-built_in">buildSerializedNetwork</span>(*network, *config));<br>    <span class="hljs-keyword">if</span> (!plan)<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">nullptr</span>;<br><br>    <span class="hljs-comment">// 将编译好的 Engine 保存到磁盘，下次直接 load</span><br>    <span class="hljs-function">std::ofstream <span class="hljs-title">outfile</span><span class="hljs-params">(enginePath, std::ios::binary)</span></span>;<br>    outfile.<span class="hljs-built_in">write</span>(<span class="hljs-built_in">reinterpret_cast</span>&lt;<span class="hljs-type">const</span> <span class="hljs-type">char</span>*&gt;(plan-&gt;<span class="hljs-built_in">data</span>()), plan-&gt;<span class="hljs-built_in">size</span>());<br><br>    <span class="hljs-keyword">auto</span> runtime =<br>        <span class="hljs-built_in">TRTPtr</span>&lt;nvinfer1::IRuntime&gt;(nvinfer1::<span class="hljs-built_in">createInferRuntime</span>(m_logger));<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">TRTPtr</span>&lt;nvinfer1::ICudaEngine&gt;(<br>        runtime-&gt;<span class="hljs-built_in">deserializeCudaEngine</span>(plan-&gt;<span class="hljs-built_in">data</span>(), plan-&gt;<span class="hljs-built_in">size</span>()));<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="使用-OpenCV-进行图片预处理"><a href="#使用-OpenCV-进行图片预处理" class="headerlink" title="使用 OpenCV 进行图片预处理"></a>使用 OpenCV 进行图片预处理</h3><p>首先要获得用于推理的图片，当前是用 PyTorch 加载数据集之后使用 OpenCV 直接导出其 原始图片，并记录了元数据，记录了每张图片的类型 id 和名称，可以用于后续的推理验证。</p><p>要进行推理，需要将图片读取并完成预处理，包括转化成浮点数和归一化，以及将排列顺序 调整成 TensorRT 使用的顺序。</p><p>一般的图片使用 RGB 的格式，而 OpenCV 则使用 BGR 来存储图片，这使得要将 OpenCV 中 的图片喂给 TensorRT，必须先将 BGR 转为 RGB。</p><p>除此之外，OpenCV 默认使用 HWC 的顺序排列数据，即按照 RGBRGBRGB的形式排列，而 在 TensorRT 中，为了提升访存效率，使用 CHW 布局进行排列，即 RRRGGGBBB 的形 式。必须重新排布数据才能进行推理。</p><p>之后就是 PyTorch 中训练进行了张量化和归一化，因此要进行推理，我们也需要将原本的 uint8 类型转为 float，再归一化。</p><p>目前已实现了一个对 Cifar100 数据集进行预处理的类，并进行了一些性能优化上的尝试。</p><p>目前的实现中大量使用了模板元编程，使用模板参数来传递各种静态信息增强通用性，比如 通过可变模板参数实现编译期的数组等。</p><p>对于类似输入数据的维度，均值和方差等常量，以及不同颜色空间的映射声明模板类和对应 的 concept，声明 constexpr 成员函数和变量来传递。</p><p>将原本通过 opencv 完成的颜色空间重映射改为在改变排列顺序和归一化时一并进行，效率 比原本要再高一些，同时支持不同的颜色空间变换。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// common.h</span><br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">TRTDeleter</span> &#123;<br>    <span class="hljs-keyword">template</span> &lt;<span class="hljs-keyword">typename</span> T&gt;<br>    <span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">operator</span><span class="hljs-params">()</span><span class="hljs-params">(T* obj)</span> <span class="hljs-type">const</span> </span>&#123;<br>        <span class="hljs-keyword">if</span> (obj) &#123;<br><span class="hljs-meta">#<span class="hljs-keyword">if</span> NV_TENSORRT_MAJOR &lt; 9</span><br>            obj-&gt;<span class="hljs-built_in">destroy</span>();<br><span class="hljs-meta">#<span class="hljs-keyword">else</span></span><br>            <span class="hljs-keyword">delete</span> obj;  <span class="hljs-comment">// TensorRT 10.0+ 推荐做法</span><br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br>        &#125;<br>    &#125;<br>&#125;;<br><br><span class="hljs-keyword">template</span> &lt;<span class="hljs-keyword">typename</span> T&gt;<br><span class="hljs-keyword">using</span> TRTPtr = std::unique_ptr&lt;T, TRTDeleter&gt;;<br><br><span class="hljs-keyword">template</span> &lt;<span class="hljs-keyword">typename</span> T&gt;<br><span class="hljs-keyword">concept</span> Processor = <span class="hljs-built_in">requires</span>(<span class="hljs-type">const</span> cv::Mat&amp; img, <span class="hljs-type">float</span>* output) &#123;<br>    &#123; T::<span class="hljs-built_in">process</span>(img, output) &#125; -&gt; std::same_as&lt;<span class="hljs-type">void</span>&gt;;<br>&#125;;<br><br><span class="hljs-comment">// 张量规格，编译期维度信息</span><br><span class="hljs-keyword">template</span> &lt;<span class="hljs-type">size_t</span>... Sizes&gt;<br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">TensorSpec</span> &#123;<br>    <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-keyword">constexpr</span> <span class="hljs-type">size_t</span> <span class="hljs-title">total_size</span><span class="hljs-params">()</span> </span>&#123;<br>        <span class="hljs-type">size_t</span> size = <span class="hljs-number">1</span>;<br>        ((size *= Sizes), ...);<br>        <span class="hljs-keyword">return</span> size;<br>    &#125;<br>    <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-keyword">constexpr</span> std::array&lt;<span class="hljs-type">size_t</span>, <span class="hljs-keyword">sizeof</span>...<span class="hljs-params">(Sizes)</span>&gt; <span class="hljs-title">dims</span><span class="hljs-params">()</span> </span>&#123;<br>        <span class="hljs-keyword">return</span> &#123;Sizes...&#125;;<br>    &#125;<br>&#125;;<br><br><span class="hljs-keyword">template</span> &lt;<span class="hljs-keyword">typename</span> T&gt;<br><span class="hljs-keyword">concept</span> TensorSpecType = <span class="hljs-keyword">requires</span> &#123;<br>    &#123; T::<span class="hljs-built_in">total_size</span>() &#125; -&gt; std::convertible_to&lt;<span class="hljs-type">size_t</span>&gt;;<br>&#125;;<br><br><span class="hljs-comment">// 编译期数组</span><br><span class="hljs-keyword">template</span> &lt;<span class="hljs-type">float</span>... elems&gt;<br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">FloatArraySpec</span> &#123;<br>    <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-keyword">constexpr</span> std::array&lt;<span class="hljs-type">float</span>, <span class="hljs-keyword">sizeof</span>...<span class="hljs-params">(elems)</span>&gt; <span class="hljs-title">values</span><span class="hljs-params">()</span> </span>&#123;<br>        <span class="hljs-keyword">return</span> &#123;elems...&#125;;<br>    &#125;<br>&#125;;<br><br><span class="hljs-comment">/* 是是否编译期数组规格类型，即可通过values()方法获取std::array&lt;float,</span><br><span class="hljs-comment"> * N&gt;类型的数组，其中N为元素个数个数 */</span><br><span class="hljs-keyword">template</span> &lt;<span class="hljs-keyword">typename</span> T&gt;<br><span class="hljs-keyword">concept</span> FloatArraySpecType = <span class="hljs-keyword">requires</span> &#123;<br>    &#123;<br>        T::<span class="hljs-built_in">values</span>()<br>    &#125; -&gt; std::convertible_to&lt;std::array&lt;<span class="hljs-type">float</span>, T::<span class="hljs-built_in">values</span>().<span class="hljs-built_in">size</span>()&gt;&gt;;<br>&#125;;<br><br><span class="hljs-comment">// 颜色空间映射，编译器索引信息</span><br><span class="hljs-comment">// 注意：使用RGB表示第一个，第二个和第三个通道的索引位置，哪怕你不是要转成RGB</span><br><span class="hljs-keyword">template</span> &lt;<span class="hljs-keyword">typename</span> T&gt;<br><span class="hljs-keyword">concept</span> ChannelMapType =<br>    <span class="hljs-keyword">requires</span> &#123;<br>        &#123; T::r &#125; -&gt; std::convertible_to&lt;<span class="hljs-type">int</span>&gt;;<br>        &#123; T::g &#125; -&gt; std::convertible_to&lt;<span class="hljs-type">int</span>&gt;;<br>        &#123; T::b &#125; -&gt; std::convertible_to&lt;<span class="hljs-type">int</span>&gt;;<br>        &#123; T::<span class="hljs-built_in">index</span>() &#125; -&gt; std::convertible_to&lt;std::array&lt;<span class="hljs-type">int</span>, 3&gt;&gt;;<br>    &#125; &amp;&amp; (T::r &gt;= <span class="hljs-number">0</span> &amp;&amp; T::r &lt; <span class="hljs-number">3</span>) &amp;&amp; (T::g &gt;= <span class="hljs-number">0</span> &amp;&amp; T::g &lt; <span class="hljs-number">3</span>) &amp;&amp;<br>    (T::b &gt;= <span class="hljs-number">0</span> &amp;&amp; T::b &lt; <span class="hljs-number">3</span>);<br><br><span class="hljs-keyword">template</span> &lt;<span class="hljs-type">int</span> R, <span class="hljs-type">int</span> G, <span class="hljs-type">int</span> B&gt;<br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">ChannelMapSpec</span> &#123;<br>    <span class="hljs-type">static</span> <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> r = R;<br>    <span class="hljs-type">static</span> <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> g = G;<br>    <span class="hljs-type">static</span> <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> b = B;<br><br>    <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-keyword">constexpr</span> std::array&lt;<span class="hljs-type">int</span>, 3&gt; <span class="hljs-title">index</span><span class="hljs-params">()</span> </span>&#123;<br>        <span class="hljs-keyword">return</span> &#123;R, G, B&#125;;<br>    &#125;<br>&#125;;<br><br><span class="hljs-keyword">using</span> KeepChannelMap = ChannelMapSpec&lt;<span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>&gt;;<br><br><br><span class="hljs-comment">// Processor.h</span><br><span class="hljs-keyword">template</span> &lt;<span class="hljs-type">size_t</span>... Is&gt;<br><span class="hljs-function"><span class="hljs-keyword">constexpr</span> <span class="hljs-keyword">auto</span> <span class="hljs-title">make_alphas_impl</span><span class="hljs-params">(<span class="hljs-type">const</span> std::array&lt;<span class="hljs-type">float</span>, <span class="hljs-keyword">sizeof</span>...(Is)&gt;&amp; stds,</span></span><br><span class="hljs-params"><span class="hljs-function">                                std::index_sequence&lt;Is...&gt;)</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> std::array&lt;<span class="hljs-type">float</span>, <span class="hljs-keyword">sizeof</span>...(Is)&gt; &#123;(<span class="hljs-number">1.0f</span> / (<span class="hljs-number">255.0f</span> * stds[Is]))...&#125;;<br>&#125;<br><br><span class="hljs-keyword">template</span> &lt;<span class="hljs-type">size_t</span>... Is&gt;<br><span class="hljs-function"><span class="hljs-keyword">constexpr</span> <span class="hljs-keyword">auto</span> <span class="hljs-title">make_betas_impl</span><span class="hljs-params">(<span class="hljs-type">const</span> std::array&lt;<span class="hljs-type">float</span>, <span class="hljs-keyword">sizeof</span>...(Is)&gt;&amp; means,</span></span><br><span class="hljs-params"><span class="hljs-function">                               <span class="hljs-type">const</span> std::array&lt;<span class="hljs-type">float</span>, <span class="hljs-keyword">sizeof</span>...(Is)&gt;&amp; stds,</span></span><br><span class="hljs-params"><span class="hljs-function">                               std::index_sequence&lt;Is...&gt;)</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> std::array&lt;<span class="hljs-type">float</span>, <span class="hljs-keyword">sizeof</span>...(Is)&gt; &#123;(-means[Is] / stds[Is])...&#125;;<br>&#125;<br><br><span class="hljs-keyword">template</span> &lt;TensorSpecType InputSpec, FloatArraySpecType Mean,<br>          FloatArraySpecType Std, ChannelMapType ChannelMap = KeepChannelMap&gt;<br><span class="hljs-keyword">class</span> ConvertHWC2CHW &#123;<br><span class="hljs-keyword">private</span>:<br>    <span class="hljs-keyword">constexpr</span> <span class="hljs-type">static</span> <span class="hljs-type">int</span> input_height = InputSpec::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">0</span>];<br>    <span class="hljs-keyword">constexpr</span> <span class="hljs-type">static</span> <span class="hljs-type">int</span> input_width = InputSpec::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">1</span>];<br>    <span class="hljs-keyword">constexpr</span> <span class="hljs-type">static</span> <span class="hljs-type">int</span> channel_num = InputSpec::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">2</span>];<br>    <span class="hljs-keyword">constexpr</span> <span class="hljs-type">static</span> std::array&lt;<span class="hljs-type">int</span>, channel_num&gt; channel_map =<br>        ChannelMap::<span class="hljs-built_in">index</span>();<br><br>    <span class="hljs-keyword">constexpr</span> <span class="hljs-type">static</span> std::array&lt;<span class="hljs-type">float</span>, channel_num&gt; alphas = <span class="hljs-built_in">make_alphas_impl</span>(<br>        Std::<span class="hljs-built_in">values</span>(), std::make_index_sequence&lt;channel_num&gt; &#123;&#125;);<br>    <span class="hljs-keyword">constexpr</span> <span class="hljs-type">static</span> std::array&lt;<span class="hljs-type">float</span>, channel_num&gt; betas =<br>        <span class="hljs-built_in">make_betas_impl</span>(Mean::<span class="hljs-built_in">values</span>(), Std::<span class="hljs-built_in">values</span>(),<br>                        std::make_index_sequence&lt;channel_num&gt; &#123;&#125;);<br><br><span class="hljs-keyword">public</span>:<br>    <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">void</span> <span class="hljs-title">process</span><span class="hljs-params">(<span class="hljs-type">const</span> cv::Mat&amp; input, <span class="hljs-type">float</span>* output)</span> </span>&#123;<br>        <span class="hljs-type">int</span> channel_size = input_height * input_width;<br><br>        std::array&lt;cv::Mat, channel_num&gt; bgr_channels;<br>        cv::<span class="hljs-built_in">split</span>(input, bgr_channels);<br><br>        <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; channel_num; ++i) &#123;<br>            <span class="hljs-function">cv::Mat <span class="hljs-title">target_slice</span><span class="hljs-params">(</span></span><br><span class="hljs-params"><span class="hljs-function">                input_height, input_width, CV_32FC1,</span></span><br><span class="hljs-params"><span class="hljs-function">                output + ChannelMap::index()[i] * channel_size)</span></span>;<br><br>            bgr_channels[i].<span class="hljs-built_in">convertTo</span>(target_slice, CV_32FC1,<br>                                      alphas[channel_map[i]],<br>                                      betas[channel_map[i]]);<br>        &#125;<br>    &#125;<br>&#125;;<br></code></pre></td></tr></table></figure><h3 id="编写推理类"><a href="#编写推理类" class="headerlink" title="编写推理类"></a>编写推理类</h3><p>声明推理类，用于统一管理上下文和流的生命周期，并提供访问的 API。近用于执行推理， 数据的输入输出由外部管理，所需参数由外部提供。</p><p>推理类在构造时接受 ICudaEngine 对象，并创建其引用。之后通过它创建上下文 （IExecuteContext），并创建一个流（CudaStream）。通过对外提供的公共接口由外部分配内存并绑定模型输入输出的内存指针，对外提供的 <code>infer</code> 接口仅运行推理操作，由外部 负责及时读取和写入输入输出。<code>infer</code> 函数接受一个回调函数，用于对 <code>context</code> 进行一些 配置，如设置动态的 batch size。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">using</span> TRTInferConfigFun = std::function&lt;<span class="hljs-built_in">void</span>(nvinfer1::IExecutionContext*)&gt;;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">TRTInference</span> &#123;<br><span class="hljs-keyword">protected</span>:<br>    nvinfer1::ICudaEngine&amp; engine;<br>    TRTPtr&lt;nvinfer1::IExecutionContext&gt; context;<br>    cudaStream_t stream;<br><br><span class="hljs-keyword">public</span>:<br>    <span class="hljs-built_in">TRTInference</span>() = <span class="hljs-keyword">delete</span>;<br><br>    <span class="hljs-built_in">TRTInference</span>(nvinfer1::ICudaEngine&amp; engine)<br>        : <span class="hljs-built_in">engine</span>(engine), <span class="hljs-built_in">context</span>(engine.<span class="hljs-built_in">createExecutionContext</span>()) &#123;<br>        <span class="hljs-built_in">cudaStreamCreate</span>(&amp;stream);<br>    &#125;<br><br>    <span class="hljs-built_in">TRTInference</span>(<span class="hljs-type">const</span> TRTInference&amp;) = <span class="hljs-keyword">delete</span>;<br>    TRTInference&amp; <span class="hljs-keyword">operator</span>=(<span class="hljs-type">const</span> TRTInference&amp;) = <span class="hljs-keyword">delete</span>;<br>    <span class="hljs-built_in">TRTInference</span>(TRTInference&amp;&amp;) <span class="hljs-keyword">noexcept</span> = <span class="hljs-keyword">delete</span>;<br>    TRTInference&amp; <span class="hljs-keyword">operator</span>=(TRTInference&amp;&amp;) <span class="hljs-keyword">noexcept</span> = <span class="hljs-keyword">delete</span>;<br><br>    ~<span class="hljs-built_in">TRTInference</span>() &#123;<br>        <span class="hljs-built_in">cudaStreamDestroy</span>(stream);<br>    &#125;<br><br>    <span class="hljs-keyword">template</span> &lt;<span class="hljs-keyword">typename</span>... Args&gt;<br>    <span class="hljs-function"><span class="hljs-type">bool</span> <span class="hljs-title">set_tensor_address</span><span class="hljs-params">(Args&amp;&amp;... args)</span> </span>&#123;<br>        <span class="hljs-keyword">return</span> context-&gt;<span class="hljs-built_in">setTensorAddress</span>(std::forward&lt;Args&gt;(args)...);<br>    &#125;<br><br>    <span class="hljs-keyword">template</span> &lt;<span class="hljs-keyword">typename</span>... Args&gt;<br>    <span class="hljs-function"><span class="hljs-type">const</span> <span class="hljs-type">void</span>* <span class="hljs-title">get_tensor_address</span><span class="hljs-params">(Args&amp;&amp;... args)</span> </span>&#123;<br>        <span class="hljs-keyword">return</span> context-&gt;<span class="hljs-built_in">getTensorAddress</span>(std::forward&lt;Args&gt;(args)...);<br>    &#125;<br><br>    <span class="hljs-function">cudaStream_t <span class="hljs-title">get_stream</span><span class="hljs-params">()</span> </span>&#123;<br>        <span class="hljs-keyword">return</span> stream;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-type">bool</span> <span class="hljs-title">infer</span><span class="hljs-params">(TRTInferConfigFun configFun = <span class="hljs-literal">nullptr</span>)</span> </span>&#123;<br>        <span class="hljs-keyword">if</span> (configFun) &#123;<br>            <span class="hljs-built_in">configFun</span>(context.<span class="hljs-built_in">get</span>());<br>        &#125;<br>        <span class="hljs-keyword">return</span> context-&gt;<span class="hljs-built_in">enqueueV3</span>(stream);<br>    &#125;<br>&#125;;<br></code></pre></td></tr></table></figure><h3 id="编写-Cifar100-模型的运行类"><a href="#编写-Cifar100-模型的运行类" class="headerlink" title="编写 Cifar100 模型的运行类"></a>编写 Cifar100 模型的运行类</h3><p>为 Cifar100 编写运行类，接受模型的文件路径，声明各种模板类型，负责分配显存存储模 型的输入输出，以及从 CPU 向 GPU 传递数据的临时缓冲区。封装预处理，向 gpu 传入数 据，发送推理任务，从 gpu 读取结果，后处理等流程。推理接口接受图片数组并传回每张 图片的分类结果，即对应类别 ID。</p><p>支持动态 batch size，在 infer 函数中将输入数组分割成一个个 batch，再批量进行处理。 预处理和后处理部分使用 OpenMP 进行简单并行处理。模型构建时设置最小，最优，最大 batch size 分别为 1，64，256。分配显存和内存时按照最大来分配避免重复分配，传递数 据和推理时使用动态大小。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// Cifar100CNN.h</span><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">Cifar100CNN</span> &#123;<br><span class="hljs-keyword">private</span>:<br>    std::string onnx_path;<br>    std::string engine_path;<br><br>    TRTPtr&lt;nvinfer1::ICudaEngine&gt; engine;<br>    TRTPtr&lt;TRTInference&gt; inference;<br><br>    <span class="hljs-type">float</span>* input_buffer;<br>    <span class="hljs-type">float</span>* output_buffer;<br><br>    <span class="hljs-type">void</span>* gpu_input;<br>    <span class="hljs-type">void</span>* gpu_output;<br><br>    <span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">set_tensor_addresses</span><span class="hljs-params">()</span></span>;<br><br>    <span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">preprocess</span><span class="hljs-params">(std::vector&lt;cv::Mat&gt;::const_iterator input,</span></span><br><span class="hljs-params"><span class="hljs-function">                    <span class="hljs-type">size_t</span> batch_size)</span></span>;<br>    <span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">postprocess</span><span class="hljs-params">(std::vector&lt;<span class="hljs-type">int</span>&gt;::iterator output, <span class="hljs-type">size_t</span> batch_size)</span></span>;<br>    <span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">infer</span><span class="hljs-params">(<span class="hljs-type">size_t</span> batch_size)</span></span>;<br>    <span class="hljs-function"><span class="hljs-keyword">auto</span> <span class="hljs-title">InputData</span><span class="hljs-params">(<span class="hljs-type">size_t</span> batch_size)</span></span>;<br>    <span class="hljs-function"><span class="hljs-keyword">auto</span> <span class="hljs-title">OutputData</span><span class="hljs-params">(<span class="hljs-type">size_t</span> batch_size)</span></span>;<br><br><span class="hljs-keyword">public</span>:<br>    <span class="hljs-built_in">Cifar100CNN</span>(std::string onnx_path, std::string engine_path,<br>                TRTLogger&amp; logger, <span class="hljs-type">bool</span> always_rebuild = <span class="hljs-literal">false</span>);<br>    ~<span class="hljs-built_in">Cifar100CNN</span>() <span class="hljs-keyword">noexcept</span>;<br>    <span class="hljs-function">std::vector&lt;<span class="hljs-type">int</span>&gt; <span class="hljs-title">infer</span><span class="hljs-params">(<span class="hljs-type">const</span> std::vector&lt;cv::Mat&gt;&amp; input,</span></span><br><span class="hljs-params"><span class="hljs-function">                           <span class="hljs-type">size_t</span> batch_size = <span class="hljs-number">0</span>)</span></span>;<br>&#125;;<br><br><span class="hljs-comment">// Cifar100CNN.cpp</span><br><span class="hljs-keyword">using</span> InputImg = TensorSpec&lt;<span class="hljs-number">32</span>, <span class="hljs-number">32</span>, <span class="hljs-number">3</span>&gt;;<br><span class="hljs-keyword">using</span> OutputRes = TensorSpec&lt;<span class="hljs-number">100</span>&gt;;<br><span class="hljs-keyword">using</span> Mean = FloatArraySpec&lt;<span class="hljs-number">0.5071f</span>, <span class="hljs-number">0.4865f</span>, <span class="hljs-number">0.4409f</span>&gt;;<br><span class="hljs-keyword">using</span> Std = FloatArraySpec&lt;<span class="hljs-number">0.2673f</span>, <span class="hljs-number">0.2564f</span>, <span class="hljs-number">0.2761f</span>&gt;;<br><span class="hljs-keyword">using</span> ChannelMap = ChannelMapSpec&lt;<span class="hljs-number">2</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>&gt;;<br><span class="hljs-keyword">using</span> Cifar100Processor = ConvertHWC2CHW&lt;InputImg, Mean, Std, ChannelMap&gt;;<br><br><span class="hljs-keyword">using</span> BatchSize = TensorSpec&lt;<span class="hljs-number">1</span>, <span class="hljs-number">64</span>, <span class="hljs-number">256</span>&gt;;<br><span class="hljs-keyword">using</span> Input = TensorSpec&lt;BatchSize::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">2</span>], <span class="hljs-number">3</span>, <span class="hljs-number">32</span>, <span class="hljs-number">32</span>&gt;;<br><span class="hljs-keyword">using</span> Output = TensorSpec&lt;BatchSize::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">2</span>], <span class="hljs-number">100</span>&gt;;<br><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">Cifar100CNN::set_tensor_addresses</span><span class="hljs-params">()</span> </span>&#123;<br>    inference-&gt;<span class="hljs-built_in">set_tensor_address</span>(<span class="hljs-string">&quot;input&quot;</span>, gpu_input);<br>    inference-&gt;<span class="hljs-built_in">set_tensor_address</span>(<span class="hljs-string">&quot;output&quot;</span>, gpu_output);<br>&#125;<br><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">Cifar100CNN::preprocess</span><span class="hljs-params">(std::vector&lt;cv::Mat&gt;::const_iterator input,</span></span><br><span class="hljs-params"><span class="hljs-function">                             <span class="hljs-type">size_t</span> batch_size)</span> </span>&#123;<br>    <span class="hljs-type">size_t</span> img_size = InputImg::<span class="hljs-built_in">total_size</span>();<br><br><span class="hljs-meta">#<span class="hljs-keyword">pragma</span> omp parallel for</span><br>    <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> i = <span class="hljs-number">0</span>; i &lt; batch_size; ++i) &#123;<br>        Cifar100Processor::<span class="hljs-built_in">process</span>(*(input + i), input_buffer + i * img_size);<br>    &#125;<br>&#125;<br><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">Cifar100CNN::postprocess</span><span class="hljs-params">(std::vector&lt;<span class="hljs-type">int</span>&gt;::iterator output,</span></span><br><span class="hljs-params"><span class="hljs-function">                              <span class="hljs-type">size_t</span> batch_size)</span> </span>&#123;<br>    <span class="hljs-type">size_t</span> res_size = OutputRes::<span class="hljs-built_in">total_size</span>();<br><br><span class="hljs-meta">#<span class="hljs-keyword">pragma</span> omp parallel for</span><br>    <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> i = <span class="hljs-number">0</span>; i &lt; batch_size; ++i) &#123;<br>        <span class="hljs-type">float</span>* output_buffer_idx = output_buffer + i * res_size;<br>        <span class="hljs-type">int</span> class_id = std::<span class="hljs-built_in">distance</span>(<br>            output_buffer_idx,<br>            std::<span class="hljs-built_in">max_element</span>(output_buffer_idx, output_buffer_idx + res_size));<br>        *(output + i) = class_id;<br>    &#125;<br>&#125;<br><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">Cifar100CNN::infer</span><span class="hljs-params">(<span class="hljs-type">size_t</span> batch_size)</span> </span>&#123;<br>    inference-&gt;<span class="hljs-built_in">infer</span>([batch_size](nvinfer1::IExecutionContext* context) &#123;<br>        context-&gt;<span class="hljs-built_in">setInputShape</span>(<br>            <span class="hljs-string">&quot;input&quot;</span>, nvinfer1::Dims4 &#123;(<span class="hljs-type">int64_t</span>) batch_size, <span class="hljs-number">3</span>, <span class="hljs-number">32</span>, <span class="hljs-number">32</span>&#125;);<br>    &#125;);<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">auto</span> <span class="hljs-title">Cifar100CNN::InputData</span><span class="hljs-params">(<span class="hljs-type">size_t</span> batch_size)</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">cudaMemcpyAsync</span>(gpu_input, input_buffer,<br>                           <span class="hljs-built_in">sizeof</span>(<span class="hljs-type">float</span>) * InputImg::<span class="hljs-built_in">total_size</span>() * batch_size,<br>                           cudaMemcpyHostToDevice, inference-&gt;<span class="hljs-built_in">get_stream</span>());<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">auto</span> <span class="hljs-title">Cifar100CNN::OutputData</span><span class="hljs-params">(<span class="hljs-type">size_t</span> batch_size)</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">cudaMemcpyAsync</span>(output_buffer, gpu_output,<br>                           <span class="hljs-built_in">sizeof</span>(<span class="hljs-type">float</span>) * OutputRes::<span class="hljs-built_in">total_size</span>() * batch_size,<br>                           cudaMemcpyDeviceToHost, inference-&gt;<span class="hljs-built_in">get_stream</span>());<br>&#125;<br><br>Cifar100CNN::<span class="hljs-built_in">Cifar100CNN</span>(std::string onnx_path, std::string engine_path,<br>                         TRTLogger&amp; logger, <span class="hljs-type">bool</span> always_rebuild)<br>    : <span class="hljs-built_in">onnx_path</span>(onnx_path), <span class="hljs-built_in">engine_path</span>(engine_path) &#123;<br>    <span class="hljs-keyword">auto</span> builder = <span class="hljs-built_in">TRTModelBuilder</span>(logger);<br>    <span class="hljs-keyword">if</span> (always_rebuild || !(engine = builder.<span class="hljs-built_in">loadFromPlan</span>(engine_path))) &#123;<br>        engine = builder.<span class="hljs-built_in">buildFromOnnx</span>(<br>            onnx_path, engine_path,<br>            [](nvinfer1::IBuilderConfig* config,<br>               nvinfer1::INetworkDefinition* network,<br>               nvinfer1::IBuilder* builder) &#123;<br>                <span class="hljs-keyword">auto</span> profile = builder-&gt;<span class="hljs-built_in">createOptimizationProfile</span>();<br>                <span class="hljs-type">const</span> <span class="hljs-type">char</span>* inputName = network-&gt;<span class="hljs-built_in">getInput</span>(<span class="hljs-number">0</span>)-&gt;<span class="hljs-built_in">getName</span>();<br>                <span class="hljs-keyword">auto</span> batch_sizes = BatchSize::<span class="hljs-built_in">dims</span>();<br>                <span class="hljs-comment">// [Min, Opt, Max]</span><br>                profile-&gt;<span class="hljs-built_in">setDimensions</span>(<br>                    inputName, nvinfer1::OptProfileSelector::kMIN,<br>                    nvinfer1::Dims4 &#123;(<span class="hljs-type">int64_t</span>) batch_sizes[<span class="hljs-number">0</span>], <span class="hljs-number">3</span>, <span class="hljs-number">32</span>, <span class="hljs-number">32</span>&#125;);<br>                profile-&gt;<span class="hljs-built_in">setDimensions</span>(<br>                    inputName, nvinfer1::OptProfileSelector::kOPT,<br>                    nvinfer1::Dims4 &#123;(<span class="hljs-type">int64_t</span>) batch_sizes[<span class="hljs-number">1</span>], <span class="hljs-number">3</span>, <span class="hljs-number">32</span>, <span class="hljs-number">32</span>&#125;);<br>                profile-&gt;<span class="hljs-built_in">setDimensions</span>(<br>                    inputName, nvinfer1::OptProfileSelector::kMAX,<br>                    nvinfer1::Dims4 &#123;(<span class="hljs-type">int64_t</span>) batch_sizes[<span class="hljs-number">2</span>], <span class="hljs-number">3</span>, <span class="hljs-number">32</span>, <span class="hljs-number">32</span>&#125;);<br>                config-&gt;<span class="hljs-built_in">addOptimizationProfile</span>(profile);<br><br>                <span class="hljs-comment">// 2. 精度设置：虽然 kFP16 弃用，但在 10.0 中作为 BuilderFlag</span><br>                <span class="hljs-comment">// 依然是生效的（会有警告）</span><br>                <span class="hljs-keyword">if</span> (builder-&gt;<span class="hljs-built_in">platformHasFastFp16</span>()) &#123;<br>                    config-&gt;<span class="hljs-built_in">setFlag</span>(nvinfer1::BuilderFlag::kFP16);<br>                &#125;<br>            &#125;);<br>    &#125;<br>    inference = <span class="hljs-built_in">TRTPtr</span>&lt;TRTInference&gt;(<span class="hljs-keyword">new</span> <span class="hljs-built_in">TRTInference</span>(*engine));<br><br>    <span class="hljs-built_in">cudaHostAlloc</span>((<span class="hljs-type">void</span>**) &amp;input_buffer, <span class="hljs-built_in">sizeof</span>(<span class="hljs-type">float</span>) * Input::<span class="hljs-built_in">total_size</span>(),<br>                  cudaHostAllocDefault);<br>    <span class="hljs-built_in">cudaHostAlloc</span>((<span class="hljs-type">void</span>**) &amp;output_buffer, <span class="hljs-built_in">sizeof</span>(<span class="hljs-type">float</span>) * Output::<span class="hljs-built_in">total_size</span>(),<br>                  cudaHostAllocDefault);<br>    <span class="hljs-built_in">cudaMalloc</span>(&amp;gpu_input, <span class="hljs-built_in">sizeof</span>(<span class="hljs-type">float</span>) * Input::<span class="hljs-built_in">total_size</span>());<br>    <span class="hljs-built_in">cudaMalloc</span>(&amp;gpu_output, <span class="hljs-built_in">sizeof</span>(<span class="hljs-type">float</span>) * Output::<span class="hljs-built_in">total_size</span>());<br><br>    <span class="hljs-built_in">set_tensor_addresses</span>();<br>&#125;<br><br>Cifar100CNN::~<span class="hljs-built_in">Cifar100CNN</span>() <span class="hljs-keyword">noexcept</span> &#123;<br>    <span class="hljs-built_in">cudaFree</span>(gpu_input);<br>    <span class="hljs-built_in">cudaFree</span>(gpu_output);<br>    <span class="hljs-built_in">cudaFreeHost</span>(input_buffer);<br>    <span class="hljs-built_in">cudaFreeHost</span>(output_buffer);<br>&#125;<br><br><span class="hljs-function">std::vector&lt;<span class="hljs-type">int</span>&gt; <span class="hljs-title">Cifar100CNN::infer</span><span class="hljs-params">(<span class="hljs-type">const</span> std::vector&lt;cv::Mat&gt;&amp; input,</span></span><br><span class="hljs-params"><span class="hljs-function">                                    <span class="hljs-type">size_t</span> batch_size)</span> </span>&#123;<br>    <span class="hljs-type">size_t</span> input_size = input.<span class="hljs-built_in">size</span>();<br>    <span class="hljs-function">std::vector&lt;<span class="hljs-type">int</span>&gt; <span class="hljs-title">res</span><span class="hljs-params">(input_size)</span></span>;<br><br>    <span class="hljs-keyword">auto</span> batch_sizes = BatchSize::<span class="hljs-built_in">dims</span>();<br>    <span class="hljs-keyword">if</span> (batch_size == <span class="hljs-number">0</span>) &#123;<br>        <span class="hljs-comment">// 使用默认的最优 batch size</span><br>        batch_size = batch_sizes[<span class="hljs-number">1</span>];<br>    &#125; <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (batch_size &gt; batch_sizes[<span class="hljs-number">2</span>]) &#123;<br>        <span class="hljs-comment">// 若超过则使用最大 batch size</span><br>        batch_size = batch_sizes[<span class="hljs-number">2</span>];<br>    &#125;<br><br>    <span class="hljs-type">size_t</span> batches =<br>        input_size / batch_size + (((input_size % batch_size) == <span class="hljs-number">0</span>) ? <span class="hljs-number">0</span> : <span class="hljs-number">1</span>);<br><br>    <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> i = <span class="hljs-number">0</span>; i &lt; batches; ++i) &#123;<br>        <span class="hljs-type">size_t</span> cur_batch_size =<br>            std::<span class="hljs-built_in">min</span>(batch_size, input_size - i * batch_size);<br>        <span class="hljs-built_in">preprocess</span>(input.<span class="hljs-built_in">cbegin</span>() + i * batch_size, cur_batch_size);<br>        <span class="hljs-built_in">InputData</span>(cur_batch_size);<br>        <span class="hljs-built_in">infer</span>(cur_batch_size);<br>        <span class="hljs-built_in">OutputData</span>(cur_batch_size);<br>        <span class="hljs-built_in">cudaStreamSynchronize</span>(inference-&gt;<span class="hljs-built_in">get_stream</span>());<br>        <span class="hljs-built_in">postprocess</span>(res.<span class="hljs-built_in">begin</span>() + i * batch_size, cur_batch_size);<br><br>        std::cout &lt;&lt; std::format(<span class="hljs-string">&quot;batch &#123;&#125; with size &#123;&#125; finished\n&quot;</span>, i,<br>                                 cur_batch_size);<br>    &#125;<br><br>    <span class="hljs-keyword">return</span> res;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="最终测试"><a href="#最终测试" class="headerlink" title="最终测试"></a>最终测试</h3><p>使用之前 Pytorch 导出的数据库和元数据进行测试，用 C++读取 JSON 元数据并用 OpenCV 读取图片，通过上面写的 Cifar100 类进行推理，batch size 设置为 128，最终成功率为 69.32%，与训练时基本一致，说明以上各流程没有明显问题。</p>]]>
    </content>
    <id>https://chillywall.github.io/ChillyBlog/2026/04/09/TensorRT%E9%83%A8%E7%BD%B2Cifar100CNN%E6%A8%A1%E5%9E%8B/</id>
    <link href="https://chillywall.github.io/ChillyBlog/2026/04/09/TensorRT%E9%83%A8%E7%BD%B2Cifar100CNN%E6%A8%A1%E5%9E%8B/"/>
    <published>2026-04-09T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>本文介绍如何使用TensorRT部署一个简单的用于Cifar100分类任务的CNN模型。</p>]]>
    </summary>
    <title>TensorRT部署Cifar100CNN模型</title>
    <updated>2026-04-10T05:07:21.884Z</updated>
  </entry>
  <entry>
    <author>
      <name>郭金锋</name>
    </author>
    <category term="AI" scheme="https://chillywall.github.io/ChillyBlog/categories/AI/"/>
    <category term="Deployment" scheme="https://chillywall.github.io/ChillyBlog/categories/AI/Deployment/"/>
    <category term="TensorRT" scheme="https://chillywall.github.io/ChillyBlog/tags/TensorRT/"/>
    <category term="YOLO" scheme="https://chillywall.github.io/ChillyBlog/tags/YOLO/"/>
    <category term="OpenCV" scheme="https://chillywall.github.io/ChillyBlog/tags/OpenCV/"/>
    <content>
      <![CDATA[<p>本文介绍我使用TensorRT部署Yolo模型并实现读取摄像头实时目标检测的过程。</p><span id="more"></span><p>本文源码仓库在 <a href="https://github.com/ChillyWall/TensorRT-Deployment-Practices.git">这里</a>。</p><h2 id="基于-TensorRT-YOLO-部署"><a href="#基于-TensorRT-YOLO-部署" class="headerlink" title="基于 TensorRT-YOLO 部署"></a>基于 TensorRT-YOLO 部署</h2><h3 id="安装-TensorRT-YOLO"><a href="#安装-TensorRT-YOLO" class="headerlink" title="安装 TensorRT-YOLO"></a>安装 TensorRT-YOLO</h3><p>使用开源框架 <a href="https://github.com/laugh12321/TensorRT-YOLO">TensorRT-YOLO</a> 进行模型的部署。将源码下载下来，正确配置 <code>TensorRT</code> 的 安装路径之后可以直接编译成功。</p><p>不过这个项目的的导出写得不是很好，明明安装目标时有 <code>EXPORT</code> 却没有安装，最后是自己手动设置的变量。我把缺失的安装部分给它加上了，之后可以正常 <code>find_package</code> 然后链接一下库就完成所有配置了。</p><h3 id="准备-YOLO-模型"><a href="#准备-YOLO-模型" class="headerlink" title="准备 YOLO 模型"></a>准备 YOLO 模型</h3><p>使用 Python 安装 <code>ultralytics</code> 之后直接导出 <code>yolov8n</code> 模型到 ONNX 格式。之后使用上面 <code>TensorRT-YOLO</code> 项目配套的 <a href="https://github.com/laugh12321/trtyolo-export">trtyolo-export</a> 工具可以将 ONNX 直接编译成 engine 格式。</p><p>同时会在Yolo模型的后面增加一个Efficient NMS插件的处理层，在GPU端进行低置信框的过滤和NMS处理。</p><h3 id="使用-OpenCV-读取摄像头"><a href="#使用-OpenCV-读取摄像头" class="headerlink" title="使用 OpenCV 读取摄像头"></a>使用 OpenCV 读取摄像头</h3><p>这里使用的摄像头之前已经标定过了，根据参数从摄像头读取图像数据后进行畸变校正后传出。</p><p>参数保存在 yml 文件中，通过 <code>cv::FileStorage</code> 读取并解析。</p><p>读取并校正代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function">cv::Mat <span class="hljs-title">YoloCamera::read_frame</span><span class="hljs-params">()</span> </span>&#123;<br>    cv::Mat frame;<br>    cap &gt;&gt; frame;<br><br>    <span class="hljs-keyword">if</span> (frame.<span class="hljs-built_in">empty</span>()) &#123;<br>        <span class="hljs-keyword">throw</span> <span class="hljs-built_in">FrameCaptureException</span>();<br>    &#125;<br><br>    cv::Mat undist_frame;<br>    cv::<span class="hljs-built_in">undistort</span>(frame, undist_frame, calib_res.camera_matrix,<br>                  calib_res.dist_coeffs);<br><br>    <span class="hljs-keyword">return</span> undist_frame;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="YOLO-实时目标监测"><a href="#YOLO-实时目标监测" class="headerlink" title="YOLO 实时目标监测"></a>YOLO 实时目标监测</h3><p>将 <code>TensorRT-YOLO</code> 提供的推理参数选项保存在 yml 文件中，创建模型时读取文件再设置。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">YoloCamera::read_infer_option</span><span class="hljs-params">(<span class="hljs-type">const</span> std::string&amp; option_file)</span> </span>&#123;<br>    <span class="hljs-function">cv::FileStorage <span class="hljs-title">fs</span><span class="hljs-params">(option_file, cv::FileStorage::READ)</span></span>;<br>    <span class="hljs-keyword">if</span> (!fs.<span class="hljs-built_in">isOpened</span>()) &#123;<br>        <span class="hljs-keyword">throw</span> <span class="hljs-built_in">FileNotFoundException</span>(CONFIG_FILE);<br>    &#125;<br>    cv::FileNode node;<br>    <span class="hljs-keyword">if</span> ((node = fs[<span class="hljs-string">&quot;device_id&quot;</span>]).<span class="hljs-built_in">isInt</span>()) &#123;<br>        infer_option.<span class="hljs-built_in">setDeviceId</span>((<span class="hljs-type">int</span>) node);<br>    &#125;<br>    <span class="hljs-keyword">if</span> ((node = fs[<span class="hljs-string">&quot;cuda_memory&quot;</span>]).<span class="hljs-built_in">isInt</span>()) &#123;<br>        <span class="hljs-keyword">if</span> ((<span class="hljs-type">int</span>) node)<br>            infer_option.<span class="hljs-built_in">enableCudaMem</span>();<br>    &#125;<br>    <span class="hljs-keyword">if</span> ((node = fs[<span class="hljs-string">&quot;managed_memory&quot;</span>]).<span class="hljs-built_in">isInt</span>()) &#123;<br>        <span class="hljs-keyword">if</span> ((<span class="hljs-type">int</span>) node)<br>            infer_option.<span class="hljs-built_in">enableManagedMemory</span>();<br>    &#125;<br>    <span class="hljs-keyword">if</span> ((node = fs[<span class="hljs-string">&quot;enable_swap_rb&quot;</span>]).<span class="hljs-built_in">isInt</span>()) &#123;<br>        <span class="hljs-keyword">if</span> ((<span class="hljs-type">int</span>) node)<br>            infer_option.<span class="hljs-built_in">enableSwapRB</span>();<br>    &#125;<br>    <span class="hljs-keyword">if</span> ((node = fs[<span class="hljs-string">&quot;enable_performance_report&quot;</span>]).<span class="hljs-built_in">isInt</span>()) &#123;<br>        <span class="hljs-keyword">if</span> ((<span class="hljs-type">int</span>) node)<br>            infer_option.<span class="hljs-built_in">enablePerformanceReport</span>();<br>    &#125;<br>    <span class="hljs-keyword">if</span> ((node = fs[<span class="hljs-string">&quot;input_dimensions&quot;</span>]).<span class="hljs-built_in">isSeq</span>()) &#123;<br>        std::vector&lt;<span class="hljs-type">int</span>&gt; dims;<br>        node &gt;&gt; dims;<br>        <span class="hljs-keyword">if</span> (dims.<span class="hljs-built_in">size</span>() == <span class="hljs-number">2</span>) &#123;<br>            infer_option.<span class="hljs-built_in">setInputDimensions</span>(dims[<span class="hljs-number">0</span>], dims[<span class="hljs-number">1</span>]);<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>预训练的 yolov8 模型使用的是 coco 数据集，搜索其类型名称保存在 yml 文件中，用于后续可视化时的标注。</p><p>将 OpenCV 校正过的图像用 TensorRT-YOLO 提供的图片类封装一下，直接进行推理。</p><p>推理完成后根据结果使用 OpenCV 绘制简单的框并加上 label，再显示出来。</p><p>可视化代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function">cv::Mat <span class="hljs-title">YoloCamera::visualize</span><span class="hljs-params">(<span class="hljs-type">const</span> cv::Mat&amp; frame,</span></span><br><span class="hljs-params"><span class="hljs-function">                              <span class="hljs-type">const</span> trtyolo::DetectRes&amp; res)</span> <span class="hljs-type">const</span> </span>&#123;<br>    cv::Mat image = frame.<span class="hljs-built_in">clone</span>();<br>    <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> i = <span class="hljs-number">0</span>; i &lt; res.num; ++i) &#123;<br>        <span class="hljs-type">const</span> <span class="hljs-keyword">auto</span>&amp; box = res.boxes[i];<br>        <span class="hljs-type">int</span> cls = res.classes[i];<br>        <span class="hljs-type">float</span> score = res.scores[i];<br>        <span class="hljs-type">const</span> <span class="hljs-keyword">auto</span>&amp; label = labels[cls];<br>        std::string label_text = label + <span class="hljs-string">&quot; &quot;</span> + cv::format(<span class="hljs-string">&quot;%.3f&quot;</span>, score);<br><br>        <span class="hljs-comment">// 绘制矩形和标签</span><br>        <span class="hljs-type">int</span> base_line;<br>        cv::Size label_size = cv::<span class="hljs-built_in">getTextSize</span>(<br>            label_text, cv::FONT_HERSHEY_SIMPLEX, <span class="hljs-number">0.6</span>, <span class="hljs-number">1</span>, &amp;base_line);<br>        cv::<span class="hljs-built_in">rectangle</span>(image, cv::<span class="hljs-built_in">Point</span>(box.left, box.top),<br>                      cv::<span class="hljs-built_in">Point</span>(box.right, box.bottom),<br>                      cv::<span class="hljs-built_in">Scalar</span>(<span class="hljs-number">251</span>, <span class="hljs-number">81</span>, <span class="hljs-number">163</span>), <span class="hljs-number">2</span>, cv::LINE_AA);<br>        cv::<span class="hljs-built_in">rectangle</span>(image, cv::<span class="hljs-built_in">Point</span>(box.left, box.top - label_size.height),<br>                      cv::<span class="hljs-built_in">Point</span>(box.left + label_size.width, box.top),<br>                      cv::<span class="hljs-built_in">Scalar</span>(<span class="hljs-number">125</span>, <span class="hljs-number">40</span>, <span class="hljs-number">81</span>), <span class="hljs-number">-1</span>);<br>        cv::<span class="hljs-built_in">putText</span>(image, label_text, cv::<span class="hljs-built_in">Point</span>(box.left, box.top),<br>                    cv::FONT_HERSHEY_SIMPLEX, <span class="hljs-number">0.6</span>, cv::<span class="hljs-built_in">Scalar</span>(<span class="hljs-number">253</span>, <span class="hljs-number">168</span>, <span class="hljs-number">208</span>),<br>                    <span class="hljs-number">1</span>);<br>    &#125;<br>    <span class="hljs-keyword">return</span> image;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="手写-C-代码进行部署"><a href="#手写-C-代码进行部署" class="headerlink" title="手写 C++代码进行部署"></a>手写 C++代码进行部署</h2><p>基于之前写的 Cifar100CNN 的类修改，去掉动态批大小之后只需要关注预处理和后处理。</p><p>介于要适配两种模型，一种支持 TensorRT 的官方插件 Efficient NMS，所以要根据模型类型 来修改分配显存和输入输出绑定。加入了成员变量 <code>enable_efficient_nms</code> 来标记模型类型。</p><p>和使用 TensorRT-YOLO 的版本一样，也加入了读取 labels 的功能。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">YoloV8n</span> &#123;<br><span class="hljs-keyword">private</span>:<br>    std::string onnx_path;<br>    std::string engine_path;<br><br>    TRTPtr&lt;nvinfer1::ICudaEngine&gt; engine;<br>    TRTPtr&lt;TRTInference&gt; inference;<br><br>    <span class="hljs-comment">// 模型是否启用了EfficientNMS插件</span><br>    <span class="hljs-type">bool</span> enable_efficient_nms;<br><br>    <span class="hljs-type">float</span>* input_buffer;<br>    <span class="hljs-type">float</span>* output_buffer;<br><br>    <span class="hljs-type">size_t</span> input_size;<br>    <span class="hljs-type">size_t</span> output_size;<br><br>    <span class="hljs-type">void</span>* gpu_input;<br>    <span class="hljs-type">void</span>* gpu_output;<br><br>    std::vector&lt;std::string&gt; labels;<br>    <span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">read_labels</span><span class="hljs-params">(<span class="hljs-type">const</span> std::string&amp; file_path)</span></span>;<br><br>    <span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">set_tensor_addresses</span><span class="hljs-params">()</span></span>;<br><br>    <span class="hljs-function">std::vector&lt;YoloDetectResult&gt; <span class="hljs-title">decode_output</span><span class="hljs-params">()</span></span>;<br><br>    <span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">apply_nms</span><span class="hljs-params">(std::vector&lt;YoloDetectResult&gt;&amp; results,</span></span><br><span class="hljs-params"><span class="hljs-function">                   <span class="hljs-type">float</span> iou_threshold = <span class="hljs-number">0.5f</span>)</span></span>;<br><br>    <span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">apply_deletterbox</span><span class="hljs-params">(std::vector&lt;YoloDetectResult&gt;&amp; results)</span></span>;<br><br>    <span class="hljs-function">std::vector&lt;YoloDetectResult&gt; <span class="hljs-title">decode_output_nms</span><span class="hljs-params">()</span></span>;<br><br>    <span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">preprocess</span><span class="hljs-params">(<span class="hljs-type">const</span> cv::Mat&amp; input)</span></span>;<br>    <span class="hljs-function">std::vector&lt;YoloDetectResult&gt; <span class="hljs-title">postprocess</span><span class="hljs-params">()</span></span>;<br>    <span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">infer</span><span class="hljs-params">()</span></span>;<br>    <span class="hljs-function"><span class="hljs-keyword">auto</span> <span class="hljs-title">InputData</span><span class="hljs-params">()</span></span>;<br>    <span class="hljs-function"><span class="hljs-keyword">auto</span> <span class="hljs-title">OutputData</span><span class="hljs-params">()</span></span>;<br><br><span class="hljs-keyword">public</span>:<br>    <span class="hljs-built_in">YoloV8n</span>(std::string onnx_path, std::string engine_path, TRTLogger&amp; logger,<br>            <span class="hljs-type">bool</span> enable_efficient_nms_plugin, <span class="hljs-type">bool</span> always_rebuild = <span class="hljs-literal">false</span>);<br>    ~<span class="hljs-built_in">YoloV8n</span>() <span class="hljs-keyword">noexcept</span>;<br>    <span class="hljs-function">std::vector&lt;YoloDetectResult&gt; <span class="hljs-title">infer</span><span class="hljs-params">(<span class="hljs-type">const</span> cv::Mat&amp; input)</span></span>;<br>    <span class="hljs-function">cv::Mat <span class="hljs-title">visualize</span><span class="hljs-params">(<span class="hljs-type">const</span> cv::Mat&amp; input,</span></span><br><span class="hljs-params"><span class="hljs-function">                      <span class="hljs-type">const</span> std::vector&lt;YoloDetectResult&gt;&amp; results)</span></span>;<br>&#125;;<br></code></pre></td></tr></table></figure><h3 id="Yolo-预处理"><a href="#Yolo-预处理" class="headerlink" title="Yolo 预处理"></a>Yolo 预处理</h3><p>目前的摄像头分辨率是 1280x720，而 YoloV8 的最佳分辨率一般为 640x640，需要进行变换， 使用 <code>letterbox</code> 方法进行。即先进行缩放，比如 1280x720 缩放为 640x360，空白部分补 上黑边。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">template</span> &lt;TensorSpecType InputSpec, TensorSpecType OutputSpec&gt;<br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">LetterBox</span> &#123;<br>    <span class="hljs-function"><span class="hljs-type">static</span> cv::Mat <span class="hljs-title">process</span><span class="hljs-params">(<span class="hljs-type">const</span> cv::Mat&amp; input)</span> </span>&#123;<br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> input_width = InputSpec::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">0</span>];<br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> input_height = InputSpec::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">1</span>];<br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> output_width = OutputSpec::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">0</span>];<br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> output_height = OutputSpec::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">1</span>];<br><br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">float</span> scale =<br>            std::<span class="hljs-built_in">min</span>(<span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">float</span>&gt;(output_width) / input_width,<br>                     <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">float</span>&gt;(output_height) / input_height);<br><br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> resized_width = <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">int</span>&gt;(input_width * scale);<br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> resized_height = <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">int</span>&gt;(input_height * scale);<br><br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> x_offset = (output_width - resized_width) / <span class="hljs-number">2</span>;<br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> y_offset = (output_height - resized_height) / <span class="hljs-number">2</span>;<br><br>        cv::Mat resized;<br>        cv::<span class="hljs-built_in">resize</span>(input, resized, cv::<span class="hljs-built_in">Size</span>(resized_width, resized_height));<br><br>        cv::Mat output =<br>            cv::Mat::<span class="hljs-built_in">zeros</span>(output_height, output_width, input.<span class="hljs-built_in">type</span>());<br><br>        <span class="hljs-function">cv::Rect <span class="hljs-title">roi</span><span class="hljs-params">(x_offset, y_offset, resized_width, resized_height)</span></span>;<br>        resized.<span class="hljs-built_in">copyTo</span>(<span class="hljs-built_in">output</span>(roi));<br><br>        <span class="hljs-keyword">return</span> output;<br>    &#125;<br>&#125;;<br></code></pre></td></tr></table></figure><h3 id="Yolo-后处理"><a href="#Yolo-后处理" class="headerlink" title="Yolo 后处理"></a>Yolo 后处理</h3><p>Yolo 的后处理相对要复杂一些。</p><p>原始的 Yolo 模型的输出结果为 <code>(1, 84, 8400)</code>，及总共 8400 个框，每个框 84 个数据， 其中 0～1 为中心点坐标，2～3 为宽和高，4～83 为各个类别的置信率。</p><p>Yolo 的后处理主要包括三个步骤，首先是从 80 个类型置信率中得到最大的值作为检测结 果，其置信率作为该框的置信率，并过滤到低置信率的框，一般阈值为 0.25；之后是过滤 掉重复框，因为 Yolo 检测中一个物体可能有很多个重复框，需要使用 NMS 算法进行去重； 之后还需要将框转换回原图片。</p><h4 id="解析推理结果并过滤"><a href="#解析推理结果并过滤" class="headerlink" title="解析推理结果并过滤"></a>解析推理结果并过滤</h4><p>解析的部分比较简单，唯一需要注意的就是结果的形状为 (84, 8400) 而不是 (8400, 84)， 也就是说在内存中是先 8400 个框的第一个元素，然后 8400 个框的第二个元素这样排列的。也 就是说一个框的 84 个元素之间都隔着 8399 个元素。</p><p>这种跳跃式读取实际上是比较慢的，因为局部性较差，CPU 缓存不命中。一种方法是转置， 但是转置其实也很慢，因为需要新分配一块内存再复制一遍。之后还需要再进行过滤，所以 虽然写法上看似简洁但性能较差。</p><p>所以最终还是选择了跳跃式读取的同时进行过滤，仅将合格的部分记录下来统一读取框的位 置，再向外输出。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function">std::vector&lt;YoloDetectResult&gt; <span class="hljs-title">YoloV8n::decode_output</span><span class="hljs-params">()</span> </span>&#123;<br>    std::vector&lt;YoloDetectResult&gt; results;<br>    <span class="hljs-comment">// 预留50个结果的空间，避免频繁扩容</span><br>    results.<span class="hljs-built_in">reserve</span>(<span class="hljs-number">50</span>);<br>    <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> COLS = OutputRes::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">1</span>];<br>    <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> ROWS = OutputRes::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">0</span>];<br><br>    <span class="hljs-comment">// 过滤低置信度的检测结果，并提取边界框和类别信息</span><br>    <span class="hljs-comment">// 采用跳跃式访问，而非先转置输出矩阵，避免不必要的内存复制</span><br>    <span class="hljs-comment">// 使用OpenMP进行并行处理，提升性能</span><br><span class="hljs-meta">#<span class="hljs-keyword">pragma</span> omp parallel</span><br>    &#123;<br>        <span class="hljs-comment">// 每个线程创建自己的局部 vector</span><br>        std::vector&lt;YoloDetectResult&gt; local_results;<br>        local_results.<span class="hljs-built_in">reserve</span>(<span class="hljs-number">10</span>);  <span class="hljs-comment">// 预留少量空间</span><br><br><span class="hljs-meta">#<span class="hljs-keyword">pragma</span> omp for nowait  <span class="hljs-comment">// nowait 减少同步开销</span></span><br>        <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> i = <span class="hljs-number">0</span>; i &lt; COLS; ++i) &#123;<br>            <span class="hljs-type">float</span> max_score = <span class="hljs-number">0</span>;<br>            <span class="hljs-type">int</span> class_id = <span class="hljs-number">-1</span>;<br>            <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> j = <span class="hljs-number">4</span>; j &lt; ROWS; ++j) &#123;<br>                <span class="hljs-type">float</span> s = output_buffer[j * COLS + i];<br>                <span class="hljs-keyword">if</span> (s &gt; max_score) &#123;<br>                    max_score = s;<br>                    class_id = j - <span class="hljs-number">4</span>;<br>                &#125;<br>            &#125;<br>            <span class="hljs-keyword">if</span> (max_score &gt; <span class="hljs-number">0.25f</span>) &#123;<br>                <span class="hljs-type">float</span> x_center = output_buffer[<span class="hljs-number">0</span> * COLS + i];<br>                <span class="hljs-type">float</span> y_center = output_buffer[<span class="hljs-number">1</span> * COLS + i];<br>                <span class="hljs-type">float</span> width = output_buffer[<span class="hljs-number">2</span> * COLS + i];<br>                <span class="hljs-type">float</span> height = output_buffer[<span class="hljs-number">3</span> * COLS + i];<br>                <span class="hljs-function">cv::Rect <span class="hljs-title">box</span><span class="hljs-params">(x_center - width / <span class="hljs-number">2</span>, y_center - height / <span class="hljs-number">2</span>, width,</span></span><br><span class="hljs-params"><span class="hljs-function">                             height)</span></span>;<br>                local_results.<span class="hljs-built_in">push_back</span>(&#123;class_id, max_score, box&#125;);<br>            &#125;<br>        &#125;<br><br><span class="hljs-comment">// 关键区域：将局部结果安全地合并到主结果</span><br><span class="hljs-meta">#<span class="hljs-keyword">pragma</span> omp critical</span><br>        &#123;<br>            results.<span class="hljs-built_in">insert</span>(results.<span class="hljs-built_in">end</span>(), local_results.<span class="hljs-built_in">begin</span>(),<br>                           local_results.<span class="hljs-built_in">end</span>());<br>        &#125;<br>    &#125;<br><br>    <span class="hljs-keyword">return</span> results;<br>&#125;<br></code></pre></td></tr></table></figure><p>后来实现了批次处理，因为跳跃访问的主要问题是缓存不明中，CPU 读取连续内存的能力是 最强的。所以后面尝试进行优化，将框再进行分批处理，一次同时处理 16 个框，读取时每一 次是读取连续的 16 个元素，再统一进行比较，最后同意过滤。</p><p>但是这个写法也有问题，一次读取 16 个元素，所以加入了局部数组作为存储，又加了一层循 环，代码相对更复杂一些。最终经过测试，在开启多线程优化的前提下两个版本速度相差不 多，但是当线程数为 2 和 16 时，原版性能都显著高于优化后的版本。可能是 84*8400 这个数 据量仍然在 CPU 的预取承受范围内，所以 CPU 自动进行的优化便已经很快了，而我家的优化反 而因为多了一层循环或其他原因 CPU 更难进行优化所以更慢。</p><h4 id="NMS-算法实现"><a href="#NMS-算法实现" class="headerlink" title="NMS 算法实现"></a>NMS 算法实现</h4><p>NMS 算法本身不算复杂，这里实现为先进行排序，然后从前至后逐个检查 IoU 即重叠率， 重叠率高于一定阈值并且是相同类别，则将删除。为优化性能，使用懒删除，为方便将置信 率作为标记，被标记要删除的框则将其置信率设为负值，最后通过 erase<sub>if</sub> 统一删除。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-keyword">inline</span> <span class="hljs-type">float</span> <span class="hljs-title">calculate_iou</span><span class="hljs-params">(<span class="hljs-type">const</span> cv::Rect&amp; a, <span class="hljs-type">const</span> cv::Rect&amp; b)</span> </span>&#123;<br>    <span class="hljs-type">float</span> inter_area = (a &amp; b).<span class="hljs-built_in">area</span>();<br>    <span class="hljs-keyword">if</span> (inter_area &lt;= <span class="hljs-number">0</span>)<br>        <span class="hljs-keyword">return</span> <span class="hljs-number">0.f</span>;<br>    <span class="hljs-type">float</span> res =<br>        inter_area / <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">float</span>&gt;(a.<span class="hljs-built_in">area</span>() + b.<span class="hljs-built_in">area</span>() - inter_area);<br>    <span class="hljs-keyword">return</span> res;<br>&#125;<br><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">YoloV8n::apply_nms</span><span class="hljs-params">(std::vector&lt;YoloDetectResult&gt;&amp; candidates,</span></span><br><span class="hljs-params"><span class="hljs-function">                        <span class="hljs-type">float</span> iou_threshold)</span> </span>&#123;<br>    <span class="hljs-keyword">if</span> (candidates.<span class="hljs-built_in">empty</span>())<br>        <span class="hljs-keyword">return</span>;<br><br>    <span class="hljs-comment">// 按置信率排序</span><br>    std::<span class="hljs-built_in">sort</span>(candidates.<span class="hljs-built_in">begin</span>(), candidates.<span class="hljs-built_in">end</span>(),<br>              [](<span class="hljs-type">const</span> YoloDetectResult&amp; a, <span class="hljs-type">const</span> YoloDetectResult&amp; b) -&gt; <span class="hljs-type">bool</span> &#123;<br>                  <span class="hljs-keyword">return</span> a.confidence &gt; b.confidence;<br>              &#125;);<br><br>    <span class="hljs-comment">// 将要被删除的框的置信率设为负值，避免额外的 bool 标记空间</span><br>    <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> i = <span class="hljs-number">0</span>; i &lt; candidates.<span class="hljs-built_in">size</span>(); ++i) &#123;<br>        <span class="hljs-keyword">if</span> (candidates[i].confidence &lt; <span class="hljs-number">0</span>)<br>            <span class="hljs-keyword">continue</span>;  <span class="hljs-comment">// 已经被标记删除</span><br><br>        <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> j = i + <span class="hljs-number">1</span>; j &lt; candidates.<span class="hljs-built_in">size</span>(); ++j) &#123;<br>            <span class="hljs-keyword">if</span> (candidates[j].confidence &lt; <span class="hljs-number">0</span>)<br>                <span class="hljs-keyword">continue</span>;<br><br>            <span class="hljs-keyword">if</span> (candidates[i].class_id == candidates[j].class_id) &#123;<br>                <span class="hljs-keyword">if</span> (<span class="hljs-built_in">calculate_iou</span>(candidates[i].box, candidates[j].box) &gt;<br>                    iou_threshold) &#123;<br>                    <span class="hljs-comment">// 利用置信率字段作为标记位，负值表示被删除</span><br>                    candidates[j].confidence = <span class="hljs-number">-1.0f</span>;<br>                &#125;<br>            &#125;<br>        &#125;<br>    &#125;<br><br>    <span class="hljs-comment">// 删除被标记的框，即置信率被设为负值的项</span><br>    std::<span class="hljs-built_in">erase_if</span>(candidates, [](<span class="hljs-type">const</span> <span class="hljs-keyword">auto</span>&amp; d) &#123; <span class="hljs-keyword">return</span> d.confidence &lt; <span class="hljs-number">0</span>; &#125;);<br>&#125;<br></code></pre></td></tr></table></figure><h4 id="启用-EfficientNMS-插件后的结果解析"><a href="#启用-EfficientNMS-插件后的结果解析" class="headerlink" title="启用 EfficientNMS 插件后的结果解析"></a>启用 EfficientNMS 插件后的结果解析</h4><p>当启用了 TensorRT 官方的 Efficient NMS 插件之后，过滤低置信框和 NMS 操作可以在 GPU 直接完成，输出结果变为四个 tensor，<code>num_dets int32(1, 1)</code>，<code>det_boxes float32(1, 100, 4)</code>，<code>det_classes int32(1, 100)</code>，<code>det_scores float32(1, 100)</code>，分别 是检测到的目标，目标的框，目标的类别和目标的置信率。</p><p>设计了一个类用于存储这四个数据，便于管理和表示，至于解析便很简单了，只需要将指向 结果缓存区的指针转为该类型，便可直接读取数据，然后直接输出即可。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">YoloDetectResultNMS</span> &#123;<br>    <span class="hljs-type">int32_t</span> num_dets;<br>    <span class="hljs-type">float</span> det_boxes[<span class="hljs-number">100</span>][<span class="hljs-number">4</span>];<br>    <span class="hljs-type">float</span> det_scores[<span class="hljs-number">100</span>];<br>    <span class="hljs-type">int32_t</span> det_classes[<span class="hljs-number">100</span>];<br>&#125;;<br><br><span class="hljs-function">std::vector&lt;YoloDetectResult&gt; <span class="hljs-title">YoloV8n::decode_output_nms</span><span class="hljs-params">()</span> </span>&#123;<br>    <span class="hljs-keyword">auto</span> output = <span class="hljs-built_in">reinterpret_cast</span>&lt;YoloDetectResultNMS*&gt;(output_buffer);<br>    <span class="hljs-built_in">static_assert</span>(<span class="hljs-built_in">sizeof</span>(YoloDetectResultNMS) == (<span class="hljs-number">1</span> + <span class="hljs-number">100</span> * <span class="hljs-number">4</span> + <span class="hljs-number">100</span> + <span class="hljs-number">100</span>) * <span class="hljs-number">4</span>);<br><br>    std::vector&lt;YoloDetectResult&gt; results;<br>    results.<span class="hljs-built_in">reserve</span>(output-&gt;num_dets);<br>    <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; output-&gt;num_dets; ++i) &#123;<br>        <span class="hljs-type">int</span> class_id = <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">int</span>&gt;(output-&gt;det_classes[i]);<br>        <span class="hljs-type">float</span> confidence = output-&gt;det_scores[i];<br>        <span class="hljs-type">float</span> x1 = output-&gt;det_boxes[i][<span class="hljs-number">0</span>];<br>        <span class="hljs-type">float</span> y1 = output-&gt;det_boxes[i][<span class="hljs-number">1</span>];<br>        <span class="hljs-type">float</span> x2 = output-&gt;det_boxes[i][<span class="hljs-number">2</span>];<br>        <span class="hljs-type">float</span> y2 = output-&gt;det_boxes[i][<span class="hljs-number">3</span>];<br>        <span class="hljs-function">cv::Rect <span class="hljs-title">box</span><span class="hljs-params">(x1, y1, x2 - x1, y2 - y1)</span></span>;<br>        results.<span class="hljs-built_in">push_back</span>(&#123;class_id, confidence, box&#125;);<br>    &#125;<br><br>    <span class="hljs-keyword">return</span> results;<br>&#125;<br></code></pre></td></tr></table></figure><h4 id="DeLetterBox-实现"><a href="#DeLetterBox-实现" class="headerlink" title="DeLetterBox 实现"></a>DeLetterBox 实现</h4><p>上面提到，由于预处理时将图片胫骨 LetterBox 算法进行了缩放，传回的结果中的坐标也是 按照缩放后的坐标系。要将框映射到原图像中，需要进行 DeLetterBox，算法很简单，只需 要简单的线性代数相关知识就很容易理解这就是一个仿射变换，当然哪怕没有也很容易理解。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">template</span> &lt;TensorSpecType InputSpec, TensorSpecType OutputSpec&gt;<br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">DeLetterBox</span> &#123;<br>    <span class="hljs-function"><span class="hljs-type">static</span> cv::Rect <span class="hljs-title">process</span><span class="hljs-params">(<span class="hljs-type">const</span> cv::Rect&amp; input)</span> </span>&#123;<br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> input_width = InputSpec::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">0</span>];<br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> input_height = InputSpec::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">1</span>];<br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> output_width = OutputSpec::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">0</span>];<br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> output_height = OutputSpec::<span class="hljs-built_in">dims</span>()[<span class="hljs-number">1</span>];<br><br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">float</span> scale =<br>            std::<span class="hljs-built_in">min</span>(<span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">float</span>&gt;(output_width) / input_width,<br>                     <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">float</span>&gt;(output_height) / input_height);<br><br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> resized_width = <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">int</span>&gt;(input_width * scale);<br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> resized_height = <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">int</span>&gt;(input_height * scale);<br><br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> x_offset = (output_width - resized_width) / <span class="hljs-number">2</span>;<br>        <span class="hljs-keyword">constexpr</span> <span class="hljs-type">int</span> y_offset = (output_height - resized_height) / <span class="hljs-number">2</span>;<br><br>        <span class="hljs-keyword">return</span> cv::<span class="hljs-built_in">Rect</span>((input.x - x_offset) / scale,<br>                        (input.y - y_offset) / scale, input.width / scale,<br>                        input.height / scale);<br>    &#125;<br>&#125;;<br></code></pre></td></tr></table></figure><h4 id="最终的后处理"><a href="#最终的后处理" class="headerlink" title="最终的后处理"></a>最终的后处理</h4><p>根据是否启用了 Efficient NMS 插件选择不同的解析函数，并进行 NMS。最后同意 DeLetterBox 后返回结果。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function">std::vector&lt;YoloDetectResult&gt; <span class="hljs-title">YoloV8n::postprocess</span><span class="hljs-params">()</span> </span>&#123;<br>    std::vector&lt;YoloDetectResult&gt; results;<br>    <span class="hljs-keyword">if</span> (enable_efficient_nms) &#123;<br>        results = <span class="hljs-built_in">decode_output_nms</span>();<br>    &#125; <span class="hljs-keyword">else</span> &#123;<br>        results = <span class="hljs-built_in">decode_output</span>();<br>        <span class="hljs-built_in">apply_nms</span>(results, <span class="hljs-number">0.45f</span>);<br>    &#125;<br>    <span class="hljs-built_in">apply_deletterbox</span>(results);<br>    <span class="hljs-keyword">return</span> results;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="绑定输入输出"><a href="#绑定输入输出" class="headerlink" title="绑定输入输出"></a>绑定输入输出</h3><p>由于显存如何分配是根据模型的结构决定的，和绑定输入输出耦合度高，因此将显存分配和 绑定放到一起。</p><p>输入没有区别都是 <code>(1, 3, 640, 640)</code> 的图片，tensor 名称为 <code>images</code>，输出要根据模型类 型进行绑定。</p><p>原始的 Yolo 输出格式为 <code>(1, 84, 8400)</code>，tensor 名称为 <code>output0</code>。</p><p>而使用了 Efficient NMS 的则变为四个 tensor 输出，上面已经介绍过。为了方便分配以 及内存拷贝，分配显存时相当于直接分配一个 <code>YoloDetectResultNMS</code> 对象，使用取地址符 而不是手动偏移保证地址计算没有错误，然后绑定即可。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">YoloV8n::set_tensor_addresses</span><span class="hljs-params">()</span> </span>&#123;<br>    <span class="hljs-comment">// 分配显存和内存，并绑定输入缓冲区地址</span><br>    input_size = <span class="hljs-built_in">sizeof</span>(<span class="hljs-type">float</span>) * InputImg::<span class="hljs-built_in">total_size</span>();<br>    <span class="hljs-built_in">cudaHostAlloc</span>((<span class="hljs-type">void</span>**) &amp;input_buffer, input_size, cudaHostAllocDefault);<br>    <span class="hljs-built_in">cudaMalloc</span>(&amp;gpu_input, input_size);<br>    inference-&gt;<span class="hljs-built_in">set_tensor_address</span>(<span class="hljs-string">&quot;images&quot;</span>, gpu_input);<br><br>    <span class="hljs-comment">// 根据是否启用EfficientNMS插件，分配不同的输出缓冲区，并绑定输出地址</span><br>    <span class="hljs-keyword">if</span> (enable_efficient_nms) &#123;<br>        output_size = <span class="hljs-built_in">sizeof</span>(YoloDetectResultNMS);<br>    &#125; <span class="hljs-keyword">else</span> &#123;<br>        output_size = <span class="hljs-built_in">sizeof</span>(<span class="hljs-type">float</span>) * OutputRes::<span class="hljs-built_in">total_size</span>();<br>    &#125;<br><br>    <span class="hljs-built_in">cudaHostAlloc</span>((<span class="hljs-type">void</span>**) &amp;output_buffer, output_size, cudaHostAllocDefault);<br>    <span class="hljs-built_in">cudaMalloc</span>(&amp;gpu_output, output_size);<br><br>    <span class="hljs-keyword">if</span> (enable_efficient_nms) &#123;<br>        inference-&gt;<span class="hljs-built_in">set_tensor_address</span>(<br>            <span class="hljs-string">&quot;num_dets&quot;</span>,<br>            &amp;(<span class="hljs-built_in">reinterpret_cast</span>&lt;YoloDetectResultNMS*&gt;(gpu_output)-&gt;num_dets));<br>        inference-&gt;<span class="hljs-built_in">set_tensor_address</span>(<br>            <span class="hljs-string">&quot;det_boxes&quot;</span>,<br>            &amp;(<span class="hljs-built_in">reinterpret_cast</span>&lt;YoloDetectResultNMS*&gt;(gpu_output)-&gt;det_boxes));<br>        inference-&gt;<span class="hljs-built_in">set_tensor_address</span>(<br>            <span class="hljs-string">&quot;det_scores&quot;</span>,<br>            &amp;(<span class="hljs-built_in">reinterpret_cast</span>&lt;YoloDetectResultNMS*&gt;(gpu_output)-&gt;det_scores));<br>        inference-&gt;<span class="hljs-built_in">set_tensor_address</span>(<br>            <span class="hljs-string">&quot;det_classes&quot;</span>,<br>            &amp;(<span class="hljs-built_in">reinterpret_cast</span>&lt;YoloDetectResultNMS*&gt;(gpu_output)-&gt;det_classes));<br>    &#125; <span class="hljs-keyword">else</span> &#123;<br>        inference-&gt;<span class="hljs-built_in">set_tensor_address</span>(<span class="hljs-string">&quot;output0&quot;</span>, gpu_output);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="构造函数输入输出推理"><a href="#构造函数输入输出推理" class="headerlink" title="构造函数输入输出推理"></a>构造函数输入输出推理</h3><p>使用 <code>output_size</code> 和 <code>input_size</code> 两个成员变量存储输入输出的数据大小，避免每次都手 动计算，传输数据的部分直接这两个变量来指定传输的数据量。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">YoloV8n::infer</span><span class="hljs-params">()</span> </span>&#123;<br>    inference-&gt;<span class="hljs-built_in">infer</span>();<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">auto</span> <span class="hljs-title">YoloV8n::InputData</span><span class="hljs-params">()</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">cudaMemcpyAsync</span>(gpu_input, input_buffer, input_size,<br>                           cudaMemcpyHostToDevice, inference-&gt;<span class="hljs-built_in">get_stream</span>());<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">auto</span> <span class="hljs-title">YoloV8n::OutputData</span><span class="hljs-params">()</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">cudaMemcpyAsync</span>(output_buffer, gpu_output, output_size,<br>                           cudaMemcpyDeviceToHost, inference-&gt;<span class="hljs-built_in">get_stream</span>());<br>&#125;<br><br>YoloV8n::<span class="hljs-built_in">YoloV8n</span>(std::string onnx_path, std::string engine_path,<br>                 TRTLogger&amp; logger, <span class="hljs-type">bool</span> enable_efficient_nms_plugin,<br>                 <span class="hljs-type">bool</span> always_rebuild)<br>    : <span class="hljs-built_in">onnx_path</span>(onnx_path),<br>      <span class="hljs-built_in">engine_path</span>(engine_path),<br>      <span class="hljs-built_in">enable_efficient_nms</span>(enable_efficient_nms_plugin) &#123;<br>    <span class="hljs-keyword">auto</span> builder = <span class="hljs-built_in">TRTModelBuilder</span>(logger);<br>    <span class="hljs-keyword">if</span> (always_rebuild || !(engine = builder.<span class="hljs-built_in">loadFromPlan</span>(engine_path))) &#123;<br>        engine = builder.<span class="hljs-built_in">buildFromOnnx</span>(<br>            onnx_path, engine_path,<br>            [](nvinfer1::IBuilderConfig* config,<br>               nvinfer1::INetworkDefinition* network,<br>               nvinfer1::IBuilder* builder) &#123;<br>                <span class="hljs-comment">// 2. 精度设置：虽然 kFP16 弃用，但在 10.0 中作为 BuilderFlag</span><br>                <span class="hljs-comment">// 依然是生效的（会有警告）</span><br>                <span class="hljs-keyword">if</span> (builder-&gt;<span class="hljs-built_in">platformHasFastFp16</span>()) &#123;<br>                    config-&gt;<span class="hljs-built_in">setFlag</span>(nvinfer1::BuilderFlag::kFP16);<br>                &#125;<br>            &#125;);<br>    &#125;<br>    inference = <span class="hljs-built_in">TRTPtr</span>&lt;TRTInference&gt;(<span class="hljs-keyword">new</span> <span class="hljs-built_in">TRTInference</span>(*engine));<br><br>    <span class="hljs-built_in">set_tensor_addresses</span>();<br><br>    <span class="hljs-built_in">read_labels</span>(std::<span class="hljs-built_in">string</span>(PACKAGE_ROOT_DIR) + <span class="hljs-string">&quot;/config/labels.yml&quot;</span>);<br><br>    std::cout &lt;&lt; std::format(<br>                     <span class="hljs-string">&quot;Model initialized:\nONNX: &#123;&#125;\nEngine: &#123;&#125;\nEfficient NMS &quot;</span><br>                     <span class="hljs-string">&quot;Plugin: &#123;&#125;\nAlways rebuild: &#123;&#125;&quot;</span>,<br>                     onnx_path, engine_path,<br>                     enable_efficient_nms ? <span class="hljs-string">&quot;Enabled&quot;</span> : <span class="hljs-string">&quot;Disabled&quot;</span>,<br>                     always_rebuild ? <span class="hljs-string">&quot;True&quot;</span> : <span class="hljs-string">&quot;False&quot;</span>)<br>              &lt;&lt; std::endl;<br>&#125;<br></code></pre></td></tr></table></figure>]]>
    </content>
    <id>https://chillywall.github.io/ChillyBlog/2026/04/09/TensorRT%E9%83%A8%E7%BD%B2YOLO%20V8%E6%A8%A1%E5%9E%8B/</id>
    <link href="https://chillywall.github.io/ChillyBlog/2026/04/09/TensorRT%E9%83%A8%E7%BD%B2YOLO%20V8%E6%A8%A1%E5%9E%8B/"/>
    <published>2026-04-09T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>本文介绍我使用TensorRT部署Yolo模型并实现读取摄像头实时目标检测的过程。</p>]]>
    </summary>
    <title>TensorRT部署YOLO V8模型</title>
    <updated>2026-04-10T05:07:21.884Z</updated>
  </entry>
  <entry>
    <author>
      <name>郭金锋</name>
    </author>
    <category term="Editor" scheme="https://chillywall.github.io/ChillyBlog/categories/Editor/"/>
    <category term="Emacs" scheme="https://chillywall.github.io/ChillyBlog/categories/Editor/Emacs/"/>
    <category term="Emacs" scheme="https://chillywall.github.io/ChillyBlog/tags/Emacs/"/>
    <content>
      <![CDATA[<p>Emacs 支持很多编译时可选的特性，当从包管理器或其他预编译的二进制版本无法满足你的 需求时，就需要自己在本地编译 Emacs 的源码。本篇文章记录编译 Emacs 源码的过程。</p><span id="more"></span><p>本文介绍流程在 Docker 创建的纯净的 Ubuntu 24.04 容器中进行实际操作，仅给出 Ubuntu24.04 中的命令，其余操作系统仅供参考。</p><h2 id="获取源码"><a href="#获取源码" class="headerlink" title="获取源码"></a>获取源码</h2><p>第一步是获取源码，我们可以直接从其<a href="https://www.gnu.org/software/emacs/download.html#gnu-linux">官网</a>上获取其源码，我们选择当前最新版本 30.2 作为 后续使用的源码。</p><p>我们将其解压，即可开始编译。</p><h2 id="配置编译选项及依赖"><a href="#配置编译选项及依赖" class="headerlink" title="配置编译选项及依赖"></a>配置编译选项及依赖</h2><p>Emacs 的源码使用 Makefile 作为构建系统，使用 Autotools 实现自动化。文档是用的 <code>makeinfo</code> 需要安装 texinfo 编译前先安装基础编译工具链。通过以下命令安装：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">sudo</span> apt install build-essential autoconf texinfo<br></code></pre></td></tr></table></figure><p>之后我们就可以运行编译脚本了，在源码根目录运行 <code>./configure</code> 命令，这会检查你的环 境依赖并生成最终的 Makefile，并且会显示最终被启用的特性列表。</p><p>如果出现以下错误：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs sh">checking <span class="hljs-keyword">for</span> X... no<br>checking <span class="hljs-keyword">for</span> X... <span class="hljs-literal">false</span><br>configure: error: You seem to be running X, but no X development libraries<br>were found.  You should install the relevant development files <span class="hljs-keyword">for</span> X<br>and <span class="hljs-keyword">for</span> the toolkit you want, such as Gtk+ or Motif.  Also make<br>sure you have development files <span class="hljs-keyword">for</span> image handling, i.e.<br>tiff, gif, jpeg, png and xpm.<br>If you are sure you want Emacs compiled without X window support, pass<br>  --without-x<br>to configure.<br></code></pre></td></tr></table></figure><p>这表示 Emacs 将会不支持图形界面，只有命令行界面，同时也会不支持处理 png 等图片格 式。原因是没有检测到系统中有图形界面相关的开发库，如果是在容器中进行编译这是必然 的，因为容器内不会安装图形界面，自然也不会有相关的开发库。但哪怕是在桌面系统中， 也可能会出现这个问题，因为一般桌面只会安装运行库，而源码编译需要开发库，在 Ubuntu 中即 <code>-dev</code> 后缀的库。</p><p>在 Linux 系统上我们一般使用 GTK 作为图形库，我们安装 GTK3 的开发库，以及一些处理图片所 需的库，以及 GnuTLS，Emacs 内置的网络模块依赖于它。以及 ncurses 开发的库，用于提供终端支持。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">sudo</span> apt install libgtk-3-dev libgif-dev libxpm-dev libgnutls28-dev libncurses-dev<br></code></pre></td></tr></table></figure><p>上述即编译 Emacs 的必须库，确保都安装之后我们再次运行 configure 命令，运行成功后，我们查看末尾当前特性列表的开启情况：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><code class="hljs text">Configured for &#x27;x86_64-pc-linux-gnu&#x27;.<br><br>  Where should the build process find the source code?    .<br>  What compiler should emacs be built with?               gcc -g3 -O2<br>  Should Emacs use the GNU version of malloc?             no<br>    (The GNU allocators don&#x27;t work with this system configuration.)<br>  Should Emacs use a relocating allocator for buffers?    no<br>  Should Emacs use mmap(2) for buffer allocation?         no<br>  What window system should Emacs use?                    x11<br>  What toolkit should Emacs use?                          GTK3<br>  Where do we find X Windows header files?                Standard dirs<br>  Where do we find X Windows libraries?                   Standard dirs<br>  Does Emacs use -lXaw3d?                                 no<br>  Is Emacs being built for Android?                       no<br>  Does Emacs use the X Double Buffer Extension?           yes<br>  Does Emacs use -lXpm?                                   yes<br>  Does Emacs use -ljpeg?                                  yes<br>  Does Emacs use -ltiff?                                  yes<br>  Does Emacs use a gif library?                           yes -lgif<br>  Does Emacs use a png library?                           yes -lpng16<br>  Does Emacs use -lrsvg-2?                                no<br>  Does Emacs use -lwebp?                                  yes<br>  Does Emacs use -lsqlite3?                               no<br>  Does Emacs use cairo?                                   yes<br>  Does Emacs use -llcms2?                                 no<br>  Does Emacs use imagemagick?                             no<br>  Does Emacs use native APIs for images?                  no<br>  Does Emacs support sound?                               yes<br>  Does Emacs use -lgpm?                                   no<br>  Does Emacs use -ldbus?                                  yes<br>  Does Emacs use -lgconf?                                 no<br>  Does Emacs use GSettings?                               yes<br>  Does Emacs use a file notification library?             yes (inotify)<br>  Does Emacs use access control lists?                    no<br>  Does Emacs use -lselinux?                               yes<br>  Does Emacs use -lgnutls?                                yes<br>  Does Emacs use -lxml2?                                  no<br>  Does Emacs use -lfreetype?                              yes<br>  Does Emacs use HarfBuzz?                                yes<br>  Does Emacs use -lm17n-flt?                              no<br>  Does Emacs use -lotf?                                   no<br>  Does Emacs use -lxft?                                   no<br>  Does Emacs use -lsystemd?                               no<br>  Does Emacs use -ltree-sitter?                           no<br>  Does Emacs use the GMP library?                         yes<br>  Does Emacs directly use zlib?                           yes<br>  Does Emacs have dynamic modules support?                yes<br>  Does Emacs use toolkit scroll bars?                     yes<br>  Does Emacs support Xwidgets?                            no<br>  Does Emacs have threading support in lisp?              yes<br>  Does Emacs support the portable dumper?                 yes<br>  Does Emacs support legacy unexec dumping?               no<br>  Which dumping strategy does Emacs use?                  pdumper<br>  Does Emacs have native lisp compiler?                   no<br>  Does Emacs use version 2 of the X Input Extension?      yes<br>  Does Emacs generate a smaller-size Japanese dictionary? no<br></code></pre></td></tr></table></figure><p>这是一个很基础的 Emacs，很多特性没有支持，这里介绍一下比较重要的几个特性，其余特 性保持默认即可，已经是推荐选项。</p><h3 id="图形系统"><a href="#图形系统" class="headerlink" title="图形系统"></a>图形系统</h3><p>首先是图形系统，目前使用 X11 和 Gtk3，如果你正在使用 X11 则不用管，但如果你只用 Wayland，并且希望 Emacs 可以原生运行在 Wayland 上，可以通过加入<code>--with-pgtk</code>参数 设置。同时需要安装 Wayland 的开发库，之前安装 GTK 开发库时已经作为依赖被安装。结 果显示如下表示成功。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">What window system should Emacs use?                    pgtk<br></code></pre></td></tr></table></figure><p>上面 Xpm 和 X Double Buffer Extension 都是 X11 下才会生效的特性，当使用 PGTK 时 会自动关闭，无需在意。</p><h3 id="图片支持"><a href="#图片支持" class="headerlink" title="图片支持"></a>图片支持</h3><p>Emacs 原生支持处理图片，你可以看到上面已经包含了 gif，png，jpeg，tiff，webp 等格 式的原生支持都一起用，但是 <code>-lrsvg-2</code> 显示为 no 表示矢量图形即 svg 渲染未启用， 这是由于系统中缺少这个开发库，安装后一般会自动启用，这里建议安装并启用。</p><p>而 <code>-llcms2</code> 也是未启用，该库与图片色彩管理相关，主要影响图片浏览。该特性以来满足 时默认开启，建议安装以启用。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">sudo</span> apt install librsvg2-dev liblcms2-dev<br></code></pre></td></tr></table></figure><p>在之前（Emacs 26 以前），往往使用 ImageMagick 进行图片相关的处理，如缩放等，但是 在新版本中推荐不使用它而是使用各种原生 api，比如上面的 libjpeg，libpng 等，以及 Cairo 来替代。Cairo 是一个跨平台的开源 2D 矢量图形渲染库，建议使用它而非 ImageMagick，安装 gtk 时一般已经安装了它的开发库，确保上面 <code>-lcairo</code> 为 yes。而</p><p>在 Linux 上并没有统一的图片 API 而是通过一个个动态库实现，因此 <code>native APIs for images</code> 显示为 no，这是正常的，无需理会。</p><h3 id="原生编译"><a href="#原生编译" class="headerlink" title="原生编译"></a>原生编译</h3><p>在 Emacs 28 之后，添加了一项新的特性成为原生编译（Native Comp），用于 ELisp 的运 行。ELisp 原先是先编译为字节码再通过虚拟机运行。而原生编译则是通过 libgccjit 将 ELisp 的字节码直接转化为机器指令，即 AOT。强烈建议将其开启。</p><p>首先安装 libgccjit，建议安装与系统默认版本 gcc 相同版本的。之后运行 configure，加入如下参数。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">sudo</span> apt install libgccjit-13-dev<br>./configure --with-pgtk --with-native-compilation=aot<br></code></pre></td></tr></table></figure><h3 id="杂项"><a href="#杂项" class="headerlink" title="杂项"></a>杂项</h3><p><strong>强烈建议</strong>开启 sqlite3 的支持，为一些插件提供数据库支持。如果在系统中安装了 sqlite3 的开发库，该选项将会默认开启。</p><p>还有 xml2 的支持，Emacs 内置的一些功能，如浏览器，RSS 阅读器等功能，会需要 xml 解析功能，<strong>强烈建议</strong>安装相应的开发库，会默认开启。</p><p><code>-lotf</code> 这一项为 no，表示不会使用 libotf 这个库来处理字体，但是我们已经启用了 HaffBuzz，其功能更加强大，即使没有也无需在意。如果系统了安装该开发库，该选项会默 认启用，启用了也没有坏处。</p><p><code>-lsystemd</code> 这一项为 no，如果要启用需要安装对应开发库。启用后可以和 systemd 更好集 成，一般没什么用，默认启用。</p><p><code>-ltree-sitter</code> <strong>强烈建议启用</strong>，安装对应开发库之后会自动启用。该特性用于 tree-sitter 支持，其用于解析代码的 AST，让 Emacs 可以理解代码的结构，非常重要。</p><p>XWidget 相关支持即通过 webkit2gtk 来实现将浏览器嵌入到 Emacs 中，webkit2gtk 非常沉重，且容易导致 Emacs 崩溃，一般没有必要启用。</p><p>还有一个参数，虽然不在特性列表上，但是可能会在末尾进行警告，与电子邮件相关。 Emacs 默认使用 movemail，这是一个过时的不安全的 POP3 客户端，Emacs 会推荐你用更安全更现代的来替代它，比如 Gnu Mailutils。一般不必在意，目前主流的 Emacs 邮件客户端这两个都不需要。</p><p>其余特性一般要么已经过时，要么没有必要，无需在意。</p><p>安装命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">sudo</span> apt install libsqlite3-dev libotf-dev libsystemd-dev libtree-sitter-dev<br></code></pre></td></tr></table></figure><h3 id="最终列表"><a href="#最终列表" class="headerlink" title="最终列表"></a>最终列表</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><code class="hljs text">Configured for &#x27;x86_64-pc-linux-gnu&#x27;.<br><br>  Where should the build process find the source code?    .<br>  What compiler should emacs be built with?               gcc -g3 -O2<br>  Should Emacs use the GNU version of malloc?             no<br>    (The GNU allocators don&#x27;t work with this system configuration.)<br>  Should Emacs use a relocating allocator for buffers?    no<br>  Should Emacs use mmap(2) for buffer allocation?         no<br>  What window system should Emacs use?                    pgtk<br>  What toolkit should Emacs use?                          GTK3<br>  Where do we find X Windows header files?                Standard dirs<br>  Where do we find X Windows libraries?                   Standard dirs<br>  Does Emacs use -lXaw3d?                                 no<br>  Is Emacs being built for Android?                       no<br>  Does Emacs use the X Double Buffer Extension?           no<br>  Does Emacs use -lXpm?                                   no<br>  Does Emacs use -ljpeg?                                  yes<br>  Does Emacs use -ltiff?                                  yes<br>  Does Emacs use a gif library?                           yes -lgif<br>  Does Emacs use a png library?                           yes -lpng16<br>  Does Emacs use -lrsvg-2?                                yes<br>  Does Emacs use -lwebp?                                  yes<br>  Does Emacs use -lsqlite3?                               yes<br>  Does Emacs use cairo?                                   yes<br>  Does Emacs use -llcms2?                                 yes<br>  Does Emacs use imagemagick?                             no<br>  Does Emacs use native APIs for images?                  no<br>  Does Emacs support sound?                               yes<br>  Does Emacs use -lgpm?                                   no<br>  Does Emacs use -ldbus?                                  yes<br>  Does Emacs use -lgconf?                                 no<br>  Does Emacs use GSettings?                               yes<br>  Does Emacs use a file notification library?             yes (inotify)<br>  Does Emacs use access control lists?                    no<br>  Does Emacs use -lselinux?                               yes<br>  Does Emacs use -lgnutls?                                yes<br>  Does Emacs use -lxml2?                                  yes<br>  Does Emacs use -lfreetype?                              yes<br>  Does Emacs use HarfBuzz?                                yes<br>  Does Emacs use -lm17n-flt?<br>  Does Emacs use -lotf?                                   yes<br>  Does Emacs use -lxft?<br>  Does Emacs use -lsystemd?                               yes<br>  Does Emacs use -ltree-sitter?                           yes<br>  Does Emacs use the GMP library?                         yes<br>  Does Emacs directly use zlib?                           yes<br>  Does Emacs have dynamic modules support?                yes<br>  Does Emacs use toolkit scroll bars?                     yes<br>  Does Emacs support Xwidgets?                            no<br>  Does Emacs have threading support in lisp?              yes<br>  Does Emacs support the portable dumper?                 yes<br>  Does Emacs support legacy unexec dumping?               no<br>  Which dumping strategy does Emacs use?                  pdumper<br>  Does Emacs have native lisp compiler?                   yes<br>  Does Emacs use version 2 of the X Input Extension?      no<br>  Does Emacs generate a smaller-size Japanese dictionary? no<br></code></pre></td></tr></table></figure><p>到此，所有参数配置基本结束。可以进行编译了。</p><p>在特性之外，如果要指定安装路径，只需要在 configure 中加入如下参数：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">./configure --with-pgtk --with-native-compilation=aot --prefix=/path/to/install<br></code></pre></td></tr></table></figure><h2 id="构建部署"><a href="#构建部署" class="headerlink" title="构建部署"></a>构建部署</h2><p>剩下的过程就是构建了，建议使用多线程并行编译加快速度。使用 <code>nproc</code> 命令获取核心数量。构建完成后安装，如果显示权限不足就加上 <code>sudo</code> 或者改到用户目录。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">make -j$(<span class="hljs-built_in">nproc</span>) &amp;&amp; make install<br></code></pre></td></tr></table></figure>]]>
    </content>
    <id>https://chillywall.github.io/ChillyBlog/2026/03/09/%E6%9C%AC%E5%9C%B0%E7%BC%96%E8%AF%91Emacs/</id>
    <link href="https://chillywall.github.io/ChillyBlog/2026/03/09/%E6%9C%AC%E5%9C%B0%E7%BC%96%E8%AF%91Emacs/"/>
    <published>2026-03-09T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>Emacs 支持很多编译时可选的特性，当从包管理器或其他预编译的二进制版本无法满足你的 需求时，就需要自己在本地编译 Emacs 的源码。本篇文章记录编译 Emacs 源码的过程。</p>]]>
    </summary>
    <title>本地编译Emacs</title>
    <updated>2026-04-10T05:07:21.886Z</updated>
  </entry>
  <entry>
    <author>
      <name>郭金锋</name>
    </author>
    <category term="Tools" scheme="https://chillywall.github.io/ChillyBlog/categories/Tools/"/>
    <category term="Pandoc" scheme="https://chillywall.github.io/ChillyBlog/categories/Tools/Pandoc/"/>
    <category term="Pandoc" scheme="https://chillywall.github.io/ChillyBlog/tags/Pandoc/"/>
    <category term="Markdown" scheme="https://chillywall.github.io/ChillyBlog/tags/Markdown/"/>
    <category term="Latex" scheme="https://chillywall.github.io/ChillyBlog/tags/Latex/"/>
    <content>
      <![CDATA[<p>Pandoc是一个强大的文档转换工具，可以将Markdown文件转换为多种格式，包括PDF。结合Latex，我们可以生成高质量的PDF文档。本文将介绍如何使用Pandoc和Latex将Markdown转换为漂亮的PDF。</p><span id="more"></span><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>Markdown是一种轻量级标记语言，广泛用于编写文档。通常情况下，我们阅读Markdown都是将其转换为网页使用浏览器来阅读，目前绝大多数Markdown阅读器都是采用此种方式。</p><p>当我们想要将Markdown文档保存为PDF文件时，如果通过转为HTML再打印为PDF，页面将会显得简陋，比如没有页眉页脚等，难以满足我们的要求。</p><p>而Latex是一个非常强大的排版系统，可以将tex文件编译成美观的的PDF文档，非常适合印刷物风格的文档。（当然Word也很好，但是惯用Word的人，大概直接就用Word写了吧。）</p><p>我们可以通过Pandoc将Markdown文档转换为latex格式，再使用Latex编译器将其编译成PDF文档。这样可以充分利用Latex的排版能力和Markdown的简洁，生成高质量的PDF文件。对于电脑上有Latex环境的人来说，这是很方便的。</p><p>我们可以通过Pandoc将Markdown转为latex再编译，也可以由pandoc自动完成转换和编译直接生成pdf。</p><p>如果觉得Pandoc的默认样式不够美观，Pandoc也支持自定义模板。如果要添加一些pandoc原生没有的功能，我们还可以自己编写过滤器，由pandoc调用，进行拓展。</p><p>下面将介绍Pandoc的基础用法以及模板Eisvogel的使用和调试。</p><h2 id="安装Pandoc"><a href="#安装Pandoc" class="headerlink" title="安装Pandoc"></a>安装Pandoc</h2><p>要安装Pandoc，推荐到其<a href="https://github.com/jgm/pandoc/releases">Github仓库</a>直接下载。大多数包管理器中都有pandoc但是版本可能较旧。</p><h2 id="Pandoc基础用法"><a href="#Pandoc基础用法" class="headerlink" title="Pandoc基础用法"></a>Pandoc基础用法</h2><p>pandoc命令行的基础命令如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">pandoc [options] [input-file]...<br></code></pre></td></tr></table></figure><p>options中常用的主要如下：</p><ul><li><code>-f FORMAT, -r FORMAT  --from=FORMAT, --read=FORMAT</code> 指定输入格式FORMAT，如markdown，latex等</li><li><code>-t FORMAT, -w FORMAT  --to=FORMAT, --write=FORMAT</code> 指定输出格式FORMAT，如pdf，latex，docx等</li><li><code>-o FILE               --output=FILE</code>，指定输出文件名</li><li><code>-s[true|false]        --standalone[=true|false]</code> 指定是否生成独立的文档，若生成latex，为true时生成包含preamble，为false时没有。不加该选项则为false，加该选项默认值为true</li><li><code>--pdf-engine=PROGRAM</code> 生成pdf时指定要使用的engine，如果通过latex作为中间媒介，则选择latex的编译器，默认为<code>pdflatex</code>亦可选择使用<code>lualatex</code>，<code>xelatex</code>等</li><li><code>--number-sections</code> 生成的文档中是否自动编号章节，默认为false</li></ul><p>例如如果我们有一个Markdown文件<code>example.md</code>，想要将其转换为PDF文件<code>example.pdf</code>，可以使用以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">pandoc -f markdown -t pdf -o example.pdf --pdf-engine=lualatex example.md<br></code></pre></td></tr></table></figure><h2 id="使用Eisvogel模板"><a href="#使用Eisvogel模板" class="headerlink" title="使用Eisvogel模板"></a>使用Eisvogel模板</h2><p>用于Pandoc的模板可以到网站<a href="https://pandoc-templates.org/">Pandoc-Templates</a>上找，其中<a href="https://github.com/Wandmalfarbe/pandoc-latex-template">Eisvogel</a>是一个非常流行的模板，提供了美观的排版和丰富的功能。</p><p>要使用模板，只需在Pandoc命令中加入参数<code>--template=FILE</code>，其中<code>FILE</code>是模板文件的路径。</p><p>或者，我们也可以将模板文件放到<code>~/.local/share/pandoc/templates</code>（Linux）或<code>~\AppData\Roaming\pandoc\templates</code>（Windows）文件夹中，Pandoc会默认在上面的文件夹中搜索模板文件。</p><p>Eisvogel模板提供了两个模板文件，一个<code>.latex</code>一个<code>.beamer</code>，我们要转换latex时使用<code>.latex</code>文件即可。</p><p>加上模板，上面的例子就变成了</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">pandoc -f markdown -t pdf -o example.pdf --pdf-engine=lualatex --template=eisvogel.latex example.md<br></code></pre></td></tr></table></figure><p>Pandoc会通过指定的模板将Markdown文档转换为Latex文档再使用指定的pdf-engine将其编译为pdf。</p><h3 id="设置字体"><a href="#设置字体" class="headerlink" title="设置字体"></a>设置字体</h3><p>如果照着上面做，但是生成的PDF文件中所有中文字体都是方块，不用惊慌，这是正常的，因为我们没有设置中文字体，而默认的只有英文字体，这些字体中没有中文字符。</p><p>要解决这个问题，我们只需要在我们的Markdown文件的Front-matter中指定即可。比如我们要使用Google的Noto CJK系列字体，就可以在Markdown的开头（前面不要加任何东西）加入这几行：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-meta">---</span><br><span class="hljs-attr">CJKmainfont:</span> <span class="hljs-string">Noto</span> <span class="hljs-string">Serif</span> <span class="hljs-string">CJK</span> <span class="hljs-string">SC</span><br><span class="hljs-attr">CJKsansfont:</span> <span class="hljs-string">Noto</span> <span class="hljs-string">Sans</span> <span class="hljs-string">CJK</span> <span class="hljs-string">SC</span><br><span class="hljs-attr">CJKmonofont:</span> <span class="hljs-string">Noto</span> <span class="hljs-string">Sans</span> <span class="hljs-string">Mono</span> <span class="hljs-string">CJK</span> <span class="hljs-string">SC</span><br><span class="hljs-meta">---</span><br></code></pre></td></tr></table></figure><p>这样在生成的latex文件中便会设置这些字体为相应的中文字体。其中<code>CJKmainfont</code>即中文正文字体，<code>CJKsansfont</code>即中文无衬线字体，常用于标题，<code>CJKmonofont</code>即中文等宽字体，一般用于一些代码块。</p><p>也可以通过此方法指定英文字体和数学字体</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">mainfont:</span> <span class="hljs-string">Libertinus</span> <span class="hljs-string">Serif</span><br><span class="hljs-attr">sansfont:</span> <span class="hljs-string">Libertinus</span> <span class="hljs-string">Sans</span><br><span class="hljs-attr">monofont:</span> <span class="hljs-string">Libertinus</span> <span class="hljs-string">Mono</span><br><span class="hljs-attr">mathfont:</span> <span class="hljs-string">Libertinus</span> <span class="hljs-string">Math</span><br></code></pre></td></tr></table></figure><h3 id="其它设置"><a href="#其它设置" class="headerlink" title="其它设置"></a>其它设置</h3><p>Eisvogel模板还创建了一些其它的变量，用于控制显示效果，比较常用的如下：</p><ul><li><code>titlepage</code> 是否创建标题页，默认是<code>false</code></li><li><code>toc-own-page</code> 是否将目录放在单独一页，默认是<code>false</code></li><li><code>listings-no-page-break</code> 是否禁止在列表中分页，默认为<code>false</code></li><li><code>header-left</code> 页眉左侧内容，默认为标题</li><li><code>header-center</code> 页眉中间内容，默认为空</li><li><code>header-right</code> 页眉右侧内容，默认为日期</li><li><code>footer-left</code> 页脚左侧内容，默认为作者</li><li><code>footer-center</code> 页脚中间内容，默认为空</li><li><code>footer-right</code> 页脚右侧内容，默认为页码</li></ul><p>更具体的列表可以到Eisvogel的README中查看。</p><h2 id="使用过滤器"><a href="#使用过滤器" class="headerlink" title="使用过滤器"></a>使用过滤器</h2><p>Pandoc支持自定义过滤器。Pandoc的处理过程是先读取解析Markdown文件，生成一个抽象语法树（AST），然后将AST转换为目标格式的文档。我们可以编写过滤器处理AST对象并将其转换为处理后的AST对象或直接生成原始目标片段比如Latex代码。</p><p>要调用过滤器，只需在Pandoc命令中加入<code>--filter=PROGRAM</code>选项，其中<code>PROGRAM</code>是过滤器程序的路径，过滤器程序应为可直接执行的程序，比如脚本或二进制。Pandoc更鼓励使用lua语言来编写过滤器，其在与Pandoc一起工作时性能上更有优势，要调用lua过滤器，只需在Pandoc命令中加入<code>--lua-filter=SCRIPTPATH</code>或<code>-L SCRIPTPATH</code>选项，其中<code>SCRIPTPATH</code>是lua脚本的路径。</p><p>也可以将程序或脚本放在<code>/pandoc/filters</code>目录下，类似于上面的templates文件夹，该文件夹会被Pandoc默认搜索。</p><p>以下将推荐几个常用的过滤器。</p><h3 id="pandoc-crossref"><a href="#pandoc-crossref" class="headerlink" title="pandoc-crossref"></a>pandoc-crossref</h3><p>pandoc-crossref是一个用于交叉引用的过滤器。原生的Pandoc和Markdown只支持使用链接来引用，无法进行自动编号等任务。而通过使用该过滤器，我们可以在Markdown文档中使用<code>{type:label}</code>的格式创建标记，再使用<code>@type:label</code>的格式来创建引用。在转换为latex时，pandoc-crossref会正确的将这些标记和引用转换为latex中合法的表达式并添加前缀。</p><p>比如对于图片可以这样写：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs plain">![caption](image.png)&#123;#fig:label&#125;<br><br>图 @fig:label 显示了一个图片<br></code></pre></td></tr></table></figure><p>在最终生成的Latex文档中，<code>fig:label</code>会成为该图片的label，<code>@fig:label</code>会被转换为latex的引用语句<code>\ref{fig:label}</code>。Latex会为其自动编号。</p><blockquote><p>Pandoc默认所使用的Markdown是Pandoc拓展后的格式，表格前后如果有这样一行形似<code>Listing: Listing caption</code>或<code>: Listing caption</code>，那么<code>Listing caption</code>将会成为这个表格的caption。我们也可以将标记放在这一行。</p></blockquote><p>如果要对section使用引用，必须令pandoc启用<code>--number-sections</code>参数，不然没有编号latex无法令引用生效。（在Latex中如果第一个标题不是一级标题，那么Latex会给其加一个前缀<code>0.</code>，如果我们希望使其变为<code>1.</code>，除了修改生成的latex之外可以在front-matter中添加<code>numberSections: true</code>，这样pandoc-crossref会自动处理这种情况，加一个隐式的一级标题。不过如果你想要<code>2.</code>，只能自己再将latex中的<code>1</code>改成<code>2</code>。）</p><p>Pandoc-crossref将引用转换为latex的引用时会自动加一个前缀，默认情况下前缀即类型名，比如<code>@fig:label</code>会变成<code>fig.~\ref{fig:label}</code>。如果我们想要更改这个前缀，可以通过在front-matter中设置相应的变量。</p><p>下面是一个示例文档，涉及了小节、公式、表格、代码块和图像的引用，并设置了字体和引用的前缀。</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br></pre></td><td class="code"><pre><code class="hljs markdown">---<br>CJKmainfont: Noto Serif CJK SC<br>CJKmonofont: Noto Sans Mono CJK SC<br>CJKsansfont: Noto Sans CJK SC<br>mainfont: Libertinus Serif<br>mathfont: Libertinus Math<br>monofont: Libertinus Mono<br>sansfont: Libertinus Sans<br>codeBlockCaptions: true<br>secPrefix: 小节<br>tblPrefix: 表格<br>eqnPrefix: 公式<br>figPrefix: 图<br><span class="hljs-section">lstPrefix: 列表</span><br><span class="hljs-section">---</span><br><br><span class="hljs-section"># 示例 &#123;#sec:example&#125;</span><br><br>@sec:equation 是一个示例小节，展示了如何在文档中使用公式、表格和代码块。<br><br>@sec:table 是一个示例小节，展示了如何在文档中使用表格。<br><br>@sec:code-block 是一个示例小节，展示了如何在文档中使用代码块。<br><br><span class="hljs-section">## 公式 &#123;#sec:equation&#125;</span><br><br>$$<br>e = \lim<span class="hljs-emphasis">_&#123;x \to \infty&#125; \left( 1 + \frac&#123;1&#125;&#123;x&#125; \right)^x</span><br><span class="hljs-emphasis">$$</span><br><span class="hljs-emphasis">&#123;#eq:natural-number&#125;</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">@eq:natural-number 是自然数e的定义。</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">## 表格 &#123;#sec:table&#125;</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">| Option | Explanation |</span><br><span class="hljs-emphasis">| :--- | :------ |</span><br><span class="hljs-emphasis">| `-L SCRIPTPATH` | 使用脚本`SCRIPTPATH` |</span><br><span class="hljs-emphasis">| `-F PROGRAM` | 使用程序`PROGRAM` |</span><br><span class="hljs-emphasis">: 过滤器相关参数 &#123;#tbl:options&#125;</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">@tbl:options 展示了与过滤器相关的参数。</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">## 代码块 &#123;#sec:code-block&#125;</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">有两种风格的代码块：一般风格和表格风格。</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">### 一般风格 &#123;#sec:code-block-general-style&#125;</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">```&#123;#lst:c-hello-world .c caption=&quot;C Hello World&quot;&#125;</span><br><span class="hljs-emphasis">#include <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">stdio.h</span>&gt;</span></span></span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">int main() &#123;</span><br><span class="hljs-emphasis">    printf(&quot;Hello, world!&quot;);</span><br><span class="hljs-emphasis">    return 0;</span><br><span class="hljs-emphasis">&#125;</span><br><span class="hljs-emphasis">```</span><br><span class="hljs-emphasis">@lst:c-hello-world 是C语言的一个简单示例。</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">&gt; 注意：caption不能省略，否则无法生成正确的引用。</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">### 表格风格 &#123;#sec:code-block-talbe-style&#125;</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">在Markdown文档的front-matter中将`codeBlockCaptions`设为`true`可以允许下面的写法，即类似表格的写法：</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">```cpp</span><br><span class="hljs-emphasis">#include <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">iostream</span>&gt;</span></span></span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">int main() &#123;</span><br><span class="hljs-emphasis">    std::cout &lt;&lt; &quot;Hello, world!&quot; &lt;&lt; std::endl;</span><br><span class="hljs-emphasis">    return 0;</span><br><span class="hljs-emphasis">&#125;</span><br><span class="hljs-emphasis">```</span><br><span class="hljs-emphasis">: C++ Hello World &#123;#lst:hello-world&#125;</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">@lst:hello-world 是C++版本的Hello World代码。</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">## 图 &#123;#sec:figure&#125;</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">![<span class="hljs-string">Pandoc Logo</span>](<span class="hljs-link">pandoc.png</span>)&#123;#fig:pandoc-logo&#125;</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">@fig:pandoc-logo 是Pandoc一个流行的非官方的logo，[<span class="hljs-string">链接</span>](<span class="hljs-link">https://github.com/tarleb/pandoc-logo</span>)。</span><br></code></pre></td></tr></table></figure><p>以下是示例的渲染结果：</p><p><img src="/ChillyBlog/2025/06/29/%E4%BD%BF%E7%94%A8Pandoc%E5%92%8CLatex%E5%B0%86Markdown%E8%BD%AC%E4%B8%BA%E6%BC%82%E4%BA%AE%E7%9A%84PDF/example-1.jpg" alt="example 1"></p><p><img src="/ChillyBlog/2025/06/29/%E4%BD%BF%E7%94%A8Pandoc%E5%92%8CLatex%E5%B0%86Markdown%E8%BD%AC%E4%B8%BA%E6%BC%82%E4%BA%AE%E7%9A%84PDF/example-2.jpg" alt="example 2"></p><p>更具体的文档可以到pandoc-crossref的<a href="https://lierdakil.github.io/pandoc-crossref">官方文档</a>中查看。</p><h3 id="diagram"><a href="#diagram" class="headerlink" title="diagram"></a>diagram</h3><p>现在很多Markdown解释器都支持在文档中插入Mermaid和Plantuml来绘制各种类型的图表。由于latex本身并不能渲染这些类型的图表，所以我们需要其他方法将这些图表放进latex中。</p><p>最简单的方法自然是先将图表渲染为图片，再插入文档中，之后便与一般图片无异。Pandoc已经有不少这一类的过滤器，这里介绍其中一个：<a href="https://github.com/pandoc-ext/diagram">diagram</a>.</p><p>该过滤器支持多种图表格式，包括Mermaid，Plantuml，TikZ，GraphViz，Asymptote和cetz。</p><p>它会将这些类型的代码块通过相对应的工具渲染之后将渲染结果插入原位置。</p><p>首先我们下载它的最新的release版本，将其中<code>diagram.lua</code>文件复制到filters文件夹下面即可。</p><p>这里主要介绍Mermaid。</p><p>在我之前的一篇文章<a href="/ChillyBlog/2025/05/11/CMake%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B">CMake基础教程</a>也曾画过一张简单的Mermaid流程图，这里以它为例：</p><pre><code class=" mermaid">flowchart LR    A[源代码] --&gt; |预处理| B[处理后源代码]    B --&gt; |编译器编译| C[汇编]    C --&gt; |汇编器编译| D[目标文件]    D --&gt; E[链接器]    F[其他目标文件] --&gt; E    G[库文件] --&gt; E    E --&gt; |链接| H[可执行文件或库文件]</code></pre><p>首先，该过滤器本身并不执行写入文件，也就说生成的图片并不会被保存，如果生成的是latex，那么最终我们只会获得一个指向不存在文件的链接。所以我们必须在pandoc命令中添加一个参数<code>--extract-media=DIR</code>，其中<code>DIR</code>是图片被保存的文件夹。如果是HTML的话，则可以使用参数<code>--embed-resources</code>。（不过要html有大把的解释器可以直接保存HTML文件，你大概不会用pandoc）</p><p>其次，生成的图片的文件名默认是hash值，毫无辨识度，我们可以在文档中指定其将会生成的文件名。</p><p>最后，我们还可以在文档中指定图表的caption和label。如果我们想要将其和pandoc-crossref配合使用，那么我们可以将label指定为<code>fig:label</code>的格式，后面引用就与原本的没有区别。</p><p>这里根据上面的Mermaid流程图写一个示例：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs markdown">---<br>CJKmainfont: Noto Serif CJK SC<br>CJKmonofont: Noto Sans Mono CJK SC<br>CJKsansfont: Noto Sans CJK SC<br>figPrefix: 图<br>mainfont: Libertinus Serif<br>mathfont: Libertinus Math<br>monofont: Libertinus Mono<br><span class="hljs-section">sansfont: Libertinus Sans</span><br><span class="hljs-section">---</span><br><br><span class="hljs-section"># C/C++的编译过程 &#123;#sec:cc-compilation-process&#125;</span><br><br>C/C++的编译过程通常如下：<br><br><span class="hljs-code">```&#123;.mermaid filename=&quot;cc-compile-process-flowchart&quot; caption=&quot;C/C++的编译流程图&quot; #fig:cc-compile-process&#125;</span><br><span class="hljs-code">flowchart LR</span><br><span class="hljs-code">    A[源代码] --&gt; |预处理| B[处理后源代码]</span><br><span class="hljs-code">    B --&gt; |编译器编译| C[汇编]</span><br><span class="hljs-code">    C --&gt; |汇编器编译| D[目标文件]</span><br><span class="hljs-code">    D --&gt; E[链接器]</span><br><span class="hljs-code">    F[其他目标文件] --&gt; E</span><br><span class="hljs-code">    G[库文件] --&gt; E</span><br><span class="hljs-code">    E --&gt; |链接| H[可执行文件或库文件]</span><br><span class="hljs-code">```</span><br><br>@fig:cc-compile-process 显示了C/C++的编译流程图。<br></code></pre></td></tr></table></figure><p>这里我们只能是会用一般风格来设置代码块的属性，因为diagram.lua的加入表格风格的写法会失效。这里<code>filename</code>的值即保存的文件名，<code>caption</code>用于设置caption，而label的设置则是直接使用了pandoc-crossref的写法，也可以使用<code>label</code>属性显示设置，最终的效果是一样的。</p><p>要对其引用，则就像之前一样使用<code>@fig:cc-compile-process</code>即可。</p><p>这是渲染效果图：</p><p><img src="/ChillyBlog/2025/06/29/%E4%BD%BF%E7%94%A8Pandoc%E5%92%8CLatex%E5%B0%86Markdown%E8%BD%AC%E4%B8%BA%E6%BC%82%E4%BA%AE%E7%9A%84PDF/example-3.jpg" alt="example 3"></p>]]>
    </content>
    <id>https://chillywall.github.io/ChillyBlog/2025/06/29/%E4%BD%BF%E7%94%A8Pandoc%E5%92%8CLatex%E5%B0%86Markdown%E8%BD%AC%E4%B8%BA%E6%BC%82%E4%BA%AE%E7%9A%84PDF/</id>
    <link href="https://chillywall.github.io/ChillyBlog/2025/06/29/%E4%BD%BF%E7%94%A8Pandoc%E5%92%8CLatex%E5%B0%86Markdown%E8%BD%AC%E4%B8%BA%E6%BC%82%E4%BA%AE%E7%9A%84PDF/"/>
    <published>2025-06-29T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>Pandoc是一个强大的文档转换工具，可以将Markdown文件转换为多种格式，包括PDF。结合Latex，我们可以生成高质量的PDF文档。本文将介绍如何使用Pandoc和Latex将Markdown转换为漂亮的PDF。</p>]]>
    </summary>
    <title>使用Pandoc和Latex将Markdown转为漂亮的PDF</title>
    <updated>2026-04-10T05:07:21.884Z</updated>
  </entry>
  <entry>
    <author>
      <name>郭金锋</name>
    </author>
    <category term="Languages" scheme="https://chillywall.github.io/ChillyBlog/categories/Languages/"/>
    <category term="CMake" scheme="https://chillywall.github.io/ChillyBlog/categories/Languages/CMake/"/>
    <category term="CMake" scheme="https://chillywall.github.io/ChillyBlog/tags/CMake/"/>
    <category term="C/C++" scheme="https://chillywall.github.io/ChillyBlog/tags/C-C/"/>
    <content>
      <![CDATA[<p>本文将介绍如何直接导入二进制文件，或导出项目，使得其他项目可以通过CMake的<code>find_package</code>直接导入我们的项目并基于其开发。</p><span id="more"></span><p>虽然CMake已经是事实上的标准，但我们有时还是会遇到只有二进制和头文件还没有模组文件和配置文件的情况（比如windows下我们通过cmake使用EasyX时就只能手动导入）。这时我们可以通过cmake将其导入，使其成为一个<code>IMPORTED</code>目标。</p><p>而实际上，库的配置文件就是一个cmake脚本，其可以将库导入成为导入库目标。<code>find_package</code>会找到这些配置文件并运行。</p><p>现在我们将先介绍如何导入，这部分比较简单。之后介绍如何导出项目。</p><h2 id="导入"><a href="#导入" class="headerlink" title="导入"></a>导入</h2><p>导入分为两种情况，导入可执行文件和导入库。一下将分别介绍。</p><h3 id="导入可执行文件"><a href="#导入可执行文件" class="headerlink" title="导入可执行文件"></a>导入可执行文件</h3><p>导入可执行文件也使用<code>add_executable</code>命令，语法如下</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">add_executable</span>(&lt;name&gt; IMPORTED [GLOBAL])<br></code></pre></td></tr></table></figure><p>默认情况下，导入目标仅在当前和子文件夹中生效，要使其全局生效，可以在最后添加<code>GLOBAL</code>关键字。</p><p>创建导入目标之后，要为其设置可执行文件的路径，一般直接设置其<code>IMPORTED_LOCATION</code>属性的值（需要绝对路径，我们可以使用cmake一些变量的值构建出来）。</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">set_target_properties</span>(&lt;<span class="hljs-keyword">target</span>&gt; PROPERTIES IMPORTED_LOCATION path/to/myexe)<br></code></pre></td></tr></table></figure><p>导入的可执行目标在<code>add_custom_command()</code>一类的命令中引用比较方便，这些命令需要需要一个可执行文件来进行一些操作，比如调用clang-format对头文件进行排版。</p><h3 id="导入库"><a href="#导入库" class="headerlink" title="导入库"></a>导入库</h3><p>导入库和导入可执行文件差不多，也是先创建导入目标再设置相应属性的值。</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">add_library</span>(&lt;name&gt; SHARED|STATIC IMPORTED [GLOBAL])<br></code></pre></td></tr></table></figure><p>设置<code>IMPORTED_LOCALTION</code>属性的值以使cmake找到该库文件。</p><blockquote><p>在Windows平台，动态库dll还需要导入库方可链接，导入库的后缀通常为<code>.lib</code>，导入库的位置通过<code>IMPORTED_IMPLIB</code>属性指定。</p></blockquote><h2 id="导出"><a href="#导出" class="headerlink" title="导出"></a>导出</h2><p>导出通常是导出库。CMake的 <code>install</code> 命令提供了相应的支持，可以在安装库的同时生成导出目标，之后我们可以安装该导出目标。这个导出目标安装结果是一个CMake脚本文件，用于导入该库（即创建导入库）。</p><p>之后为了使CMake可以便捷的找到这些导出目标，我们还需要一个配置文件用于包含这些CMake脚本，这个配置文件通常由模板生成，我们需要创建一个模板文件，用CMake填充，并将生成的完整文件一同安装。</p><p>除此之外，我们在创建目标时还需要考虑到在构建过程中和被导入时的包含目录是不同的，所以我们在给目标添加包含目录时要添加两个不同情况下的路径。</p><h3 id="包含目录"><a href="#包含目录" class="headerlink" title="包含目录"></a>包含目录</h3><p>以下面的语句为例。</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs cmake"><span class="hljs-keyword">target_include_directories</span>(<br>    demo<br>    PUBLIC<br>        <span class="hljs-string">&quot;$&lt;BUILD_INTERFACE:$&#123;CMAKE_CURRENT_SOURCE_DIR&#125;/include&gt;&quot;</span><br>        <span class="hljs-string">&quot;$&lt;INSTALL_INTERFACE:include&gt;&quot;</span><br>)<br></code></pre></td></tr></table></figure><p><code>$&lt;...&gt;</code> 称作生成器表达式，它们在构建时会被替换为相应的值。我们添加了两个生成器表达式</p><ul><li><code>$&lt;BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include&gt;</code> ：用于指定构建过程中的包含目录，不能像之前那样使用相对路径，但我们可以通过 <code>CMAKE_CURRENT_SOURCE_DIR</code> 这个变量获取当前 <code>CMakeLists.txt</code> 文件所在的路径，再相对这个路径进行指定。</li><li><code>$&lt;INSTALL_INTERFACE:include&gt;</code> ：用于指定安装后的包含目录，可以使用相对路径，相对于 <code>CMAKE_INSTALL_PREFIX</code> 。</li></ul><blockquote><p>不要忘记将公开的头文件也安装到指定位置。</p></blockquote><h3 id="导出目标"><a href="#导出目标" class="headerlink" title="导出目标"></a>导出目标</h3><p>我们在使用 <code>install</code> 命令安装目标时，可以通过 <code>EXPORT</code> 参数同时创建导出目标，命名习惯是 <code>libraryName-targets</code> 。命令类似下面</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs cmake"><span class="hljs-keyword">install</span>(<br>    TARGETS demo<br>    <span class="hljs-keyword">EXPORT</span> demo-targets<br>)<br></code></pre></td></tr></table></figure><p>我们可以在多个 <code>install</code> 命令中使用同一个 <code>EXPORT</code> 名称，这样会将多个目标导出到同一个导出目标中。</p><p>之后我们需要安装导出目标，这样会将生成的导入脚本安装到指定位置，其他项目就可以通过运行这些CMake脚本创建导入目标。命令如下</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs cmake"><span class="hljs-keyword">install</span>(<br>    <span class="hljs-keyword">EXPORT</span> demo-targets<br>    <span class="hljs-keyword">FILE</span> demo-targets.cmake<br>    NAMESPACE demo::<br>    DESTINATION lib/cmake/demo<br>)<br></code></pre></td></tr></table></figure><p>我们使用<code>install(EXPORT ...)</code> 命令来安装导出目标，<code>FILE</code> 参数指定导出目标的脚本文件名。</p><p><code>NAMESPACE</code> 用于指定命名空间，包含在该导出目标中的目标导入后其名称前会被加上命名空间作为前缀，一般使用C++的命名习惯，使用两个冒号 <code>::</code> 作为间隔。导入后像这样引用 <code>link_library(demo::demo)</code>。当然也可以留空，这样导入后的目标的名称前不会加任何前缀。</p><p><code>DESTINATION</code> 参数指定导出目标的安装位置，一般放在 <code>lib/cmake/libraryName</code> 目录下。</p><h3 id="配置文件"><a href="#配置文件" class="headerlink" title="配置文件"></a>配置文件</h3><p>之后要生成配置文件， <code>find_package</code> 命令会寻找这个文件作为库个配置文件。这个文件一般用于包含我们导出的那些导出目标文件。</p><h4 id="模板"><a href="#模板" class="headerlink" title="模板"></a>模板</h4><p>首先我们需要先写一个模板文件，命名为 <code>Config.cmake.in</code>，类似下面</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs cmake">@PACKAGE_INIT@<br><br><span class="hljs-keyword">include</span>(CMakeFindDependencyMacro)<br>find_dependency(demo_dep REQUIRED)<br><br><span class="hljs-keyword">include</span>(<span class="hljs-string">&quot;$&#123;CMAKE_CURRENT_LIST_DIR&#125;/demo-targets.cmake&quot;</span>)<br><br>check_required_components(demo)<br></code></pre></td></tr></table></figure><p>第一行将会被处理并替换为相应的初始化内容。</p><p>第二、三行用于包含依赖的包。如果我们的库有依赖，那么我们不仅在构建时需要处理好，在导出时也需要在配置文件里面处理好依赖。首先我们需要包含 <code>CMakeFindDependencyMacro</code> 模块，这个模块提供了 <code>find_dependency</code> 函数用于查找依赖的包。我们可以通过 <code>REQUIRED</code> 参数来指定该依赖是必须的。<code>find_dependency</code> 的用法与 <code>find_package</code> 基本相同，但一般只推荐在配置文件使用。</p><p>第四行用于包含我们安装的导出文件，我们通过 <code>CMAKE_CURRENT_LIST_DIR</code> 变量来获取当前处理的文件的路径，也就是该配置文件的路径。</p><p>第五行用于检查是否包含了必要的组件。我们目前没有使用组件功能，因此只有一个组件也就是 <code>demo</code></p><h4 id="生成"><a href="#生成" class="headerlink" title="生成"></a>生成</h4><p>CMake提供了一个 <code>configure_package_config_file</code> 函数用于生成配置文件。要使用这个文件我们需要先包含CMake提供的工具 <code>CMakePackageConfigHelpers</code>。这个命令可以通过模板文件生成配置文件。</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs cmake"><span class="hljs-keyword">include</span>(CMakePackageConfigHelpers)<br>configure_package_config_file(<br>    <span class="hljs-string">&quot;$&#123;CMAKE_CURRENT_SOURCE_DIR&#125;/Config.cmake.in&quot;</span><br>    <span class="hljs-string">&quot;$&#123;CMAKE_CURRENT_BINARY_DIR&#125;/demo-config.cmake&quot;</span><br>    INSTALL_DESTINATION lib/cmake/demo<br>)<br></code></pre></td></tr></table></figure><p>第一个参数是模板文件的路径，第二个参数是生成的配置文件的路径，我们将其放在当前构建文件夹而不是直接放在安装目录，第三个参数 <code>INSTALL_DESTINATION</code> 是安装位置，一般在 <code>lib/cmake/libraryName</code>。</p><h4 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h4><p>最后我们将配置文件安装到我们刚刚指定的安装位置（上一个命令虽然指定了但没有执行安装）。</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs cmake"><span class="hljs-keyword">install</span>(<br>    FILES <span class="hljs-string">&quot;$&#123;CMAKE_CURRENT_BINARY_DIR&#125;/demo-config.cmake&quot;</span><br>    DESTINATION lib/cmake/demo<br>)<br></code></pre></td></tr></table></figure><p>到此配置就完成了。</p><h3 id="完整代码示例"><a href="#完整代码示例" class="headerlink" title="完整代码示例"></a>完整代码示例</h3><p><code>CMakeLists.txt</code>：</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><code class="hljs cmake"><span class="hljs-comment"># 创建目标</span><br><span class="hljs-keyword">add_library</span>(demo SHARED demo.cpp)<br><br><span class="hljs-comment"># 链接依赖库</span><br><span class="hljs-keyword">target_link_libraries</span>(demo PUBLIC demo_dep)<br><br><span class="hljs-comment"># 添加包含目录</span><br><span class="hljs-keyword">target_include_directories</span>(<br>    demo<br>    PUBLIC<br>        <span class="hljs-string">&quot;$&lt;BUILD_INTERFACE:$&#123;CMAKE_CURRENT_SOURCE_DIR&#125;/include&gt;&quot;</span><br>        <span class="hljs-string">&quot;$&lt;INSTALL_INTERFACE:include&gt;&quot;</span><br>)<br><br><span class="hljs-comment"># 安装库</span><br><span class="hljs-keyword">install</span>(<br>    TARGETS demo<br>    <span class="hljs-keyword">EXPORT</span> demo-targets<br>)<br><br><span class="hljs-comment"># 安装头文件</span><br><span class="hljs-keyword">install</span>(DIRECTORY <span class="hljs-keyword">include</span>/ DESTINATION <span class="hljs-keyword">include</span>)<br><br><span class="hljs-comment"># 安装导出</span><br><span class="hljs-keyword">install</span>(<br>    <span class="hljs-keyword">EXPORT</span> demo-targets<br>    <span class="hljs-keyword">FILE</span> demo-targets.cmake<br>    NAMESPACE demo::<br>    DESTINATION lib/cmake/demo<br>)<br><br><span class="hljs-comment"># 生成配置文件</span><br><span class="hljs-keyword">include</span>(CMakePackageConfigHelpers)<br>configure_package_config_file(<br>    <span class="hljs-string">&quot;$&#123;CMAKE_CURRENT_SOURCE_DIR&#125;/Config.cmake.in&quot;</span><br>    <span class="hljs-string">&quot;$&#123;CMAKE_CURRENT_BINARY_DIR&#125;/demo-config.cmake&quot;</span><br>    INSTALL_DESTINATION lib/cmake/demo<br>)<br><br><span class="hljs-comment"># 安装配置文件</span><br><span class="hljs-keyword">install</span>(<br>    FILES <span class="hljs-string">&quot;$&#123;CMAKE_CURRENT_BINARY_DIR&#125;/demo-config.cmake&quot;</span><br>    DESTINATION lib/cmake/demo<br>)<br></code></pre></td></tr></table></figure><p><code>Config.cmake.in</code>：</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs cmake">@PACKAGE_INIT@<br><br><span class="hljs-keyword">include</span>(CMakeFindDependencyMacro)<br>find_dependency(demo_dep REQUIRED)<br><br><span class="hljs-keyword">include</span>(<span class="hljs-string">&quot;$&#123;CMAKE_CURRENT_LIST_DIR&#125;/demo-targets.cmake&quot;</span>)<br><br>check_required_components(demo)<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文简单介绍了如何使用CMake进行导入导出。足以让我们开发一些简单的共享库并使其他开发者可以方便地使用。</p><p>对于开发一些更复杂的库乃至框架，可能需要拆分成多个组件，本文并未涉及。</p>]]>
    </content>
    <id>https://chillywall.github.io/ChillyBlog/2025/05/16/CMake%E4%B8%93%E9%A2%98%E6%95%99%E7%A8%8B-%E5%AF%BC%E5%85%A5%E5%AF%BC%E5%87%BA/</id>
    <link href="https://chillywall.github.io/ChillyBlog/2025/05/16/CMake%E4%B8%93%E9%A2%98%E6%95%99%E7%A8%8B-%E5%AF%BC%E5%85%A5%E5%AF%BC%E5%87%BA/"/>
    <published>2025-05-16T14:00:00.000Z</published>
    <summary>
      <![CDATA[<p>本文将介绍如何直接导入二进制文件，或导出项目，使得其他项目可以通过CMake的<code>find_package</code>直接导入我们的项目并基于其开发。</p>]]>
    </summary>
    <title>CMake专题教程-导入导出</title>
    <updated>2026-04-10T05:07:21.884Z</updated>
  </entry>
  <entry>
    <author>
      <name>郭金锋</name>
    </author>
    <category term="Languages" scheme="https://chillywall.github.io/ChillyBlog/categories/Languages/"/>
    <category term="CMake" scheme="https://chillywall.github.io/ChillyBlog/categories/Languages/CMake/"/>
    <category term="CMake" scheme="https://chillywall.github.io/ChillyBlog/tags/CMake/"/>
    <category term="C/C++" scheme="https://chillywall.github.io/ChillyBlog/tags/C-C/"/>
    <content>
      <![CDATA[<p>上一篇文章<a href="/ChillyBlog/2025/05/11/CMake%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B">CMake基础教程</a>中介绍了CMake的一些基础用法，本文是该系列的进阶教程，将会介绍一些CMake的进阶用法，帮助我们更好地管理更复杂的项目。</p><span id="more"></span><h2 id="CMake的进阶使用"><a href="#CMake的进阶使用" class="headerlink" title="CMake的进阶使用"></a>CMake的进阶使用</h2><p>之前已经介绍了如何使用CMake添加目标并为其配置头文件包含目录和依赖库，但并没有涉及如何使用第三方库，以及更精细的配置。本节将会在这些方面进一步深入。</p><p>上一节已经解读了<a href="https://github.com/ChillyChill/MineSweeper">MineSweeper</a>的<code>msutils</code>和<code>ui</code>两个模块的CMakeLists.txt，本节将主要根据<code>qt_ui</code>模块的CMakeLists.txt来介绍。</p><p>这是<code>qt_ui</code>模块的CMakeLists.txt</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">find_package</span>(Qt6 COMPONENTS Core Gui Widgets Svg REQUIRED)<br><br><span class="hljs-keyword">set</span>(CMAKE_AUTOMOC <span class="hljs-keyword">ON</span>)<br><span class="hljs-keyword">set</span>(CMAKE_AUTORCC <span class="hljs-keyword">ON</span>)<br><br><span class="hljs-keyword">set</span>(SOURCES<br>    src/main.cpp<br>    src/mainwindow.cpp<br>    src/diff_menu.cpp<br>    src/cust_diff_form.cpp<br>    src/game_field.cpp<br>)<br><br><span class="hljs-keyword">set</span>(HEADERS<br>    <span class="hljs-keyword">include</span>/mainwindow.hpp<br>    <span class="hljs-keyword">include</span>/diff_menu.hpp<br>    <span class="hljs-keyword">include</span>/cust_diff_form.hpp<br>    <span class="hljs-keyword">include</span>/game_field.hpp<br>)<br><br><span class="hljs-keyword">set</span>(RUNTIME_LIBS Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Svg)<br><br><span class="hljs-keyword">set</span>(RESOURCES resources.qrc)<br><br><span class="hljs-keyword">add_executable</span>(MineSweeper-qt <span class="hljs-variable">$&#123;SOURCES&#125;</span> <span class="hljs-variable">$&#123;HEADERS&#125;</span> <span class="hljs-variable">$&#123;RESOURCES&#125;</span>)<br><span class="hljs-keyword">target_link_libraries</span>(MineSweeper-qt PRIVATE msutils <span class="hljs-variable">$&#123;RUNTIME_LIBS&#125;</span>)<br><span class="hljs-keyword">target_include_directories</span>(MineSweeper-qt PRIVATE <span class="hljs-keyword">include</span>)<br><br><span class="hljs-keyword">install</span>(TARGETS MineSweeper-qt DESTINATION bin)<br><br><span class="hljs-keyword">if</span>(<span class="hljs-variable">$&#123;CMAKE_SYSTEM_NAME&#125;</span> <span class="hljs-keyword">STREQUAL</span> <span class="hljs-string">&quot;Windows&quot;</span>)<br>    <span class="hljs-keyword">if</span>(<span class="hljs-keyword">NOT</span> <span class="hljs-keyword">DEFINED</span> QT_INSTALL_PATH)<br>        <span class="hljs-keyword">message</span>(<br>            SEND_ERROR<br>            <span class="hljs-string">&quot;You must define variable QT_INSTALL_PATH, which is the path where your Qt installed\n&quot;</span><br>            <span class="hljs-string">&quot;You can pass it by -D or set it in your CMakePresets.json&quot;</span><br>        )<br>    <span class="hljs-keyword">endif</span>()<br>    <span class="hljs-keyword">install</span>(<br>        FILES <span class="hljs-variable">$&#123;QT_INSTALL_PATH&#125;</span>/plugins/platforms/qwindows.dll<br>        DESTINATION bin/plugins/platforms<br>    )<br>    <span class="hljs-keyword">install</span>(<br>        FILES <span class="hljs-variable">$&#123;QT_INSTALL_PATH&#125;</span>/plugins/iconengines/qsvgicon.dll<br>        DESTINATION bin/plugins/iconengines<br>    )<br>    <span class="hljs-keyword">set_target_properties</span>(MineSweeper-qt PROPERTIES WIN32_EXECUTABLE <span class="hljs-keyword">TRUE</span>)<br>    <span class="hljs-keyword">install</span>(IMPORTED_RUNTIME_ARTIFACTS <span class="hljs-variable">$&#123;RUNTIME_LIBS&#125;</span> DESTINATION bin)<br><span class="hljs-keyword">endif</span>()<br></code></pre></td></tr></table></figure><h3 id="使用第三方库"><a href="#使用第三方库" class="headerlink" title="使用第三方库"></a>使用第三方库</h3><p>上一篇文章中介绍了如何链接库，但链接的只是我们自己创建的库，而真正开发时自然少不了需要使用第三方库的时候。</p><p>使用第三方库，最常用的还是使用<code>find_package</code>函数来查找。</p><p>其基础的语法是</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">find_package</span>(&lt;PackageName&gt; [&lt;version&gt;] [REQUIRED] [COMPONENTS &lt;components&gt;...])<br></code></pre></td></tr></table></figure><p>其寻找库的方式分为两种，模块模式和配置模式。</p><h4 id="模块模式"><a href="#模块模式" class="headerlink" title="模块模式"></a>模块模式</h4><p>模块模式通常使用外部提供而不是库本身提供的<code>Find&lt;PackageName&gt;.cmake</code>文件来按照特定方式去寻找模块。会在所有在变量<code>CMAKE_MODULE_PATH</code>中列出的目录中寻找。如果我们要使用非标准方式提供的模块文件，则只需要将其所在目录添加到该变量中即可。</p><p>CMake会设置相应的变量来存储查找结果，变量比较多，详情可以参阅<a href="https://cmake.com.cn/cmake/help/latest/manual/cmake-developer.7.html#standard-variable-names">官方文档</a>，这里只介绍几个</p><ul><li><code>&lt;PackageName&gt;_FOUND</code>：是否找到</li><li><code>&lt;PackageName&gt;_INCLUDE_DIRS</code>：模块中所有包含目录的最终集合</li><li><code>&lt;PackageName&gt;_LIBRARIES</code>：模块中所有的库</li></ul><p>如果是按照这种方式查找的，我们可以通过这三个变量判断库有没有找到，并将其包含目录和库全部添加到我们的目标上。</p><p>这种模式已经被逐渐弃用，现在主流的是使用配置模式。其使用更符合Modern CMake的基于目标的实现形式。</p><h4 id="配置模式"><a href="#配置模式" class="headerlink" title="配置模式"></a>配置模式</h4><p>在此模式下，CMake 搜索名为<code>&lt;lowercasePackageName&gt;-config.cmake</code>或<code>&lt;PackageName&gt;Config.cmake</code>的文件。如果指定了版本详细信息，它还会查找<code>&lt;lowercasePackageName&gt;-config-version.cmake</code>或<code>&lt;PackageName&gt;ConfigVersion.cmake</code>文件，去做版本相关的处理。这些文件通常在<code>lib/cmake/PackageName</code>下面。</p><p>CMake确定路径结构时常常需要使用相对路径，其会将某个路径作为Prefix，以这些路径作为当前目录取相对路径。比如之前我们设置<code>CMAKE_INSTALL_PREFIX</code>变量为某个值，就是以这个路径作为当前目录取相对路径来确定安装目标位置。</p><p>CMake搜索配置文件时也会以一系列路径作为前缀路径来确定配置文件的位置，这些前缀路径存储在<code>CMAKE_PREFIX_PATH</code>变量中，在Linux中其默认值为<code>/usr</code>，通过包管理器安装的C&#x2F;C++库往往都会将配置文件放到<code>/usr/lib/cmake</code>下面包名对应的文件夹中，这些可以被CMake默认搜索到。当我们使用其他方法将库安装到非标准位置时，只需要将其Prefix附加到变量<code>CMAKE_PREFIX_PATH</code>中。</p><p>比如我们从源码编译一些库并安装到<code>~/.local</code>下（安装时指定<code>CMAKE_INSTALL_PREFIX</code>为<code>~/.local</code>），要调用时就可以将<code>~/.local</code>添加到<code>CMAKE_PREFIX_PATH</code>中，一般不建议在<code>CMakeLists.txt</code>中直接硬编码，而是通过设置环境变量或在预设文件中设置该变量，又或者调用cmake时手动传入。</p><p>该模式下，允许包将自己分为多个组件(Component)，一个组件相当于一个子包，其中也可以包含多个目标。一个组件共享一个配置文件，便于选择性安装和包配置。</p><blockquote><p>组件不等于目标，一个组件可能含有多个目标，上面Qt6::Gui是Qt6 Gui组件中的一个目标，虽然同名，但不是一个概念。一些比较小的库往往不使用组件，这时候如果以为组件就是目标而擅自在<code>find_package</code>中加上<code>COMPONENTS target REQUIRED</code>可能会导致找不到组件而报错。</p></blockquote><p>一个包往往会有一个以包名命名的名称空间，引用包中一个目标一般可以通过<code>PackageName::Component</code>实现。而如果包中添加了组件，那么每一个组件可能也有各自名称空间，但也有可能设置为和包共用名称空间。这由库的开发者决定。</p><p>上面的CMakeLists.txt中使用了<code>find_package</code>函数来查找Qt6，便是使用了配置模式。Qt6是个庞大的框架，我们使用其中四个组件，要确保这些组件被找到的方式便是加上<code>COMPONENTS component1 ...</code>，这些组件如果有一个没有找到，则视为这个包没有找到。最后的<code>REQUIRED</code>表示这是必须的，找不到无法完成配置。</p><h3 id="控制语句"><a href="#控制语句" class="headerlink" title="控制语句"></a>控制语句</h3><p>这是<code>msutils</code>模块的CMakeLists.txt的后半部分内容</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">if</span>(<span class="hljs-variable">$&#123;CMAKE_CXX_COMPILER_ID&#125;</span> <span class="hljs-keyword">STREQUAL</span> <span class="hljs-string">&quot;MSVC&quot;</span>)<br>    <span class="hljs-keyword">target_compile_options</span>(msutils PUBLIC /W4)<br><span class="hljs-keyword">elseif</span>(<span class="hljs-variable">$&#123;CMAKE_CXX_COMPILER_ID&#125;</span> <span class="hljs-keyword">STREQUAL</span> <span class="hljs-string">&quot;GNU&quot;</span>)<br>    <span class="hljs-keyword">target_compile_options</span>(msutils PUBLIC -Wall -Wextra -Wpedantic)<br><span class="hljs-keyword">elseif</span>(<span class="hljs-variable">$&#123;CMAKE_CXX_COMPILER_ID&#125;</span> <span class="hljs-keyword">STREQUAL</span> <span class="hljs-string">&quot;Clang&quot;</span>)<br>    <span class="hljs-keyword">target_compile_options</span>(msutils PUBLIC -Wall -Wextra -Wpedantic)<br><span class="hljs-keyword">endif</span>()<br></code></pre></td></tr></table></figure><p>由于C&#x2F;C++有众多编译器，每个编译器支持的编译选项也不同，因此我们通常需要根据编译器的不同来选择不同的编译参数。上面部分就使用了if语句来分情况添加编译选项。</p><p>CMake的if语句写法大致如上，通过<code>STREQUAL</code>函数来比较两个字符串是否相同，内置变量<code>CMAKE_CXX_COMPILER_ID</code>表示C++使用的编译器名字。具体列表见<a href="https://cmake.com.cn/cmake/help/latest/variable/CMAKE_LANG_COMPILER_ID.html">官网</a>。</p><p>我们还可以像<code>qt_ui</code>的CMakeLists.txt中那样通过<code>CMAKE_SYSTEM_NAME</code>变量判断操作系统（交叉编译的话，该值应是目标系统）。具体列表见<a href="https://cmake.com.cn/cmake/help/latest/variable/CMAKE_SYSTEM_NAME.html">官网</a>。</p><h3 id="编译选项-compile-option"><a href="#编译选项-compile-option" class="headerlink" title="编译选项(compile option)"></a>编译选项(compile option)</h3><p>在编译时，编译器提供了很多选项（options），我们可以通过CMake为目标设置特定的选项。</p><p>我们使用<code>target_compile_options</code>函数来设置编译选项。语法上与之前介绍的两个没有什么区别，只是后面的列表中是编译器的参数，无需使用字符串，直接写出来空格隔开。</p><p>上面我们便为<code>msutils</code>设置了编译时打印所有warnings的选项。</p><p>在<code>msutils</code>中我们使用了<code>PUBLIC</code>作用域，因此链接了该静态库的其他两个可执行文件也都会开启这些选项。</p><h3 id="安装-install"><a href="#安装-install" class="headerlink" title="安装(install)"></a>安装(install)</h3><p>CMake要配置安装一般使用<code>install</code>命令，该命令可以安装多种对象，本文将介绍以下这些。</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">install</span>(TARGETS &lt;<span class="hljs-keyword">target</span>&gt;... [...])<br><span class="hljs-keyword">install</span>(IMPORTED_RUNTIME_ARTIFACTS &lt;<span class="hljs-keyword">target</span>&gt;... [...])<br><span class="hljs-keyword">install</span>(&#123;FILES | PROGRAMS&#125; &lt;<span class="hljs-keyword">file</span>&gt;... [...])<br><span class="hljs-keyword">install</span>(DIRECTORY &lt;dir&gt;... [...])<br></code></pre></td></tr></table></figure><p>安装命令较为复杂，但大多都是写开发库和框架时需要的，本文中不会涉及，因此删去大部分参数，只保留少数常用的参数。</p><p>比如所有的安装命令都有指定文件访问权限的参数，本文中都将其删去。要了解其详细用法，请参考<a href="https://cmake.com.cn/cmake/help/latest/command/install.html">官网文档</a></p><h4 id="目标安装"><a href="#目标安装" class="headerlink" title="目标安装"></a>目标安装</h4><p>这是最常用到的命令，将目标安装到指定路径下。路径若使用相对路径，则相对<code>CMAKE_INSTALL_PREFIX</code>变量的值，该值在linux系统下一般默认为<code>/usr</code>或<code>/usr/local</code>。</p><p>基础语法如下</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">install</span>(TARGETS &lt;<span class="hljs-keyword">target</span>&gt;...<br>        [&lt;artifact-<span class="hljs-keyword">option</span>&gt;...]<br>        [&lt;artifact-kind&gt; &lt;artifact-<span class="hljs-keyword">option</span>&gt;...]...<br>        )<br></code></pre></td></tr></table></figure><p>其中<code>&lt;artifact-option&gt;</code>包含很多内容，我们主要使用<code>DESTINATION &lt;dir&gt;</code>用来指定输出目录。但对于一般的静态库、动态库或可执行文件，这并不是必要的，因为一般默认值就是推荐值。</p><p>其中<code>&lt;artifact-kind&gt;</code>表示目标中对象的类型，亦包括很多种，但我们主要介绍三种：</p><ol><li><code>ARCHIVE</code>：静态库或动态库的导入库。</li><li><code>LIBRARY</code>：动态库（除了DLL）。</li><li><code>RUNTIME</code>：可执行文件或DLL。</li></ol><p>第一次设置是为所有类型设置。第二次设置是为指定类型单独设置选项。</p><p><code>qt_ui</code>中便使用此命令安装<code>MineSweeper-qt</code>目标，并指定了输出目录为bin（实际上并不需要指定，因为这就是默认值）。</p><p>Windows下动态库默认安装到bin，和可执行文件在同一个目录，因此通常不会有找不到动态库的问题。在linux默认安装到lib，如果安装到非标准位置，你需要将该位置下的lib文件夹加入到ld的搜索路径中，才能保证动态库被正确调用。</p><h4 id="动态库依赖安装"><a href="#动态库依赖安装" class="headerlink" title="动态库依赖安装"></a>动态库依赖安装</h4><p>我们的程序若使用了动态库，且该动态库没有安装到系统全局中，我们可能需要在安装自己的程序时，将其依赖的动态库也安装到对应位置上。</p><p>如果这个动态库是我们项目中的一个目标，自然很简单，直接将其作为目标安装即可。但如果是通过<code>find_package</code>导入的第三方库，是无法通过这个方法安装的。</p><p>比如上面<code>qt_ui</code>在windows下时，由于一般windows下面安装qt都不会将qt安装到系统全局中，所以我们必须在安装程序的同时安装其依赖的qt相关动态库文件。</p><p>CMake提供了相应的安装命令，其基础语法如下</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">install</span>(IMPORTED_RUNTIME_ARTIFACTS &lt;<span class="hljs-keyword">target</span>&gt;...<br>        [[LIBRARY|RUNTIME|FRAMEWORK|BUNDLE]<br>         [DESTINATION &lt;dir&gt;]<br>        ] [...]<br>        )<br></code></pre></td></tr></table></figure><p>整体与安装目标的命令一样，该命令用于安装运行时工件，最常用的便是用于安装动态库依赖（该命令不会安装动态库的导入库）。</p><p>上面<code>qt_ui</code>便通过此命令安装了qt6相关动态库。</p><h4 id="文件安装"><a href="#文件安装" class="headerlink" title="文件安装"></a>文件安装</h4><p>如果要安装一些文件，可以使用命令</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">install</span>(&lt;FILES|PROGRAMS&gt; &lt;<span class="hljs-keyword">file</span>&gt;...<br>        TYPE &lt;type&gt; | DESTINATION &lt;dir&gt;<br>        [RENAME &lt;name&gt;])<br></code></pre></td></tr></table></figure><p><code>FILES</code>和<code>PROGRAMS</code>使用相同的形式，<code>PROGRAMS</code>一般用于安装脚本文件，与<code>FILES</code>的区别在于其可以设置执行权限。</p><p>我们应该在指定<code>TYPE</code>和<code>DESTINATION</code>中二选一，若指定了类型，则会根据类型决定安装位置。常用类型有</p><table><thead><tr><th>Type</th><th>Destination</th></tr></thead><tbody><tr><td><code>BIN</code></td><td><code>bin</code></td></tr><tr><td><code>LIB</code></td><td><code>lib</code></td></tr><tr><td><code>INCLUDE</code></td><td><code>include</code></td></tr><tr><td><code>SYSCONF</code></td><td><code>etc</code></td></tr></tbody></table><p>上面<code>qt_ui</code>在windows下将会通过此命令安装Qt6的一些必须的plugins到安装路径中。（可以通过指定环境变量让程序找到这些plugins，但直接自己装上一劳永逸）。</p><h4 id="文件夹安装"><a href="#文件夹安装" class="headerlink" title="文件夹安装"></a>文件夹安装</h4><p>要安装文件夹的基础命令如下</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">install</span>(DIRECTORY dirs...<br>        TYPE &lt;type&gt; | DESTINATION &lt;dir&gt;<br>        [FILES_MATCHING]<br>        [PATTERN &lt;pattern&gt; | REGEX &lt;regex&gt;] [...])<br></code></pre></td></tr></table></figure><p><code>DIRECTORY</code>可以分别设置目录权限和文件权限，并且可以使用匹配机制只安装文件夹下匹配到的文件。</p><p>比如如果我们想要将<code>qt_ui</code>模块下的resources文件夹中的svg图片都安装到指定位置（原项目使用qrc文件，通过rcc工具将资源文件直接打包进可执行文件中）可以这样写</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">install</span>(DIRECTORY resources DESTINATION share/MineSweeper<br>        FILES_MATCHING PATTERN <span class="hljs-string">&quot;*.svg&quot;</span>)<br></code></pre></td></tr></table></figure><h3 id="编译特性-compile-feature"><a href="#编译特性-compile-feature" class="headerlink" title="编译特性(compile feature)"></a>编译特性(compile feature)</h3><p>编译特性是编译器支持的特性，比如C++11、C++14、C++17标准，或者对constexpr、decltype、final的单独支持等。我们可以通过<code>target_compile_features</code>函数来设置编译器特性。</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">target_compile_features</span>(&lt;<span class="hljs-keyword">target</span>&gt; &lt;PRIVATE|PUBLIC|INTERFACE&gt; &lt;feature&gt; [...])<br></code></pre></td></tr></table></figure><p>编译器支持的特性可以通过变量<code>CMAKE_CXX_COMPILE_FEATURES</code>或<code>CMAKE_C_COMPILE_FEATURES</code>获取。所有的已知的特性都可以通过<a href="https://cmake.com.cn/cmake/help/latest/prop_gbl/CMAKE_CXX_KNOWN_FEATURES.html"><code>CMAKE_CXX_KNOWN_FEATURES</code></a>或<a href="https://cmake.com.cn/cmake/help/latest/prop_gbl/CMAKE_C_KNOWN_FEATURES.html"><code>CMAKE_C_KNOWN_FEATURES</code></a>来查询。</p><p>比如要为<code>msutils</code>启用C++20的特性，可以这样写<code>target_compile_features(msutils PRIVATE cxx_std_20)</code>。</p><p>单个特性主要是C++11和C++17的，为其单独设置在现在已经没有太大的必要。因此这个命令主要用于指定标准。最后实际上是通过传递<code>-std=</code>标志实现。</p><h3 id="编译定义-compile-definition"><a href="#编译定义-compile-definition" class="headerlink" title="编译定义(compile definition)"></a>编译定义(compile definition)</h3><p>编译定义主要指宏的定义。为C&#x2F;C++源文件动态的注入宏的值。基础命令如下</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">target_compile_definitions</span>(&lt;<span class="hljs-keyword">target</span>&gt;<br>  &lt;INTERFACE|PUBLIC|PRIVATE&gt; [items1...]<br>  [&lt;INTERFACE|PUBLIC|PRIVATE&gt; [items2...] ...])<br></code></pre></td></tr></table></figure><p>比如我们可以通过<code>target_compile_definitions(msutils PRIVATE FOO=1)</code>来为<code>msutils</code>添加一个宏定义。该宏定义会在预编译时注入到源文件中。</p><p>也可以使用<code>add_compile_definitions</code>函数来添加。</p><h3 id="属性-property"><a href="#属性-property" class="headerlink" title="属性(property)"></a>属性(property)</h3><p>CMake中属性可以影响到方方面面，从编译到构建过程到测试等都会有影响，CMake的所有信息基本都保存在各种对象的属性中。属性分为多种，有全局属性、目录属性、目标属性测试属性、源文件属性等等。</p><p>比如我们通过各种命令为目标添加的包含目录，最终都会被存储到目标的<code>INCLUDE_DIRECTORIES</code>属性中去。链接的库都会被记录到<code>LINK_LIBRARIES</code>属性中去。</p><p>还有一些其他的，起到特定作用的属性。比如上面<code>qt_ui</code>中当操作系统是windows时，会将<code>MineSweeper-qt</code>的<code>WIN32_EXECUTABLE</code>属性设为<code>TRUE</code>。这个属性为TRUE时会为程序构建一个带有WinMain入口的可执行文件，这使得其成为GUI程序而不是控制台程序。</p><p>读取和设置单个属性的命令为<code>get_property()</code>和<code>set_property()</code>。或者使用<code>set_target_properties</code>, <code>set_source_files_properties</code>, <code>set_tests_properties</code>, <code>set_directory_properties</code>为单个目标、源文件、测试、目录设置属性。将<code>set</code>改为<code>get</code>即对应的读取属性的命令。</p><p>这里只介绍最为常用的设置目标属性，其语法如下。</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">set_target_properties</span>(&lt;targets&gt; ...<br>                      PROPERTIES &lt;prop1&gt; &lt;value1&gt;<br>                      [&lt;prop2&gt; &lt;value2&gt;] ...)<br></code></pre></td></tr></table></figure><p>全部的属性列表见<a href="https://cmake.com.cn/cmake/help/latest/manual/cmake-properties.7.html#properties-on-targets">官网</a>。其他命令的具体用法可自行到<a href="https://cmake.com.cn/cmake/help/latest/manual/cmake-commands.7.html">CMake官网</a>查询。</p><h3 id="输出信息"><a href="#输出信息" class="headerlink" title="输出信息"></a>输出信息</h3><p>有时我们想要在CMake的输出中查看一些信息，可以使用<code>message</code>命令。该命令可以输出一些信息到终端中，语法如下</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">message</span>([&lt;mode&gt;] <span class="hljs-string">&quot;message text&quot;</span> ...)<br></code></pre></td></tr></table></figure><p><code>&lt;mode&gt;</code>中可以指定输出的类型，常用的有<code>STATUS</code>、<code>WARNING</code>、<code>AUTHOR_WARNING</code>、<code>SEND_ERROR</code>、<code>FATAL_ERROR</code>、<code>VERBOSE</code>等。具体列表见<a href="https://cmake.com.cn/cmake/help/latest/command/message.html">官网</a>。</p><p>比如我们想要查看<code>MineSweeper-qt</code>的<code>LINK_LIBRARIES</code>属性的值，可以这样写</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">get_property</span>(ms_qt_link_libs <span class="hljs-keyword">TARGET</span> MineSweeper-qt PROPERTY <span class="hljs-keyword">LINK_LIBRARIES</span>)<br><span class="hljs-keyword">message</span>(VERBOSE <span class="hljs-string">&quot;LINK_LIBRARIES: $&#123;ms_qt_link_libs&#125;&quot;</span>)<br></code></pre></td></tr></table></figure><p>生成构建系统时加上参数<code>--log-level VERBOSE</code>，cmake就会输出上面的信息。如果没有输出，删除原本的构建系统重新运行再看看。</p><p>输出结果中应有这一行</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">-- LINK_LIBRARIES: msutils;Qt6::Core;Qt6::Gui;Qt6::Widgets;Qt6::Svg<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>以上就是本文全部内容。目前介绍了如何使用CMake进行更细致的配置，如何使用第三方库，如何将程序安装到目标位置等等。</p><p>但如果想要开发的不是一般工程，而是开发库，甚至是大型的框架，这些显然是不够的。</p><p>但我掌握的内容暂时就到这里了，我也正在学习中，后续随着我的学习，可能还会再写一些专题教程。</p>]]>
    </content>
    <id>https://chillywall.github.io/ChillyBlog/2025/05/15/CMake%E8%BF%9B%E9%98%B6%E6%95%99%E7%A8%8B/</id>
    <link href="https://chillywall.github.io/ChillyBlog/2025/05/15/CMake%E8%BF%9B%E9%98%B6%E6%95%99%E7%A8%8B/"/>
    <published>2025-05-15T08:00:00.000Z</published>
    <summary>
      <![CDATA[<p>上一篇文章<a href="/ChillyBlog/2025/05/11/CMake%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B">CMake基础教程</a>中介绍了CMake的一些基础用法，本文是该系列的进阶教程，将会介绍一些CMake的进阶用法，帮助我们更好地管理更复杂的项目。</p>]]>
    </summary>
    <title>CMake进阶教程</title>
    <updated>2026-04-10T05:07:21.884Z</updated>
  </entry>
  <entry>
    <author>
      <name>郭金锋</name>
    </author>
    <category term="Languages" scheme="https://chillywall.github.io/ChillyBlog/categories/Languages/"/>
    <category term="CMake" scheme="https://chillywall.github.io/ChillyBlog/categories/Languages/CMake/"/>
    <category term="CMake" scheme="https://chillywall.github.io/ChillyBlog/tags/CMake/"/>
    <category term="C/C++" scheme="https://chillywall.github.io/ChillyBlog/tags/C-C/"/>
    <content>
      <![CDATA[<p>CMake 是一个跨平台的开源构建工具，用于自动化编译、测试和安装软件项目。本文将介绍CMake的基础部分。</p><span id="more"></span><h2 id="什么是CMake"><a href="#什么是CMake" class="headerlink" title="什么是CMake"></a>什么是CMake</h2><p>CMake 是一个跨平台的开源构建工具，用于自动化编译、测试和安装软件项目。</p><p>在C&#x2F;C++构建过程中，我们知道必须要有一个编译器。但在实际应用中，除了编译器之外，我们往往还需要构建工具，甚至元构建工具来构建我们的项目。</p><p>要了解这些概念我们首先需要了解C&#x2F;C++语言的编译过程。</p><h3 id="C-C-的编译过程"><a href="#C-C-的编译过程" class="headerlink" title="C&#x2F;C++的编译过程"></a>C&#x2F;C++的编译过程</h3><p>C&#x2F;C++的编译过程通常如下：</p><pre><code class=" mermaid">flowchart LR    A[源代码] --&gt; |预处理| B[处理后源代码]    B --&gt; |编译器编译| C[汇编]    C --&gt; |汇编器编译| D[目标文件]    D --&gt; E[链接器]    F[其他目标文件] --&gt; E    G[库文件] --&gt; E    E --&gt; |链接| H[可执行文件或库文件]</code></pre><h4 id="预处理"><a href="#预处理" class="headerlink" title="预处理"></a>预处理</h4><p>预处理阶段会展开所有的宏，将头文件粘贴到<code>include</code>语句位置。</p><p>以GCC编译器为例，预处理的命令为</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">gcc -E input.c -o output.i<br></code></pre></td></tr></table></figure><h4 id="编译"><a href="#编译" class="headerlink" title="编译"></a>编译</h4><p>经预处理的源代码会被编译器编译成汇编语言。汇编语言经汇编器生成目标文件。</p><p>现在市面上主流的编译器有三个，GCC, Clang, MSVC。</p><p>其中我们最常用的是GCC，其原本用于linux系统，移植到到Windows上则是MinGW。MSVC是微软开发的编译器，仅用于Windows系统，也就是Visual Studio使用的编译器。Clang是LLVM项目的编译器，最初用于Mac OS X，现已移植到Windows和Linux上。</p><p>以GCC编译器为例，生成目标文件的命令为</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">gcc -c input.c -o output.o<br></code></pre></td></tr></table></figure><p>Linux系统下的目标文件后缀为<code>.o</code>，Windows系统下的目标文件后缀为<code>.obj</code>。</p><h4 id="链接"><a href="#链接" class="headerlink" title="链接"></a>链接</h4><p>原则上我们需要单独编译每一个源文件，并生成一个单独目标文件，这个过程中实际上是独立的，也就是说编译的过程中，各个源文件是感知不到其他源文件的。</p><p>我们通常在头文件声明变量、函数或类等，但是其定义可能分散在数个源文件中，那么每个源文件中的内容大概率是不完整的。比如在该源文件中调用的函数其定义在另一个源文件中，在编译时，只要有声明，编译器便不会报错，不会在意某个函数的定义是否存在。</p><p>链接器的作用就是将这些目标文件链接在一起，生成一个可执行文件或库文件。这个阶段，链接器会为各个目标文件中声明的变量、函数等等在所有目标文件中寻找定义，最终链接成一个完整的可执行文件或库文件。</p><p>以GCC编译器为例，链接几个目标文件的命令为</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">gcc a.o b.o -o c<br></code></pre></td></tr></table></figure><p>通常我们想要编译几个源文件时，可以这样写命令</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">gcc a.c b.c c.c -o d<br></code></pre></td></tr></table></figure><p>当项目非常小且没有什么依赖项时，这样似乎足够了。但是当项目变得比较庞大，我们要编译的源文件从几个变成几十个，要生成的东西从一个可执行文件变成数个库文件和数个可执行文件，那么一个一个敲命令来编译就显得效率很低而且易错、不易维护。</p><p>要解决这样的问题，很多人就会说了，写一个shell脚本不就好了。这正是下面一节要说的。</p><h3 id="构建工具"><a href="#构建工具" class="headerlink" title="构建工具"></a>构建工具</h3><p>什么是构建工具，构建工具是用于构建C&#x2F;C++项目的工具（当然也可以是别的语言），运行一种特定的脚本文件来构建项目。</p><p>比如Linux上的make，通过运行Makefile来构建项目。我们只需要编写Makefile,而编写Makefile要比我们手写普通shell要方便得多，因为它有有更好的可拓展性、可维护性。</p><p>这就使得我们编译大型项目也能得心应手，只需要写好Makefile，后续构建只需要一行命令即可。</p><p>但是问题是，构建工具没有统一的标准，堪称百花齐放，各自与各自的平台深度绑定，往往互不兼容，导致难以迁移。</p><p>比如MSVC自带的构建系统NMake，仅在Windows下面可以用。</p><p>虽然也有像Ninja这样的跨平台构建系统，但还有一个问题是，这类脚本虽然比直接敲命令方便太多，但是也算不上好写，相当繁琐。</p><p>于是另一类工具诞生了，即下一节要介绍的元构建工具。</p><h3 id="元构建工具"><a href="#元构建工具" class="headerlink" title="元构建工具"></a>元构建工具</h3><p>元构建工具是一种用于生成构建脚本或配置文件的工具，它位于构建系统的更高层次，用于自动化构建过程的配置和管理。</p><p>我们用元构建工具写一个配置文件，根据这个配置文件元构建工具可以生成指定构建工具的构建脚本再调用对应构建工具来构建项目。</p><p>使用元构建系统，配置可读性更高、更易维护，功能也更加强大。</p><p>当前主流的元构建工具便是CMake，当然也有其他一些元构建工具，比如xmake和bazel。但是目前CMake仍可以说是事实上的标准。使用的更广泛。</p><p>说了这么多，我们终于可以进入正题了，下面将介绍CMake的使用方法。</p><h2 id="CMake工作流"><a href="#CMake工作流" class="headerlink" title="CMake工作流"></a>CMake工作流</h2><p>这一节将介绍使用CMake的工作流。</p><p>以我自己写的一个简单小项目<a href="https://github.com/ChillyWall/MineSweeper.git">MineSweeper</a>为例子，读者可以将该项目clone下来，尝试根据下面的介绍将其编译出来。</p><p>如果读者要使用该项目，请先根据该项目中的README.md文件先安装依赖。</p><h3 id="编写CMakeLists-txt"><a href="#编写CMakeLists-txt" class="headerlink" title="编写CMakeLists.txt"></a>编写CMakeLists.txt</h3><p>CMakeLists.txt是CMake的配置文件，这个名字是固定的，是CMake的配置文件，虽然后缀是txt但实际上是一个脚本，使用一个独立的语言（它甚至是图灵完备的）。</p><p>后文中会介绍这个文件如何写。现在我们在该项目的根目录中可以看到一个CMakeLists.txt文件。我们要通过它构建这个项目。</p><h3 id="生成构建系统"><a href="#生成构建系统" class="headerlink" title="生成构建系统"></a>生成构建系统</h3><p>接下来我们要做的是生成构建系统。即通过cmake生成指定的构建系统的配置文件。</p><p>CMake可以生成多种构建系统，构建工具在cmake中被称为生成器generator，在linux下默认的为<code>Unix Makefiles</code>。推荐使用<code>Ninja</code>，不过需要单独安装</p><p>命令格式为<code>cmake path</code>，path指我们最外层CMakeLists.txt所在的目录。这会在我们当前目录下输出生成的构建系统。一般不推荐在项目根目录直接运行这个命令，不然生成结果会与源码混合，不利于管理。我们一般专门创建一个文件夹用来放生成结果。一般命名为build或out。</p><p>我们可以通过参数<code>-B</code>指定输出目录，<code>-S</code>指定源代码目录。我们也可以通过<code>-G</code>指定生成器。</p><p>我们也可以传递一些cmake变量的值，比如后面部署安装这一步默认安装到系统全局中（linux下即<code>/usr</code>下面），如果我们不希望这样，而是希望安装到我们自己的用户目录下的<code>.local</code>下，我们可以传递<code>CMAKE_INSTALL_PREFIX</code>变量来实现。语法为在变量名前加<code>-D</code>（无空格），在后面加<code>=</code>后面跟变量值。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">cmake -S . -B build -G Ninja -DCMAKE_INSTALL_PREFIX=/home/<span class="hljs-variable">$USER</span>/.local<br></code></pre></td></tr></table></figure><p>执行完成之后，我们会在当前目录下看到一个<code>build</code>文件夹，里面就是cmake生成的构建系统。如果你使用的是<code>Unix Makefiles</code>, 那么你将在里面看到一个<code>Makefile</code>文件。如果你使用的是Ninja，就会在里面看到<code>build.ninja</code>文件。这些便是对应构建系统的构建脚本。</p><h3 id="构建"><a href="#构建" class="headerlink" title="构建"></a>构建</h3><p>之后的步骤就是调用构建工具来构建项目了。一般需要配置的在此之前都已完成，我们只需要运行</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">cmake --build build<br></code></pre></td></tr></table></figure><p><code>--build</code>后面跟的是我们在上一步中指定的输出目录。</p><p>你也可以调用对应构建系统的构建工具来运行。比如<code>Makefile</code>可以直接调用make命令。</p><p>完成之后进入<code>build</code>文件夹就可以看到编译结果。一般没有在CMakeLists.txt中指定输出目录的话，输出结果的目录结构与源代码的结构是对应的（结构由CMakeLists.txt路径决定）。</p><p>比如在上面那个项目中，build中会有一个src文件夹，src下面有<code>msutils</code>，<code>qt_ui</code>，<code>ui</code>三个文件夹，这三个文件下面就是三个对应的CMakeLists.txt中配置的动态库文件和可执行文件。</p><h3 id="部署安装"><a href="#部署安装" class="headerlink" title="部署安装"></a>部署安装</h3><p>构建完成之后，如果想要把构建结果安装到我们自己的系统中，就可以使用cmake提供的安装功能（需要在CMakeLists.txt中写相应的配置，不然没法用）将编译出的结果（不仅仅是二进制文件）安装到指定的目录中。</p><p>默认的安装目录是<code>/usr</code>或<code>/usr/local</code>（linux），我们也可以通过<code>CMAKE_INSTALL_PREFIX</code>变量来指定安装目录。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">cmake --install build<br></code></pre></td></tr></table></figure><p><code>build</code>是我们在之前指定的输出目录。</p><p>或者你也可以调用对应构建系统的构建工具来运行，比如<code>Makefile</code>可以使用<code>make install</code>来安装。</p><p>如果安装的是某些开发库，那么不仅仅二进制文件，可能还会有头文件或者CMake库配置文件（帮助CMake加载库的配置文件）被安装。</p><h2 id="项目结构示例"><a href="#项目结构示例" class="headerlink" title="项目结构示例"></a>项目结构示例</h2><p>上一节我们试着使用cmake编译运行了一个项目，现在我们将要尝试分析这个项目的结构和管理方式。</p><p>本节将会提供一种组织项目目录的结构，并结合上文给出的项目作讲解。</p><h3 id="模块分离"><a href="#模块分离" class="headerlink" title="模块分离"></a>模块分离</h3><p>根目录下放置一个顶层CMakeLists.txt，和一个<code>src</code>文件夹，我们项目的代码分模块放在<code>src</code>下面。</p><p>每个模块单独占一个文件夹，每个文件夹下放置一个CMakeLists.txt，用于本模块的配置。这样我们就隔离了模块之间的配置，防止互相干扰，便于管理。</p><p>比如这个项目的src文件夹下面有三个模块， <code>msutils</code>，<code>ui</code>，<code>qt_ui</code>。其中<code>msutils</code>是一个静态库，提供扫雷的底层处理。<code>ui</code>和<code>qt_ui</code>分别是基于Qt和终端编写的界面，为可执行文件。</p><h3 id="头文件与源文件分离"><a href="#头文件与源文件分离" class="headerlink" title="头文件与源文件分离"></a>头文件与源文件分离</h3><p>C&#x2F;C++项目中，我们一般将头文件与源文件进行分离，分别放入<code>include</code>，<code>src</code>文件夹中，便于管理。</p><h2 id="CMake的基本使用"><a href="#CMake的基本使用" class="headerlink" title="CMake的基本使用"></a>CMake的基本使用</h2><p>接下来本节将会以上面项目为例讲解CMake的基本使用。</p><h3 id="基础设置"><a href="#基础设置" class="headerlink" title="基础设置"></a>基础设置</h3><p>首先是项目的一些基本设置，比如要求的cmake版本、项目名称、使用的语言、使用的标准等等。</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">cmake_minimum_required</span>(VERSION <span class="hljs-number">3.16</span>)<br><span class="hljs-keyword">project</span>(<br>    MineSweeper<br>    VERSION <span class="hljs-number">1.0</span><br>    DESCRIPTION <span class="hljs-string">&quot;A MineSweeper game written in C++ with Qt6&quot;</span><br>    LANGUAGES CXX<br>)<br><span class="hljs-keyword">set</span>(CMAKE_CXX_STANDARD <span class="hljs-number">17</span>)<br><br><span class="hljs-keyword">add_subdirectory</span>(src/msutils)<br><span class="hljs-keyword">add_subdirectory</span>(src/ui)<br><span class="hljs-keyword">add_subdirectory</span>(src/qt_ui)<br></code></pre></td></tr></table></figure><p>上面第一行要求cmake的版本至少要是3.16。</p><p>第二行<code>project</code>函数接受的第一个参数表示项目名称，这个是必须的，之后所有内容都是可选的。</p><p>如果想要指定当前项目的版本，可以加上<code>VERSION major.minor.patch.tweak</code>，当然实际使用中版本更迭除了major都是可选的，根据实际更迭去写就好。比如这个项目只是象征性的写了一个1.0。</p><p>如果想要给项目添加一点描述，那么就是加上<code>DESCRIPTION &quot;your description&quot;</code>。</p><p>后面如果想要指定使用的语言，比如C或C++或都有，都有可以添加<code>LANGUAGES C CXX</code>，只用一个就只填那个就行。</p><p>这些在函数参数列表中使用的全大写的关键字只是用来划分参数的，没有实际意义。</p><p>第三行，用来指定C++的标准，同理也可以指定C的标准，<code>set(CMAKE_C_STANDARD 11)</code>。</p><h3 id="变量"><a href="#变量" class="headerlink" title="变量"></a>变量</h3><p>CMake的脚本语言中也有变量的概念。上面的<code>set</code>函数就是用来设置变量的，CMake中所有的变量都是全局的，除非你在函数中使用<code>set(VAR value PARENT_SCOPE)</code>来设置父作用域的变量。</p><p>赋值语句也很简单，<code>set(VAR VAL)</code>，VAR是变量名，VAL是变量值。变量值可以是字符串、数字、列表等。如果是列表，元素之间只需要空格隔开即可。</p><p>要对变量取值，使用<code>${VAR}</code>即可。</p><p>其实CMake的变量其实本质上都是简单的字符串替换，和shell脚本类似。</p><h3 id="添加子文件夹"><a href="#添加子文件夹" class="headerlink" title="添加子文件夹"></a>添加子文件夹</h3><p>其后的三行使用<code>add_subdirectory(path)</code>函数添加子文件夹，path是子文件夹的路径。这个函数会在path下查找CMakeLists.txt文件并执行。你需要确保path文件夹下面存在一个CMakeLists.txt。</p><p>这相当于添加子项目，层数可以无限深。</p><p>子文件夹下的CMakeLists.txt文件会在父文件夹的CMakeLists.txt文件执行完后执行，并继承所有变量。但不同的子项目之间变量是隔离的。</p><h3 id="TOP面向目标编程"><a href="#TOP面向目标编程" class="headerlink" title="TOP面向目标编程"></a>TOP面向目标编程</h3><p>之后我们开始分别分析子模块的CMakeLists.txt文件。在此之前我们要先了解一个重要的概念：Target目标。</p><p>Modern CMake可以说是面向目标编程（Target-oriented programming），而目标实际上就是可执行文件、或是库。Modern CMake的设计理念是将每个可执行文件或库视为一个目标（Target），并通过设置目标的属性和依赖关系来管理构建过程。</p><p>具体要设置的东西，其实无非就是这个目标包括哪些源文件、头文件搜索路径、要链接的库、编译参数、编译选项、安装路径等等，以及一些属性。所以只要我们理清了这些，CMake可以很简单。</p><p>我们现在分析一下该项目的<code>ui</code>和<code>msutils</code>模块的CMakeLists.txt文件。</p><h4 id="executable可执行目标"><a href="#executable可执行目标" class="headerlink" title="executable可执行目标"></a>executable可执行目标</h4><p>我们先来分析<code>ui</code>模块的CMakeLists.txt文件。这是其前三行。</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">add_executable</span>(MineSweeper src/main.cpp src/ui.cpp)<br><span class="hljs-keyword">target_link_libraries</span>(MineSweeper PRIVATE msutils)<br><span class="hljs-keyword">target_include_directories</span>(MineSweeper PRIVATE <span class="hljs-keyword">include</span>)<br></code></pre></td></tr></table></figure><p>第一行命令使用<code>add_executable</code>函数添加一个可执行文件目标，<code>MineSweeper</code>是目标名称，后面是源文件列表。</p><p>这样就创建了一个名为<code>MineSweeper</code>的可执行文件目标，后续以<code>MineSweeper</code>来引用这个目标。</p><h4 id="library库目标"><a href="#library库目标" class="headerlink" title="library库目标"></a>library库目标</h4><p>之后分析<code>msutils</code>的CMakeLists.txt文件。这是其前两行。</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs CMake"><span class="hljs-keyword">add_library</span>(msutils STATIC src/Cell.cpp src/Grid.cpp src/Sweeper.cpp)<br><span class="hljs-keyword">target_include_directories</span>(msutils PUBLIC <span class="hljs-keyword">include</span>)<br></code></pre></td></tr></table></figure><p>第一行命令使用<code>add_library</code>函数添加一个库目标，<code>msutils</code>是目标名称，<code>STATIC</code>表示该库为静态库，若要生成动态库则使用<code>SHARED</code>，后面是源文件列表。</p><p>库目标的创建与可执行文件目标类似，后续以<code>msutils</code>来引用这个目标。其他的语法也差不多。</p><h3 id="包含目录"><a href="#包含目录" class="headerlink" title="包含目录"></a>包含目录</h3><p>有过C&#x2F;C++多文件编程经验的读者都知道，编译器在编译时需要知道头文件的位置，需要设置头文件的搜索路径。</p><p>上面两个目标，我们需要为其添加其各自模块的include文件夹到搜索路径中去。</p><p>注意到<code>msutils</code>模块因为是用于被其他模块使用的模块，所以其头文件放在了<code>include/ms</code>下，这样我们在其他模块中引用时需要写成<code>#include &lt;ms/Cell.hpp&gt;</code>的形式方能调用。这是管理大型项目必要的技巧，虽然以这个示例项目的体量其实并不必要。</p><h4 id="作用域"><a href="#作用域" class="headerlink" title="作用域"></a>作用域</h4><p><code>target_include_directories</code>函数的第一个参数是目标名称，第二个参数是作用域，<code>PUBLIC</code>表示该目录对所有依赖于该目标的目标可见，<code>PRIVATE</code>表示仅对该目标可见，<code>INTERFACE</code>表示仅对依赖于该目标的目标可见。之后是路径列表。</p><blockquote><p>注意：所有路径，包括之前指定源文件的路径，若是写成相对路径的格式都是相对于该命令所在的CMakeLists.txt所在的路径，变量<code>CMAKE_CURRENT_SOURCE_DIR</code>的值便是该路径。</p></blockquote><p>msutils的头文件设置为PUBLIC，因此MineSweeper链接了msutils之后，会将msutils添加的路径自动添加到其自己的头文件搜索路径。若是PRIVATE，则MineSweeper模块中将不会自动添加。若为<code>INTERFACE</code>，则是msutils本身不添加但MineSweeper添加。</p><h4 id="全局添加"><a href="#全局添加" class="headerlink" title="全局添加"></a>全局添加</h4><p>除了使用<code>target_include_directories</code>之外，我们还可以使用<code>include_directories</code>函数。该函数不需要指定target和作用域，会将路径直接添加到其后的所有目标上。</p><p>这并不符合Modern CMake的理念，但是如果你觉得这确实很方便，也不妨使用。</p><h3 id="链接库"><a href="#链接库" class="headerlink" title="链接库"></a>链接库</h3><p>链接库与添加头文件搜索目录类似，使用<code>target_link_libraries</code>和<code>link_libraries</code>即可。参数为库目标列表。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>CMake的基础使用到这里就结束了。本文介绍了CMake的基本概念、工作流、基本使用方法和一些常用函数。读者可以根据自己的需求进行扩展。</p><p>能够熟练使用上面介绍的几个简单函数，就足以使用cmake来管理构建一些简单的没有外部依赖的项目了。</p><p>更深入的部分，比如调用第三方库、编译选项、目标属性、接口库、控制语句、安装等，读者可以关注后续的<a href="/ChillyBlog/2025/05/15/CMake%E8%BF%9B%E9%98%B6%E6%95%99%E7%A8%8B">CMake进阶教程</a>。</p>]]>
    </content>
    <id>https://chillywall.github.io/ChillyBlog/2025/05/11/CMake%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/</id>
    <link href="https://chillywall.github.io/ChillyBlog/2025/05/11/CMake%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/"/>
    <published>2025-05-11T07:00:00.000Z</published>
    <summary>
      <![CDATA[<p>CMake 是一个跨平台的开源构建工具，用于自动化编译、测试和安装软件项目。本文将介绍CMake的基础部分。</p>]]>
    </summary>
    <title>CMake基础教程</title>
    <updated>2026-04-10T05:07:21.884Z</updated>
  </entry>
  <entry>
    <author>
      <name>郭金锋</name>
    </author>
    <category term="Editor" scheme="https://chillywall.github.io/ChillyBlog/categories/Editor/"/>
    <category term="Vim" scheme="https://chillywall.github.io/ChillyBlog/categories/Editor/Vim/"/>
    <category term="Vim" scheme="https://chillywall.github.io/ChillyBlog/tags/Vim/"/>
    <content>
      <![CDATA[<p>本系列教程适合零基础想要快速入手的(neo)vim初学者，同时作为LazyVim教程的前置教程。</p><span id="more"></span><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>很多人对vim的印象是密密麻麻的vim键位图，认为vim指令繁多，难以记忆。但事实上，这些并不需要特别去记忆，我们不需要背完键位表再开始使用，而是在使用的过程中逐渐熟悉我们常用的命令，再根据需要去拓展我们原本不熟悉的命令。</p><p>Vim除了键位繁多之外，给人的印象是，没有一眼就看出怎么用的界面引导，而又不想为了一个操作专门去搜索教程。网上的教程也往往是大肆罗列各种命令，令人头大。</p><p>以及，vim没有IDE那样的完善的集成好的开发环境。</p><p>事实上这些问题确实存在，但又不是完全无法克服。</p><p>vim有很多优秀的插件，可以提供方便又强大的功能，也有一些成熟的、开箱即用的配置框架。依托于LSP的兴起，Vim的编程体验不说比肩IDE，也足以满足大多数人的大多数要求。</p><p>同时vim能带来更高的可拓展性，用户可以根据自己的需要定制自己的编辑器，享受随心所欲的编辑体验。</p><p>本教程包括三个部分，第一部分介绍vim的基础知识，第二部分介绍LazyVim和其中包含的各类插件，第三部分引导读者进行简单的配置。</p><p>本文将介绍vim的基础知识，帮助读者熟悉vim中的一些常见概念。</p><h2 id="如何阅读本文"><a href="#如何阅读本文" class="headerlink" title="如何阅读本文"></a>如何阅读本文</h2><p>本文简单地介绍了一些vim中的常见概念，读者不必牢牢记住所有内容，只需大致理解其逻辑即可。可以在原生的vim中稍作尝试，体验各种操作的细节。</p><p>本文主要为后续教程作铺垫，下面罗列命令，仅仅是为了让读者知道有这么一回事儿，大多数命令都不是刚需的。</p><p>后续教程会引导用户安装配置框架，届时会借助插件获取更方便快捷的体验，会用一些被广泛接受的快捷键替换原生命令。</p><h2 id="模式"><a href="#模式" class="headerlink" title="模式"></a>模式</h2><p>vim的特点是有多个模式，在不同的模式中进行不同的操作，这样可以缓解快捷键不够用的问题，也可以使我们不必像Emacs那样大量的使用Ctrl、Alt键，减少小指的压力。</p><p>vim共计有6中模式，分别为Normal（正常）、Command（命令）、Insert（插入）、Visual（可视），Replace（替换）和Terminal（终端）。。</p><p>该篇主要讲解Normal、Command和Insert模式。</p><h3 id="Normal模式"><a href="#Normal模式" class="headerlink" title="Normal模式"></a>Normal模式</h3><p>Normal模式是我们进入vim后的默认模式。我们仅在有需求时进入其他模式，其余时间皆停留在Normal下。大多数操作都在Normal下完成。</p><p>此时我们输入的按键不会被解读为输入的字符，而是被解读为指令。因此我们不必大量使用Ctrl等按键也可以实现方便的输入指令。</p><p>该模式下我们主要进行移动光标到我们需要的位置以便后续其他操作或者仅仅阅读文字信息。</p><p>Normal模式下我们可以通过简单的按键进行光标的移动，以及一些回退等操作。</p><h4 id="方向键"><a href="#方向键" class="headerlink" title="方向键"></a>方向键</h4><table><thead><tr><th>按键</th><th>操作</th></tr></thead><tbody><tr><td><code>h</code></td><td>左</td></tr><tr><td><code>l</code></td><td>右</td></tr><tr><td><code>j</code></td><td>下</td></tr><tr><td><code>k</code></td><td>上</td></tr></tbody></table><h4 id="跳转"><a href="#跳转" class="headerlink" title="跳转"></a>跳转</h4><table><thead><tr><th>按键</th><th>操作</th></tr></thead><tbody><tr><td><code>$</code></td><td>跳转到行尾</td></tr><tr><td><code>0</code></td><td>跳转到行首</td></tr><tr><td><code>^</code></td><td>跳转到行首非空字符</td></tr><tr><td><code>gg</code></td><td>跳转到buffer第一行</td></tr><tr><td><code>G</code></td><td>跳转到buffer最后一行</td></tr></tbody></table><h4 id="单词跳转"><a href="#单词跳转" class="headerlink" title="单词跳转"></a>单词跳转</h4><table><thead><tr><th>按键</th><th>操作</th></tr></thead><tbody><tr><td><code>w</code>, <code>b</code></td><td>跳转到下一个，上一个单词开头</td></tr><tr><td><code>e</code>, <code>ge</code></td><td>跳转到下一个，上一个单词末尾</td></tr></tbody></table><h4 id="回退，再执行"><a href="#回退，再执行" class="headerlink" title="回退，再执行"></a>回退，再执行</h4><table><thead><tr><th>按键</th><th>操作</th></tr></thead><tbody><tr><td><code>u</code></td><td>回退上一次操作</td></tr><tr><td><code>&lt;C-r&gt;</code></td><td>重新执行上一次操作</td></tr></tbody></table><h4 id="删除"><a href="#删除" class="headerlink" title="删除"></a>删除</h4><table><thead><tr><th>案件</th><th>操作</th></tr></thead><tbody><tr><td><code>d + 跳转命令</code></td><td>删除当前位置到跳转目标之间的内容</td></tr><tr><td><code>dd</code></td><td>删除当前行</td></tr><tr><td><code>x</code></td><td>删除光标下文字</td></tr></tbody></table><h4 id="搜索"><a href="#搜索" class="headerlink" title="搜索"></a>搜索</h4><p>Normal模式下，可以进行搜索，按<code>/</code>后可以开始输入搜索内容，输入完后按下回车执行，可以使用按键快速在搜索结果之间跳转。 支持正则表达式。</p><table><thead><tr><th>按键</th><th>操作</th></tr></thead><tbody><tr><td><code>n</code>, <code>N</code></td><td>跳转到下一个，上一个搜索结果</td></tr></tbody></table><h3 id="Command模式"><a href="#Command模式" class="headerlink" title="Command模式"></a>Command模式</h3><p>vim除了按下按键发送指令外，还可以直接输入命令来执行。vim有一套自己专门的脚本语言vimscript，我们在命令模式下输入的便是这种语言，但我们一般只需要输入最简单的命令即可。</p><p>在Normal模式要输入命令，只需要按下<code>:</code>即可进入Command模式，此时我们可以开始输入命令，命令一般会显示在左下角。</p><p>这里先介绍必要的几个命令</p><table><thead><tr><th>命令</th><th>含义</th></tr></thead><tbody><tr><td><code>:q</code></td><td>退出</td></tr><tr><td><code>:w</code></td><td>写入，即保存</td></tr><tr><td><code>:open filename</code></td><td>打开文件，若不存在则创建</td></tr></tbody></table><p>在<code>:</code>和命令之间加<code>!</code>表示在终端中运行。在命令后加<code>!</code>表示强制执行。</p><p>在<code>q</code>和<code>w</code>之后加字母<code>a</code>表示all，即退出全部、保存全部的意思。</p><p>在退出时，可能会询问是否要保存内容，若不想保存可以可以使用<code>:q!</code>强制退出。</p><h3 id="Insert模式"><a href="#Insert模式" class="headerlink" title="Insert模式"></a>Insert模式</h3><p>Insert模式是进行字符输入的模式，在Insert模式下大多数快捷键失效，输入的按键会被解读为字符，与一般的文本编辑器无异。</p><p>从Normal模式切换到Insert模式有很多种方式，我们只需根据自己的习惯掌握对应的方式即可。</p><ol><li>按<code>i</code>，会在当前光标位置之前开始插入字符。比如如果当前光标（一般是方块状）落在这一句的‘比’字上，按<code>i</code>将在‘比’字之前插入字符。</li><li>按<code>a</code>，会在当前光标位置之后开始插入字符。</li><li>按<code>o</code>，会在光标所在行的下一行开始插入。</li><li>按<code>I</code>，会在当前行的开头开始插入。</li><li>按<code>A</code>，会在当前行的结尾开始插入。</li><li>按<code>O</code>，会在当前行的上一行开始插入。</li><li>按<code>c + 跳转命令</code>，和<code>d</code>相似，但会在删除内容后进入insert模式。</li></ol><p>要退出Insert模式回到Normal模式，只需要按下<code>&lt;Esc&gt;</code>即可，或者使用<code>&lt;C-[&gt;</code>也可以。</p><blockquote><p><code>&lt;C-[&gt;</code>是内置快捷键，与<code>&lt;Esc&gt;</code>相同，由于<code>&lt;Esc&gt;</code>一般离手比较远，所以很多人不喜欢，这个快捷键便出现了。</p></blockquote><h3 id="Visual模式"><a href="#Visual模式" class="headerlink" title="Visual模式"></a>Visual模式</h3><p>Visual模式用于选中文本，按<code>v</code>可进入Visual模式，vim会选中进入处到当前光标之间的文本内容。在visual模式下，可以使用normal模式下相同的方式进行光标的移动。</p><p>Visual模式下可以使用一些按键对选中的文本进行操作，一般操作后自动退回Normal模式。</p><table><thead><tr><th>按键</th><th>含义</th></tr></thead><tbody><tr><td><code>y</code></td><td>复制选中内容到寄存器</td></tr><tr><td><code>d</code></td><td>删除选中的内容，并复制到寄存器</td></tr><tr><td><code>p</code></td><td>粘贴当前寄存器的内容覆盖选中内容，再将选中内容复制到寄存器</td></tr><tr><td><code>c</code></td><td>删除选中的内容，并进入Insert模式</td></tr></tbody></table><p>Vim内部有一个独立的寄存器，用于保存在vim中复制的内容，与系统剪贴板并不互通。</p><p>若要与系统剪贴板互通，需要设置寄存器，再使用其他命令进行复制粘贴。本教程不打算涉及这方面，而推荐使用插件来实现vim与系统剪贴板互通。</p><h3 id="Replace模式"><a href="#Replace模式" class="headerlink" title="Replace模式"></a>Replace模式</h3><p>介绍这个模式前先介绍一个与它关系紧密的命令。在Normal模式下，按下<code>r</code>键再输入一个字符，这个字符会替换光标下原本的字符。比如你将光标移动到Normal的N上，按下<code>rn</code>会将N替换为n。</p><p>如果按下<code>R</code>键，会进入Replace模式。在该模式下，你可以想Insert模式下那样输入字符，但是你输入的内容会取代那个位置原本的内容，而不是插入。使用<code>&lt;Esc&gt;</code>键推出该模式。</p><p>比如你可以将光标移动到以上段的Insert的I上，按下<code>R</code>之后，输入<code>Normal&lt;Esc&gt;</code>，Insert就会被Normal覆盖（字符数量一样）。</p><h3 id="Terminal模式"><a href="#Terminal模式" class="headerlink" title="Terminal模式"></a>Terminal模式</h3><p>neovim或新版vim支持内部打开终端。使用<code>:term COMMAND</code>来打开终端，其中<code>COMMAND</code>为要执行的命令，比如<code>bash</code>，<code>zsh</code>等。Terminal模式下，你的操作都会被传递给终端，可以和终端进行交互。</p><p>要退出该模式，输入快捷键<code>&lt;C-\&gt;&lt;C-n&gt;</code>。之后终端页面将会变成一个只读的缓冲区（缓冲区见下节），你可以使用vim的命令去操作，和你操作只读文件没什么不同，你可以自由的浏览和复制其中内容。</p><p>要再进入Terminal模式，只需按下<code>i</code>键即可。</p><h2 id="Vim的组成"><a href="#Vim的组成" class="headerlink" title="Vim的组成"></a>Vim的组成</h2><p>Vim中有Tab（标签页）、Buffer（缓冲区）、Window（窗口）三个概念。</p><p>Vim的高效主要来自于其独特的模式编辑，但也与其Tab、Buffer、Window的紧密结合有关。</p><h3 id="Buffer缓冲区"><a href="#Buffer缓冲区" class="headerlink" title="Buffer缓冲区"></a>Buffer缓冲区</h3><p>Buffer缓冲区用于存储内容，比如我们打开的文件，它的内容会被vim读取并存储在一个buffer中。</p><p>Vim中可以同时打开多个buffer，存储不同的内容，互相独立。我们可以在不同的buffer之间切换，实现多文件同时修改。</p><p>我们也可以将一些其它的文本内容加载到buffer中方便查看，比如当前的目录信息，tags列表，帮助文档等等。</p><h4 id="buffer命令"><a href="#buffer命令" class="headerlink" title="buffer命令"></a>buffer命令</h4><table><thead><tr><th>按键</th><th>含义</th></tr></thead><tbody><tr><td><code>:ls</code></td><td>显示所有buffer</td></tr><tr><td><code>:bp</code></td><td>切换上一个buffer</td></tr><tr><td><code>:bn</code></td><td>切换下一个buffer</td></tr><tr><td><code>:bd</code></td><td>删除buffer，后加索引删除指定buffer，不加删除当前buffer</td></tr></tbody></table><h3 id="Window窗口"><a href="#Window窗口" class="headerlink" title="Window窗口"></a>Window窗口</h3><p>Window窗口用于显示buffer的内容，我们调用退出命令时退出的实际上是当前Window。所有windows被退出后，回到终端。</p><p>我们进入vim时会默认打开一个window，称为主window。</p><p>每个window只能显示一个buffer的内容，因此当我们打开多个文件、有多个buffer之后，只有最后一个buffer的内容会被显示在主window上。</p><p>但我们可以将主window分割为多个小window，实现同时查看编辑多个buffer，或者多个window可以同时显示或编辑同一个buffer不同位置的内容。</p><p>当我们有多个window时，输入指令<code>:q</code>会关闭当前window，我们关闭一个window并不会删除该window正在显示的buffer。</p><p>因此我们使用vim时可以分割一小部分window用于显示一些内容，左侧显示当前目录树、右侧显示tags列表，下部显示终端等等。</p><p>Vim中可见的一切都是buffer和window，因此我们可以使用相同的操作逻辑操作可见的所有内容。这种基于TUI的界面的操作模式与其他基于GUI的编辑器相比，或许显得简陋，但也更具有连贯性，更加随心所欲。</p><p>Vim的编辑键位很多其他编辑器&#x2F;IDE都有不错的插件实现，但往往仅仅局限于文本编辑区，其他部分仍然遵循原本的GUI操作。</p><h4 id="window跳转"><a href="#window跳转" class="headerlink" title="window跳转"></a>window跳转</h4><table><thead><tr><th>按键</th><th>含义</th></tr></thead><tbody><tr><td><code>&lt;C-w&gt;h</code></td><td>跳转到左侧window</td></tr><tr><td><code>&lt;C-w&gt;l</code></td><td>跳转到右侧window</td></tr><tr><td><code>&lt;C-w&gt;j</code></td><td>跳转到下册window</td></tr><tr><td><code>&lt;C-w&gt;k</code></td><td>跳转到上册window</td></tr></tbody></table><h4 id="window分割"><a href="#window分割" class="headerlink" title="window分割"></a>window分割</h4><table><thead><tr><th>按键</th><th>含义</th></tr></thead><tbody><tr><td><code>:split</code></td><td>水平(上下)分割window</td></tr><tr><td><code>:vsplit</code></td><td>垂直(左右)分割window</td></tr></tbody></table><p>在分割后可以添加文件名，会在分割出的新window中打开该文件，否则会显示当前buffer。</p><h3 id="Tab标签页"><a href="#Tab标签页" class="headerlink" title="Tab标签页"></a>Tab标签页</h3><p>Tab就是标签页。Tab保存的是buffer和window的布局信息。</p><p>即window的分割情况，包括window的数量、位置、长宽等，以及每个window中显示哪一个buffer。</p><p>多个tab之间的buffer是互通的，仅仅window布局不同。</p><p>比如我们编辑一个文件时可能打开了很多其他window，我们希望保留这些布局但又需要打开新的文件、新的window，就可以创建新的tab，在不同tab之间可以随意转换，轻松驾驭复杂布局，不用每次都重新打开。</p><p>如果读者熟悉tmux，就会发现两者管理方式有很多共通之处。tmux中一个打开的shell就相当于vim中一个buffer，tmux中的panel则相当于vim中window，tmux的window则相当于vim的tab。不过tmux中的终端实例和panel是</p><p>对于熟悉其他编辑器的用户来说，可能会尝试这样使用vim的tab：在每个tab中分别打开一个文件，切换tab来实现切换文件。这样做是可行的，但并不是惯常用法。</p><p>惯常做法是通过buffer的切换来切换文件，因为切换tab会切换window布局，因此单纯想要切换正在修改的文件没必要切换tab。</p><h4 id="标签页命令"><a href="#标签页命令" class="headerlink" title="标签页命令"></a>标签页命令</h4><table><thead><tr><th>按键</th><th>含义</th></tr></thead><tbody><tr><td><code>:tabs</code></td><td>显示所有标签页</td></tr><tr><td><code>:tabnew</code></td><td>创建新标签页</td></tr><tr><td><code>:tabp</code></td><td>切换上一个标签页</td></tr><tr><td><code>:tabn</code></td><td>切换下一个标签页</td></tr><tr><td><code>:tabc</code></td><td>删除当前标签页</td></tr></tbody></table><h4 id="cwd"><a href="#cwd" class="headerlink" title="cwd"></a>cwd</h4><p>Vim和shell一样有CWD的概念，在vim中运行一些命令，比如<code>:open</code>，需要使用相对路径的，都是根据vim的CWD来的。</p><p>你可以运行<code>:pwd</code>来查看cwd，使用<code>:cd</code>来切换。</p><p>在vim中每个tab的cwd是独立的，不会同步改变。</p><blockquote><p>你可以设置让cwd跟随当前buffer改变，始终让当前buffer中的文件所在目录为cwd。一般不推荐这样做，如果你要运行一些项目级的命令，比如lsp相关，这通常需要知道你的项目根目录。</p></blockquote><h2 id="键位与宏"><a href="#键位与宏" class="headerlink" title="键位与宏"></a>键位与宏</h2><p>Vim的模式编辑使得vim中用户的所有操作都可以通过一系列按键事件表示。Vim中我们可以将一系列按键映射为一个键，按下这个键便相当于按顺序按下被映射到的一系列键。</p><p>因此我们可以直接将输入命令映射为一个键，比如将<code>:bp&lt;CR&gt;</code> (<code>&lt;CR&gt;``表示回车键) 映射到</code>H<code>，这样我们按下Shift + h时，便相当于我们输入了</code>:bp&#96;之后按下回车，实现buffer切换。</p><p>又比如我们可以将<code>jj</code>映射到<code>&lt;ESC&gt;</code>这样我们在Insert模式只需连按两下j就可以退出插入模式。</p><p>有关如何设置快捷键的问题，我们在后续的教程中会涉及。</p><h3 id="Leader键"><a href="#Leader键" class="headerlink" title="Leader键"></a>Leader键</h3><p>Vim中还有一个特殊的按键机制，称为Leader键。Leader键可以被用户指定为哪个键，而在键位映射中使用<code>&lt;leader&gt;</code>指代这个键。</p><p>我们使用vim的过程中会出现快捷键越来越多，按键序列可能会越来越长，而且很多快捷键其实并不常用，如果绑定在字母按键上，容易误触。</p><p>于是我们有了一一个解决方案，就是统一指定一个键作为前缀键，用来存放相对没那么常用的功能。而只将最常用的部分功能映射到普通字母上。</p><p>以Leader开头的快捷键往往是按照键的意义，进行分级分类。比如buffer相关的操作，都以<code>&lt;leader&gt;b</code>开头，相当于这些快捷键都被分到b这一分类中。类似的widnow相关的分到<code>w</code>，tab相关的分到<code>&lt;TAB&gt;</code>，terminal相关的分到<code>t</code>。</p><p>又比如可以将一些常用插件，其相关的快捷键占用一个一级字母前缀，没那么常用的，和几个同类型的占用一个一级字母下的二级字母，或者快捷键数量不多的话混合起来。</p><p>Leader键可以由用户指定值，那么我们就既可以随我们心意去设置，也不必担心某个插件使用的Leader键和我们不同。</p><p>Vim中一般不使用<code>Alt</code>, <code>Ctrl</code>也很少使用，对我们的小指非常友好。</p><h3 id="LocalLeader"><a href="#LocalLeader" class="headerlink" title="LocalLeader"></a>LocalLeader</h3><p>Vim中除了Leader之外还有LocalLeader，特点是仅对当前buffer有效。这常用于某些只在特定场合下使用的快捷键，比如只对某个文件类型有用的快捷键。</p><p>若是有快捷键只在特定的场合下有效，那么就没有必要将其作为全局快捷键，因此另外使用一个键作为此类快捷键的前缀键是合适的。这同时使得同一个快捷键可以在不同场合下映射到不同的操作。</p><p>同时此类快捷键应当是只在特定场合下才加载的，关于如何在特定场合下加载快捷键，或者说加载配置，后面介绍autocmd时会详细介绍。</p><h3 id="宏"><a href="#宏" class="headerlink" title="宏"></a>宏</h3><p>Vim中同样提供宏的功能，并且相当便捷和强大。</p><p>由于在vim中所有操作基本都可以靠一系列按键事件来完成，因此vim中记录宏也就是记录我们按下的按键，在我们使用宏时原样敲一遍。</p><p>我们只需要按<code>q+字母</code>之后敲下一个个按键或者输入命令（快捷键包括以Leader开头的都可以正常使用）这些指令就会被绑定到对应字母上，之后<code>@+字母</code>就可以播放对应字母中绑定的宏。</p><p>被绑定的宏可以在vim中随意使用，甚至可以在不同的buffer中使用，但只会保存至当前vim实例退出。</p><h3 id="重复"><a href="#重复" class="headerlink" title="重复"></a>重复</h3><p>Vim中可以通过在指令前输入数字来将指令重复执行指定次数。比如，按下<code>3j</code>，表示向下移动光标三次，<code>3dd</code>表示删除当前行及其下两行。</p><p>这对于宏也同样适用。比如<code>3@a</code>，表示播放字母a中绑定的宏三次。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>到这里基础篇就已经介绍完毕。读完本篇你应该已经对vim有了大致的了解。在下一篇中，将会介绍LazyVim的安装和配置，届时会介绍上面的操作在LazyVim中如何实现。</p>]]>
    </content>
    <id>https://chillywall.github.io/ChillyBlog/2025/05/08/Vim%E6%95%99%E7%A8%8B-%E5%9F%BA%E7%A1%80%E7%AF%87/</id>
    <link href="https://chillywall.github.io/ChillyBlog/2025/05/08/Vim%E6%95%99%E7%A8%8B-%E5%9F%BA%E7%A1%80%E7%AF%87/"/>
    <published>2025-05-08T16:44:00.000Z</published>
    <summary>
      <![CDATA[<p>本系列教程适合零基础想要快速入手的(neo)vim初学者，同时作为LazyVim教程的前置教程。</p>]]>
    </summary>
    <title>Vim教程-基础篇</title>
    <updated>2026-04-10T05:07:21.884Z</updated>
  </entry>
  <entry>
    <author>
      <name>郭金锋</name>
    </author>
    <category term="Tools" scheme="https://chillywall.github.io/ChillyBlog/categories/Tools/"/>
    <category term="Docker" scheme="https://chillywall.github.io/ChillyBlog/categories/Tools/Docker/"/>
    <category term="Docker" scheme="https://chillywall.github.io/ChillyBlog/tags/Docker/"/>
    <content>
      <![CDATA[<p>Docker是一种虚拟容器应用这使我们可以在性能损失很小的情况下，方便地创建并管理虚拟环境。 本教程涵盖Docker的基础操作，帮助初学者快速上手。</p><span id="more"></span><h2 id="1-安装Docker"><a href="#1-安装Docker" class="headerlink" title="1 安装Docker"></a>1 安装Docker</h2><p>要安装docker需要添加Docker的源，推荐使用清华的<a href="https://mirrors4.tuna.tsinghua.edu.cn/help/docker-ce/">镜像源</a>来安装。</p><p>根据清华镜像站中的使用教程一般顺利完成docker的安装。</p><h2 id="2-配置nvidia-gpu支持"><a href="#2-配置nvidia-gpu支持" class="headerlink" title="2 配置nvidia gpu支持"></a>2 配置nvidia gpu支持</h2><p>要在docker容器中调用nvidia gpu，无需在容器内安装nvidia显卡驱动，而可以令容器调用宿主机的显卡驱动，秩序宿主机正确安装显卡驱动即可。要完成这一点需要安装nvidia提供的nvidia-container-toolkit和nvidia-container-runtime，并正确配置daemon。</p><h3 id="2-1-安装nvidia-container-toolkit和runtime"><a href="#2-1-安装nvidia-container-toolkit和runtime" class="headerlink" title="2.1 安装nvidia-container-toolkit和runtime"></a>2.1 安装nvidia-container-toolkit和runtime</h3><p>首先需要添加相关的仓库源。根据nvidia官方教程，指令如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | <span class="hljs-built_in">sudo</span> gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg &amp;&amp; curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | sed <span class="hljs-string">&#x27;s#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g&#x27;</span> | <span class="hljs-built_in">sudo</span> <span class="hljs-built_in">tee</span> /etc/apt/sources.list.d/nvidia-container-toolkit.list<br></code></pre></td></tr></table></figure><p>之后使用apt安装即可。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> apt update<br><span class="hljs-built_in">sudo</span> apt install -y nvidia-container-toolkit nvidia-container-runtime<br></code></pre></td></tr></table></figure><h3 id="2-2-配置docker-daemon"><a href="#2-2-配置docker-daemon" class="headerlink" title="2.2 配置docker daemon"></a>2.2 配置docker daemon</h3><p>要使用nvidia显卡还需要配置docker daemon的runtime，修改<code>/etc/docker/daemon.json</code>文件，添加如下内容：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;default-runtime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;nvidia&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;runtimes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;nvidia&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>      <span class="hljs-attr">&quot;path&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/usr/bin/nvidia-container-runtime&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;runtimeArgs&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><br>    <span class="hljs-punctuation">&#125;</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>之后重启docker服务即可。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> systemctl restart docker<br></code></pre></td></tr></table></figure><h2 id="3-容器与镜像基础操作"><a href="#3-容器与镜像基础操作" class="headerlink" title="3 容器与镜像基础操作"></a>3 容器与镜像基础操作</h2><p>Docker容器是由Docker镜像创建的运行实例，镜像是一个只读的模板，容器是镜像的运行实例。容器可以被启动、开始、停止、删除、暂停等。</p><p>一个容器相当于一个独立于宿主机的系统环境，我们可以像在宿主机上一样在容器内实现各种操作，比如安装软件、运行程序等等。</p><p>我们可以保存容器的状态成一个镜像，也可以运行镜像创建一个与该镜像创建时的容器一模一样的新容器。</p><h3 id="3-1-获取镜像"><a href="#3-1-获取镜像" class="headerlink" title="3.1 获取镜像"></a>3.1 获取镜像</h3><p>要获取镜像文件，有三种方式，第一种是直接拉取云端的镜像或加载被保存到本地的离线镜像文件，第二种是从已存在的容器创建镜像，第三种是通过编写Dockerfile创建镜像。</p><p>拉取镜像的命令为，若运行容器时使用了本地没有的镜像，会先尝试拉取该镜像。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> docker pull image_name<br></code></pre></td></tr></table></figure><h3 id="3-2-查看镜像"><a href="#3-2-查看镜像" class="headerlink" title="3.2 查看镜像"></a>3.2 查看镜像</h3><p>docker镜像名称通常由两部分组成，一个是仓库名，一个是tag，写成<code>repo:tag</code>的形式。比如<code>ubuntu:22.04</code>, <code>ubuntu:latest</code>。</p><p>要查看本地的镜像可以使用下面命令。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> docker images<br></code></pre></td></tr></table></figure><p>输出结果类似下面。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">REPOSITORY                      TAG                   IMAGE ID       CREATED        SIZE<br>osrf/ros                        noetic-desktop-full   f19749f1e3da   5 months ago   3.44GB<br>ubuntu                          20.04                 6013ae1a63c2   5 months ago   72.8MB<br></code></pre></td></tr></table></figure><h3 id="3-3-运行容器"><a href="#3-3-运行容器" class="headerlink" title="3.3 运行容器"></a>3.3 运行容器</h3><p>运行容器需要使用命令<code>run</code>，格式为</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker run [OPTIONS] IMAGE [COMMAND] [ARG...]<br></code></pre></td></tr></table></figure><p>以下列举常用的选项：</p><ul><li><code>-d</code> 表示后台运行</li><li><code>-e ENVVAR=VAL</code> 添加环境变量的值，ENVVAR为环境变量名，VAL为值</li><li><code>--name container_name</code> 为容器指定一个名字</li><li><code>-it</code> 表示交互式运行终端，默认为bash，在[COMMAND]位置可以指定具体命令，如zsh。</li><li><code>-p host_port:container_port</code> 映射端口，host_port为宿主机端口，container_port为容器端口</li><li><code>--device DEVICE_PATH</code> 挂载宿主机设备，是容器可以访问该设备，一般为<code>/dev</code>目录下的</li><li><code>--privileged</code> 给予容器高权限(不太安全，使容器拥有破坏宿主机环境的能力)</li></ul><p>例如，要运行一个ubuntu容器，可以使用下面命令。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> docker run --name ubuntu20 -it ubuntu:20.04 bash<br></code></pre></td></tr></table></figure><h3 id="3-4-查看容器信息"><a href="#3-4-查看容器信息" class="headerlink" title="3.4 查看容器信息"></a>3.4 查看容器信息</h3><p>查看信息可以使用<code>ps</code>命令，格式为</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> docker ps -a<br></code></pre></td></tr></table></figure><p>输出类似下方</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">CONTAINER ID   IMAGE          COMMAND                  CREATED       STATUS                     PORTS     NAMES<br>3c02dd5ef563   mysql:latest   &quot;docker-entrypoint.s…&quot;   12 days ago   Exited (0) 6 days ago                rdnb_mysql<br></code></pre></td></tr></table></figure><h3 id="3-5-从容器运行程序"><a href="#3-5-从容器运行程序" class="headerlink" title="3.5 从容器运行程序"></a>3.5 从容器运行程序</h3><p>对于一个正在运行的容器，我们可以在宿主机中通过<code>exec</code>命令运行容器内的程序。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker <span class="hljs-built_in">exec</span> [OPTIONS] CONTAINER COMMAND [ARG...]<br></code></pre></td></tr></table></figure><p><code>-e</code>, <code>-d</code>, <code>-it</code>依旧可以使用，<code>CONTAINER</code>为容器名称，<code>COMMAND</code>为要运行的命令，<code>ARG</code>为命令的参数。</p><p>例如我们想打开一个容器中的终端</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> docker <span class="hljs-built_in">exec</span> -it ubuntu20 bash<br></code></pre></td></tr></table></figure><h3 id="3-6-其他常用命令"><a href="#3-6-其他常用命令" class="headerlink" title="3.6 其他常用命令"></a>3.6 其他常用命令</h3><ul><li><code>docker start CONTAINER</code> 启动容器</li><li><code>docker stop CONTAINER</code> 停止容器</li><li><code>docker restart CONTAINER</code> 重启容器</li><li><code>docker rm CONTAINER</code> 删除容器</li><li><code>docker image rm IMAGE</code> 删除镜像</li></ul><h3 id="3-7-保存容器到镜像"><a href="#3-7-保存容器到镜像" class="headerlink" title="3.7 保存容器到镜像"></a>3.7 保存容器到镜像</h3><p>我们可以使用<code>commit</code>指令保存容器当前状态到镜像</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]<br></code></pre></td></tr></table></figure><p>例如将名称为ubuntu20的容器保存为名为myenv:ubuntu20</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> docker commit ubuntu20 myenv:ubuntu20<br></code></pre></td></tr></table></figure><h3 id="3-8-镜像迁移"><a href="#3-8-镜像迁移" class="headerlink" title="3.8 镜像迁移"></a>3.8 镜像迁移</h3><p>Docker镜像可以保存到.tar文件，并且加载被保存在文件中的镜像。这使得docker容器便于部署和分发。</p><p>保存到文件</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker save -o image.tar image_name<br></code></pre></td></tr></table></figure><p>从文件加载</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker load -i image.tar<br></code></pre></td></tr></table></figure><h2 id="4-运行GUI程序"><a href="#4-运行GUI程序" class="headerlink" title="4 运行GUI程序"></a>4 运行GUI程序</h2><p>要运行docker容器内的GUI程序，一般使用xorg进行。这需要容器内安装了x server，将GUI界面通过宿主机桌面上显示。一般ubuntu系统的容器已默认安装。</p><h3 id="4-1-配置宿主机权限"><a href="#4-1-配置宿主机权限" class="headerlink" title="4.1 配置宿主机权限"></a>4.1 配置宿主机权限</h3><p>要在宿主机上显示容器内的GUI界面，需要配置宿主机的权限，使得容器可以访问宿主机的桌面。</p><p>下面这行命令直接允许所有的本地访问。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">xhost +<span class="hljs-built_in">local</span>:<br></code></pre></td></tr></table></figure><h3 id="4-2-配置容器参数"><a href="#4-2-配置容器参数" class="headerlink" title="4.2 配置容器参数"></a>4.2 配置容器参数</h3><p>要运行GUI程序，需要在运行容器时添加相应参数，使得容器可以正常访问宿主机的桌面显示图形界面。</p><ul><li><code>-e &quot;DISPLAY=$DISPLAY&quot;</code> 将环境变量DISPLAY设置为主机显示器;</li><li><code>--mount type=bind,src=/tmp/.X11-unix,dst=/tmp/.X11-unix</code> 将主机X服务器套接字挂载到相同路径下的容器内;</li><li><code>--device=/dev/dri:/dev/dri</code> 允许容器直接访问主机上的直接渲染(DRI) 设备。</li></ul><p>例如，要运行一个ubuntu容器并运行终端，可以使用下面命令。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> docker run -e <span class="hljs-string">&quot;DISPLAY=<span class="hljs-variable">$DISPLAY</span>&quot;</span> --mount <span class="hljs-built_in">type</span>=<span class="hljs-built_in">bind</span>,src=/tmp/.X11-unix,dst=/tmp/.X11-unix --device=/dev/dri:/dev/dri -it ubuntu:20.04<br></code></pre></td></tr></table></figure><p>之后在容器内运行GUI程序一般可以直接在宿主机上打开。</p><h2 id="5-调用nvidia-gpu加速容器"><a href="#5-调用nvidia-gpu加速容器" class="headerlink" title="5 调用nvidia gpu加速容器"></a>5 调用nvidia gpu加速容器</h2><h3 id="5-1-使容器可以访问宿主机显卡"><a href="#5-1-使容器可以访问宿主机显卡" class="headerlink" title="5.1 使容器可以访问宿主机显卡"></a>5.1 使容器可以访问宿主机显卡</h3><p>要调用nvidia gpu，需要在运行容器时添加相应参数，使得容器可以正常访问宿主机的显卡。</p><p>一般需要添加以下参数：</p><ul><li><code>--gpus all</code> 使得容器可以访问宿主机的显卡</li><li><code>--runtime=nvidia</code> 使用nvidia runtime</li><li><code>-e NVIDIA_DRIVER_CAPABILITIES=all</code> 设置环境变量，开启所有nvidia驱动功能</li></ul><p>例如打开一个需要调用gpu并且要打开图形界面的容器可以使用如下命令。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> docker run -e <span class="hljs-string">&quot;DISPLAY=<span class="hljs-variable">$DISPLAY</span>&quot;</span> --mount <span class="hljs-built_in">type</span>=<span class="hljs-built_in">bind</span>,src=/tmp/.X11-unix,dst=/tmp/.X11-unix --device=/dev/dri:/dev/dri --gpus all --runtime=nvidia -e NVIDIA_DRIVER_CAPABILITIES=all -it ubuntu:20.04<br></code></pre></td></tr></table></figure><p>无需在容器内安装nvidia显卡驱动，在容器内可以通过调用<code>nvidia-smi</code>来确认是否可以正常调用宿主机显卡驱动来访问显卡。</p><h3 id="5-2-容器中配置cuda、cudnn等"><a href="#5-2-容器中配置cuda、cudnn等" class="headerlink" title="5.2 容器中配置cuda、cudnn等"></a>5.2 容器中配置cuda、cudnn等</h3><p>要在容器中使用cuda等工具，推荐做法是直接使用已经配置好cuda、cudnn等工具的镜像，nvidia官方提供了相关镜像。<a href="https://catalog.ngc.nvidia.com/containers">官网</a></p><p>但如果确实需要自己安装cuda等工具，就像在宿主机上安装一样直接在容器中安装即可。</p><p><strong>注意</strong>：不建议使用包管理器安装，这往往会同时安装nvidia显卡驱动，导致驱动版本不兼容问题。推荐做法是使用nvidia的安装程序安装，并选择不安装驱动。</p><h2 id="6-数据可持久化"><a href="#6-数据可持久化" class="headerlink" title="6 数据可持久化"></a>6 数据可持久化</h2><p>容器是一个独立的环境，容器内的数据在容器删除后会丢失。为了保存数据，docker提供了数据可持久化的方法。</p><h3 id="6-1-复制文件"><a href="#6-1-复制文件" class="headerlink" title="6.1 复制文件"></a>6.1 复制文件</h3><p>Docker提供了<code>cp</code>命令，可以在宿主机和容器之间复制文件或文件夹。格式如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker <span class="hljs-built_in">cp</span> SRC DST<br></code></pre></td></tr></table></figure><p>会将SRC文件或文件夹复制到DST文件或文件夹，宿主机上的文件直接写路径即可，容器内的文件需要在容器内路径前加上<code>CONTAINER:</code>。</p><h3 id="6-2-卷"><a href="#6-2-卷" class="headerlink" title="6.2 卷"></a>6.2 卷</h3><p>Docker可以通过创建卷(volume)将数据可持久化，卷可以被映射为容器内的某个文件夹，该文件夹下的文件都会被保存到宿主机上，不会随着容器被删除而消失，可以重新挂载。</p><p>多个容器挂载相同的卷可以用来共享数据。</p><h4 id="6-2-1-运行时挂载卷"><a href="#6-2-1-运行时挂载卷" class="headerlink" title="6.2.1 运行时挂载卷"></a>6.2.1 运行时挂载卷</h4><p>在运行容器时可以使用<code>-v</code>参数来创建卷，格式如下：</p><p><code>-v volume_name:container_path</code></p><p>如果volume_name不存在，docker会自动创建一个新的卷。</p><p>如果要将宿主机的某个文件夹映射到容器内的某个文件夹，可以使用下面格式，这并不会创建卷。</p><p><code>-v host_path:container_path</code></p><h4 id="卷其他操作"><a href="#卷其他操作" class="headerlink" title="卷其他操作"></a>卷其他操作</h4><p>Docker中卷相关的命令以<code>docker volume</code>开头，</p><ul><li><code>docker volume create</code> 创建卷</li><li><code>docker volume ls</code> 列出所有卷</li><li><code>docker volume rm</code> 删除卷</li><li><code>docker volume inspect</code> 查看卷信息</li><li><code>docker volume prune</code> 删除所有未被使用的卷</li></ul><h2 id="7-网络network"><a href="#7-网络network" class="headerlink" title="7 网络network"></a>7 网络network</h2><p>Docker可以为容器配置network，使得容器间通过网络访问更加方便。挂载同一个network的docker容器处于同一个网络环境内，可以通过network的名字作为域名来互相访问。</p><p>例如，容器A和B挂载了名叫abnet的network，那么在A中访问<code>http://abnet:3000</code>可以访问B中localhost:3000上的服务。在B中也同理。</p><p>我们使用<code>-p</code>暴露端口将容器内端口映射到到宿主机的一个端口上。</p><h3 id="7-1-运行时挂载network"><a href="#7-1-运行时挂载network" class="headerlink" title="7.1 运行时挂载network"></a>7.1 运行时挂载network</h3><p>要在运行容器时为其挂载network只需要加上参数即可，如果该名称网络不存在则会创建一个新的。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">--network NETWORK_NAME<br></code></pre></td></tr></table></figure><p>Docker默认创建三个network，<code>host</code>，<code>bridge</code>，<code>none</code>。使用<code>host</code>网络，则容器使用宿主机的网络环境，没有自己独立的网络环境。<code>none</code>则表示不配置网络。</p><p>默认的网络为<code>bridge</code>，每个容器有自己独立的网络环境，包括ip地址等。我们可以创建新的使用<code>bridge</code>模式的网络，实现更好的隔离以避免端口冲突。</p><h3 id="7-2-管理network"><a href="#7-2-管理network" class="headerlink" title="7.2 管理network"></a>7.2 管理network</h3><p>要管理所有的network，需使用<code>docker network</code>相关命令</p><ul><li><code>docker network ls</code> 列出网络</li><li><code>docker network inspect NETWORK</code> 查看网络详细信息</li><li><code>docker network connect NETWORK CONTAINER</code> 将容器连接到某个网络</li><li><code>docker network disconnect NETWORK CONTAINER</code> 解除连接</li><li><code>docker network create NETWORK</code> 创建网络，默认使用<code>bridge</code>模式</li></ul><h2 id="8-Dockerfile构建镜像"><a href="#8-Dockerfile构建镜像" class="headerlink" title="8 Dockerfile构建镜像"></a>8 Dockerfile构建镜像</h2><p>上面提到，我们使用可以使用Dockerfile构建脚本来构建镜像。</p><h3 id="8-1-Dockerfile语法"><a href="#8-1-Dockerfile语法" class="headerlink" title="8.1 Dockerfile语法"></a>8.1 Dockerfile语法</h3><p>Dockerfile脚本实际上是基于某个镜像，再通过在容器中运行shell命令来完成容器配置。</p><p>我们创建一个文件夹，该文件夹下创建名为<code>Dockerfile</code>的文件</p><p>Dockerfile语法十分简单，常用命令为</p><ul><li><code>RUN COMMAND</code> 在容器内终端中运行COMMAND命令，</li><li><code>WORKDIR PATH</code> 将当前目录设置为PATH，推荐使用，不推荐用cd</li><li><code>COPY SRC DST</code> 复制文件或文件夹，前者为宿主机上的路径，后者为容器内路径，无需加前缀</li><li><code>FROM REPO:TAG</code> 选择基于哪个镜像，若本地没有该镜像则尝试拉取</li><li><code>USER USERNAME</code> 指定之后RUN等命令的执行用户</li><li><code>SHELL [command, args...]</code> command和args均为字符串，指定后面RUN使用的shell，如使用zsh(确认已安装)则为<code>SHELL [&#39;/usr/bin/zsh&#39;, &#39;-c&#39;]</code>。</li></ul><h3 id="8-2-构建镜像"><a href="#8-2-构建镜像" class="headerlink" title="8.2 构建镜像"></a>8.2 构建镜像</h3><p>将当前目录设置为上面创建的文件夹，运行以下命令即开始创建名为<code>repo:tag</code>的镜像。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> docker build . -t repo:tag<br></code></pre></td></tr></table></figure><p>会一步一步运行Dockerfile中的命令，并且默认每一步都会保存一个缓存。如果你修改了一行命令，那么build时会加载该行命令前的缓存，从这一行开始构建。</p><h2 id="9-其它配置"><a href="#9-其它配置" class="headerlink" title="9 其它配置"></a>9 其它配置</h2><h3 id="9-1-配置用户权限"><a href="#9-1-配置用户权限" class="headerlink" title="9.1 配置用户权限"></a>9.1 配置用户权限</h3><p>Docker命令仅能由root用户和docker用户组中的用户运行。要使普通用户也可运行docker命令，除了使用sudo命令外，还可以将该用户加入到docker权限组。</p><p>使用如下命令可将当前用户添加到docker用户组，之后可以正常运行docker命令。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> usermod -aG docker <span class="hljs-variable">$USER</span><br></code></pre></td></tr></table></figure><h3 id="9-2-Docker-Desktop卸载残留"><a href="#9-2-Docker-Desktop卸载残留" class="headerlink" title="9.2 Docker Desktop卸载残留"></a>9.2 Docker Desktop卸载残留</h3><p>若安装过Docker Desktop但又卸载，在用户目录下可能残留配置和数据文件。当我们将当前用户加入到docker用户组之后，尝试直接运行docker命令时，可能会错误地尝试加载这些配置文件而产生错误。</p><p>例如提示无法连接至<code>/home/username/.docker/desktop/docker.sock</code>。</p><p>要解决该问题，只需删除用户目录下的<code>.docker</code>文件夹。</p>]]>
    </content>
    <id>https://chillywall.github.io/ChillyBlog/2025/04/02/Docker%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/</id>
    <link href="https://chillywall.github.io/ChillyBlog/2025/04/02/Docker%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/"/>
    <published>2025-04-02T06:00:00.000Z</published>
    <summary>
      <![CDATA[<p>Docker是一种虚拟容器应用这使我们可以在性能损失很小的情况下，方便地创建并管理虚拟环境。 本教程涵盖Docker的基础操作，帮助初学者快速上手。</p>]]>
    </summary>
    <title>Docker基础教程</title>
    <updated>2026-04-10T05:07:21.884Z</updated>
  </entry>
</feed>
