<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>S1eke&#39;s Blog</title>
  
  <subtitle>I&#39;m curious.</subtitle>
  <link href="https://blog.jugg.xyz/atom.xml" rel="self"/>
  
  <link href="https://blog.jugg.xyz/"/>
  <updated>2026-05-02T11:24:14.332Z</updated>
  <id>https://blog.jugg.xyz/</id>
  
  <author>
    <name>Sean Fang</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Localhost 为什么比 127.0.0.1 慢？</title>
    <link href="https://blog.jugg.xyz/2026/05/02/frontend/why-is-localhost-slower-than-127-0-0-1/"/>
    <id>https://blog.jugg.xyz/2026/05/02/frontend/why-is-localhost-slower-than-127-0-0-1/</id>
    <published>2026-05-02T09:50:10.000Z</published>
    <updated>2026-05-02T11:24:14.332Z</updated>
    
    <content type="html"><![CDATA[<p>先说结论：<code>localhost</code> 和 <code>127.0.0.1</code> 本身没有谁天然更快。正常情况下，<code>localhost</code> 多出来的主机名解析，只是在本机 resolver 或 hosts 文件里查一下 <code>localhost</code> 对应的 IP 地址，通常只是微秒级差异，几乎可以忽略。</p><p>真正容易拉开差距的是另一件事：<code>localhost</code> 不一定只指向 <code>127.0.0.1</code>，它还可能优先解析到 IPv6 的 <code>::1</code>。如果你的服务只监听了 IPv4，客户端先尝试连 <code>::1</code>，再回退到 <code>127.0.0.1</code>，这一步就可能带来 200ms 到 300ms 级别的连接延迟。</p><h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p>先看一个本地开发服务的例子。通过 <code>127.0.0.1</code> 访问：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/why-is-localhost-slower-than-127-0-0-1/127server.png" alt="通过 127.0.0.1 访问本地服务" title="">                </div>                <div class="image-caption">通过 127.0.0.1 访问本地服务</div>            </figure><p>再通过 <code>localhost</code> 访问同一个服务：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/why-is-localhost-slower-than-127-0-0-1/localhostserver.png" alt="通过 localhost 访问本地服务" title="">                </div>                <div class="image-caption">通过 localhost 访问本地服务</div>            </figure><p>同一个页面，同样 12 次请求，传输数据量也差不多，一个耗时 214ms，一个耗时 691ms，几乎差了三倍。</p><p>这个页面的请求不多，所以瀑布图里很容易看到有几个请求明显更慢。点开其中一个请求，问题集中在连接阶段：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/why-is-localhost-slower-than-127-0-0-1/TTFB1.png" alt="localhost 请求的连接耗时" title="">                </div>                <div class="image-caption">localhost 请求的连接耗时</div>            </figure><p>再放一张相同请求在另一个访问方式下的对比：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/why-is-localhost-slower-than-127-0-0-1/TTFB2.png" alt="127.0.0.1 请求的连接耗时" title="">                </div>                <div class="image-caption">127.0.0.1 请求的连接耗时</div>            </figure><p>这里需要注意一个细节：这不是传统意义上的“后端处理慢”，也不该简单归因成“DNS 查询慢”。DevTools 里看到的慢点更接近 <code>Initial connection</code>，也就是建立 TCP 连接时的地址选择和回退成本。</p><h2 id="根因：IPv6-优先和-Happy-Eyeballs"><a href="#根因：IPv6-优先和-Happy-Eyeballs" class="headerlink" title="根因：IPv6 优先和 Happy Eyeballs"></a>根因：IPv6 优先和 Happy Eyeballs</h2><p>在很多系统里，<code>localhost</code> 会同时对应 IPv6 和 IPv4 地址。比如可以先看本机解析结果：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">getent hosts localhost</span></span><br><span class="line">::1             localhost</span><br></pre></td></tr></table></figure><p>这只能说明当前环境里 <code>localhost</code> 的解析结果优先给了 <code>::1</code>。不同系统、不同 hosts 配置、不同 resolver 策略下，结果顺序可能不同。更完整的排查可以用：</p><figure class="highlight shell"><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><span class="line">getent ahosts localhost</span><br><span class="line">curl -v http://localhost:4173</span><br><span class="line">curl -4 -v http://localhost:4173</span><br><span class="line">curl -6 -v http://localhost:4173</span><br></pre></td></tr></table></figure><p>如果服务没有监听 <code>::1</code>，<code>curl -v</code> 里常见的现象会是这样：</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></pre></td><td class="code"><pre><span class="line">* Host localhost:4173 was resolved.</span><br><span class="line">* IPv6: ::1</span><br><span class="line">* IPv4: 127.0.0.1</span><br><span class="line">*   Trying [::1]:4173...</span><br><span class="line">* connect to ::1 port 4173 from ::1 port 51430 failed: Connection refused</span><br><span class="line">*   Trying 127.0.0.1:4173...</span><br><span class="line">* Connected to localhost (127.0.0.1) port 4173</span><br></pre></td></tr></table></figure><p>这就是 IPv6 到 IPv4 的回退。</p><p>curl 的 Happy Eyeballs 默认会先尝试 IPv6；如果 IPv6 连接还没有成功，默认 200ms 后会并行发起 IPv4 连接。Chrome&#x2F;Chromium 也有类似策略，Chromium 源码里 <code>kIPv6FallbackTime</code> 当前是 300ms，和上面截图中慢出来的连接耗时基本对得上。</p><p>所以这里的“慢”不是 <code>localhost</code> 这个字符串慢，而是：</p><ol><li><code>localhost</code> 优先解析到 <code>::1</code>；</li><li>服务实际只监听 <code>127.0.0.1</code>；</li><li>客户端先尝试 IPv6，再回退到 IPv4；</li><li>每新建一次 TCP 连接，就可能重复一次回退成本。</li></ol><h2 id="为什么会重复出现"><a href="#为什么会重复出现" class="headerlink" title="为什么会重复出现"></a>为什么会重复出现</h2><p>如果只是第一次连接慢一次，影响还比较有限。但本地开发时经常能稳定复现，是因为请求没有复用连接。</p><p>在 Vite 的代理场景里，<code>server.proxy</code> &#x2F; <code>preview.proxy</code> 的配置会传给底层 <code>http-proxy-3</code>。<code>http-proxy-3</code> 向上游服务发请求时会使用 Node 的 HTTP Agent。Node 文档里说明，当 <code>keepAlive=false</code> 且 <code>maxSockets=Infinity</code> 时，请求会使用 <code>Connection: close</code>。也就是说，请求结束后连接会关闭，下一次请求又要重新建连。</p><p>vite 默认没有开启 keep-alive。只要每个接口请求都要重新建连，就可能反复经历：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">localhost -&gt; ::1 -&gt; 连接失败/等待回退 -&gt; 127.0.0.1 -&gt; 成功</span><br></pre></td></tr></table></figure><p>这也是为什么瀑布图里不止一个请求慢。</p><h2 id="怎么处理"><a href="#怎么处理" class="headerlink" title="怎么处理"></a>怎么处理</h2><p>处理方法就是给 Vite proxy 新增一个 keep-alive Agent，让代理到上游服务的连接可以复用，避免每个接口都重新经历连接阶段：</p><figure class="highlight ts"><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><span class="line"><span class="keyword">import</span> http <span class="keyword">from</span> <span class="string">&#x27;node:http&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; defineConfig &#125; <span class="keyword">from</span> <span class="string">&#x27;vite&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> keepAliveAgent = <span class="keyword">new</span> http.<span class="title class_">Agent</span>(&#123;</span><br><span class="line">  <span class="attr">keepAlive</span>: <span class="literal">true</span>,</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(&#123;</span><br><span class="line">  <span class="attr">server</span>: &#123;</span><br><span class="line">    <span class="attr">proxy</span>: &#123;</span><br><span class="line">      <span class="string">&#x27;/api&#x27;</span>: &#123;</span><br><span class="line">        <span class="attr">target</span>: <span class="string">&#x27;http://127.0.0.1:8080&#x27;</span>,</span><br><span class="line">        <span class="attr">changeOrigin</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="attr">agent</span>: keepAliveAgent,</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">preview</span>: &#123;</span><br><span class="line">    <span class="attr">proxy</span>: &#123;</span><br><span class="line">      <span class="string">&#x27;/api&#x27;</span>: &#123;</span><br><span class="line">        <span class="attr">target</span>: <span class="string">&#x27;http://127.0.0.1:8080&#x27;</span>,</span><br><span class="line">        <span class="attr">changeOrigin</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="attr">agent</span>: keepAliveAgent,</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>如果上游是 HTTPS，则要换成 <code>https.Agent</code>。</p><h2 id="排查建议"><a href="#排查建议" class="headerlink" title="排查建议"></a>排查建议</h2><p>可以用下面这个命令把 DNS、连接、首字节和总耗时拆开看：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -o /dev/null -s -w &#x27;namelookup=%&#123;time_namelookup&#125; connect=%&#123;time_connect&#125; starttransfer=%&#123;time_starttransfer&#125; total=%&#123;time_total&#125;\n&#x27; http://localhost:4173/</span><br></pre></td></tr></table></figure><p>再分别对比：</p><figure class="highlight shell"><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><span class="line">curl -o /dev/null -s -w &#x27;namelookup=%&#123;time_namelookup&#125; connect=%&#123;time_connect&#125; starttransfer=%&#123;time_starttransfer&#125; total=%&#123;time_total&#125;\n&#x27; http://127.0.0.1:4173/</span><br><span class="line">curl -4 -o /dev/null -s -w &#x27;connect=%&#123;time_connect&#125; total=%&#123;time_total&#125;\n&#x27; http://localhost:4173/</span><br><span class="line">curl -6 -o /dev/null -s -w &#x27;connect=%&#123;time_connect&#125; total=%&#123;time_total&#125;\n&#x27; http://localhost:4173/</span><br></pre></td></tr></table></figure><p>如果 <code>localhost</code> 明显慢，而 <code>curl -4</code> 恢复正常，基本就可以确认是 IPv6 优先和 IPv4 回退带来的连接成本。</p><p>参考：</p><ul><li>everything curl: Happy Eyeballs: <a href="https://everything.curl.dev/usingcurl/connections/happy.html">https://everything.curl.dev/usingcurl/connections/happy.html</a></li><li>Chromium <code>kIPv6FallbackTime</code>: <a href="https://chromium.googlesource.com/chromium/src/+/refs/heads/main/net/socket/transport_connect_job.h#124">https://chromium.googlesource.com/chromium/src/+/refs/heads/main/net/socket/transport_connect_job.h#124</a></li><li>Node.js HTTP Agent: <a href="https://nodejs.org/api/http.html#new-agentoptions">https://nodejs.org/api/http.html#new-agentoptions</a></li><li>Vite server.proxy: <a href="https://vite.dev/config/server-options.html#server-proxy">https://vite.dev/config/server-options.html#server-proxy</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;先说结论：&lt;code&gt;localhost&lt;/code&gt; 和 &lt;code&gt;127.0.0.1&lt;/code&gt; 本身没有谁天然更快。正常情况下，&lt;code&gt;localhost&lt;/code&gt; 多出来的主机名解析，只是在本机 resolver 或 hosts 文件里查一下 &lt;code</summary>
      
    
    
    
    <category term="前端" scheme="https://blog.jugg.xyz/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="Node.js" scheme="https://blog.jugg.xyz/tags/Node-js/"/>
    
    <category term="PWA" scheme="https://blog.jugg.xyz/tags/PWA/"/>
    
    <category term="DNS" scheme="https://blog.jugg.xyz/tags/DNS/"/>
    
  </entry>
  
  <entry>
    <title>AI 开发体验</title>
    <link href="https://blog.jugg.xyz/2026/03/29/musings/ai-development-experience/"/>
    <id>https://blog.jugg.xyz/2026/03/29/musings/ai-development-experience/</id>
    <published>2026-03-29T15:30:27.000Z</published>
    <updated>2026-05-02T11:24:14.332Z</updated>
    
    <content type="html"><![CDATA[<p>最近花了差不多两个星期，开发了一个小说阅读器：<a href="https://github.com/s1eke-labs/PlotMapAI">PlotMapAI</a>，感慨万分，现在的 AI 是真的强大。我基本上没有修改过 AI 生成的任何代码，一直在提需求，只有一些比较简单的东西自己手动微调了一下。今天也发布了 v1.1.0 版本，算是告一段落了，可以来写一下一路上踩过的坑。</p><h2 id="Tokens"><a href="#Tokens" class="headerlink" title="Tokens"></a>Tokens</h2><p>Tokens 不够是最大的坑，我本来用的是 Antigravity，弄了一个学生账号，免费试用一年的 Google AI Pro。但是 Google 对额度的管控越来越严格，从一开始的五小时刷新额度，到后面一个礼拜刷新一次额度，而且额度少得可怜，等于说我用完了要么换个账号，要么就只能等一个礼拜。非常折磨人，也没办法接受。</p><p>然后我开始研究国内的 IDE，试用了字节的 Trae 和腾讯的 CodeBuddy，我发现 Trae 模型好像停止更新了，不知道为啥没有最新的模型。而腾讯有最新的 GPT-5.4，用了一下感觉也挺好的，直接买了一个会员，结果没用半天又用完额度了……</p><p>最后研究了一下拼车，结果在闲鱼上发现 Codex Business 会员（虽然卖家一般都会标注是 team 账号），只要七块钱一个月。搜了一下 Business 账号的限制，发现 Business 管理员只拥有审计员工账号操作日志的最高权限，聊天记录之类的按照官方的说法还是私密的，而且我只是写个人的开源项目，所以没啥顾虑，直接创建了一个新账号上车了，七块钱能用一个礼拜也算赚了。</p><p>然后才发现之前过的都是什么苦日子，相对于 Google 来说，OpenAI 实在是太大方了。后面用到额度不够的时候，还发现一个账户可以直接加入多个 Business，额度不够直接切一个工作区就好了，甚至 session 也可以复用，比 Google 真是不知道高到哪里去了。</p><p>使用 Codex 和 VSCode 官方插件，很快就完成了 <a href="https://github.com/s1eke-labs/PlotMapAI">PlotMapAI</a> 75% 的功能。非常之快，但是为什么 v1.1.0 版本拖到两个礼拜才完成？因为剩下的 25% 真的是折磨。</p><h2 id="重构"><a href="#重构" class="headerlink" title="重构"></a>重构</h2><p>折磨就是从细化细节开始的。只要开始细化细节，tokens 就不够用了。没错，就是这么恐怖。</p><p>项目一开始，我提了需求和想法，然后根据 AI 的建议，技术选型选了 Flask + React + SQLite。结果刚开发完雏形，75% 的功能实现了，突然发现其实根本不需要后端，数据直接存 IndexedDB 就好了，而且所有的数据都在用户本地，更适合这种私有化部署的项目。纯前端项目的话，部署上也有很多可以白嫖的资源，客户端也可以直接支持 PWA，简直完美。</p><p>但是出于节省 tokens 的想法，还是准备放弃把项目重构成纯前端的计划了。结果就在这时，小米发布了 mimo v2 pro。当然这并不是重点，重点是首周免费（现在已经延期成首两周免费了）。我直接试用了一下，感觉勉强也算 t1 档位，用来做一些简单的事情还是没问题的。所以直接用 Claude Opus 4.6 （没错，还是用 Antigravity 的额度）做了一个重构计划，然后用 kilocode + mimo v2 pro 来实施。最后成功<a href="https://github.com/s1eke-labs/PlotMapAI/commit/75cdd1ee0a31da8caf5499c55fc9418e83f6a630">提交</a>。</p><p>有了第一次重构，然后就开始一发不可收拾。不断新增需求，然后感觉很臃肿，开始拆分，再加需求，再拆分重构，反复循环。简单的部分直接用 mimo v2 pro ，复杂的、难得、需要有设计的地方用的 gpt 5.4。中间又经历了大规模清理和模块化：重命名了 API 实体，重构了 epub 解析模块、AI 分析模块等等。还迁移了一次架构，从扁平的 src&#x2F;services&#x2F; + src&#x2F;pages&#x2F; 结构迁移到了领域驱动设计。</p><p>但是重构并不是万能的。重构的目的一般都是为了增强可维护性，提高可扩展性，提升可读性等等，重构手段一般是拆分、抽象等等。我一边开发，一边学习别的项目的架构。我在开发新功能的时候，发现书的排版布局确实是个大问题，有些地方改了很多遍都没有达到我想要的效果。于是我开始研究同类项目，找了一个 Readium 的 ts-toolkit 开始学习。我创了一个分支，参照 ts-toolkit 的思想重构整个阅读模块。结果一个 bug 一直改不好，有些效果甚至没有重构之前的效果好。因为本身也是为了解决这些问题进行重构的，所以我有点崩溃。我有了一个新想法，干脆按照新架构直接重写吧，这样可以直接抛弃很多技术债。</p><p>于是我开始让 AI 分析当前项目的技术架构，还有一些技术细节，为重写做准备。结果 AI 的分析结果是：过度工程化，过度设计。</p><p>我有点懵，原来不是架构越高级越好。我仔细看了分析报告，最终决定直接放弃这个分支，也不重写了，直接从 dev 分支延续之前的架构进行开发，目标转变成先做到能用。</p><p>然后又开始吭哧吭哧地干，终于是完成了这个阶段，发布了 1.1.0 版本。</p><h2 id="教训"><a href="#教训" class="headerlink" title="教训"></a>教训</h2><p>今天发布了 1.1.0 版本之后，我又回头看了下合并的 PR，发现除了新增功能外，还有一些是可以避免的，比如一些代码规范的问题、单文件代码超过 1000 行、膨胀的模块级全局单例、复杂度过高的 hook，这些至少可以通过 ESLint 配置更早暴露出来，还是吃了没有经验的亏，浪费了很多的时间和 tokens。</p><p>又找了一些项目看了一下，后续 ESLint 这块准备直接用现成的 <a href="https://github.com/alibaba/f2e-spec">阿里巴巴前端规范</a>。</p><p>除了这些之外就是还得继续学习设计模式，不过这些也都不是最重要的，当前最重要的还是先找工作。没有真实的业务，还是很难想到要写什么东西，现在这些纯粹就是当练习的玩具了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近花了差不多两个星期，开发了一个小说阅读器：&lt;a href=&quot;https://github.com/s1eke-labs/PlotMapAI&quot;&gt;PlotMapAI&lt;/a&gt;，感慨万分，现在的 AI 是真的强大。我基本上没有修改过 AI 生成的任何代码，一直在提需求，只有一些</summary>
      
    
    
    
    
    <category term="开发" scheme="https://blog.jugg.xyz/tags/%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>Tailscale 自建 DERP 节点全教程：解决国内中继高延迟问题</title>
    <link href="https://blog.jugg.xyz/2026/03/03/ops/tailscale-self-hosted-derp-node-complete-guide-solve-high-latency-in-domestic-network/"/>
    <id>https://blog.jugg.xyz/2026/03/03/ops/tailscale-self-hosted-derp-node-complete-guide-solve-high-latency-in-domestic-network/</id>
    <published>2026-03-03T11:58:42.000Z</published>
    <updated>2026-05-02T11:24:14.333Z</updated>
    
    <content type="html"><![CDATA[<h2 id="为什么需要-Tailscale？"><a href="#为什么需要-Tailscale？" class="headerlink" title="为什么需要 Tailscale？"></a>为什么需要 Tailscale？</h2><p>AI推动了硬件销售和软件迭代，很多人家里都部署了一下自己玩的或者用的服务，然后可能有些人为了方便随时能用就直接把服务暴露在公网上，这就导致人为增加了一堆入侵点。从安全角度来说，漏洞修复永远赶不上被利用的速度。一个 0day 出来，几小时之内全网扫描工具就开始扫了。</p><p>最近的飞牛 NAS OS 被 0day 打穿，详情看<a href="https://club.fnnas.com/forum.php?mod=viewthread&tid=53420&extra=page=1">飞牛社区公告</a>，一堆暴露在公网的设备变成了肉鸡；还有<a href="https://www.163.com/dy/article/KN3U5CL10511D3QS.html">超22万OpenClaw部署实例暴露公网</a>,基本是裸奔部署，甚至未启用身份验证。</p><p>所以结论很简单：<strong>别把东西直接丢公网上</strong>。你需要一个安全的内网隧道，让设备之间像在同一个局域网一样互通，对外面的人来说你的服务压根不存在，这样能很大程度给你增加容错空间，让你发现安全问题时能有更多时间去修复。</p><h2 id="选型：为什么是-Tailscale？"><a href="#选型：为什么是-Tailscale？" class="headerlink" title="选型：为什么是 Tailscale？"></a>选型：为什么是 Tailscale？</h2><p>市面上组网方案不少，简单列一下：</p><table><thead><tr><th>特性</th><th>Tailscale</th><th>WireGuard</th><th>OpenVPN</th></tr></thead><tbody><tr><td>配置复杂度</td><td>⭐ 极低</td><td>⭐⭐ 中等</td><td>⭐⭐⭐ 较高</td></tr><tr><td>NAT 穿透</td><td>✅ 自动打洞</td><td>❌ 需手动配置</td><td>❌ 需中心服务器</td></tr><tr><td>网络拓扑</td><td>Mesh（去中心化）</td><td>点对点 &#x2F; 星型</td><td>星型（中心化）</td></tr><tr><td>性能</td><td>高（基于 WireGuard）</td><td>高</td><td>中等</td></tr><tr><td>多平台支持</td><td>全平台</td><td>全平台</td><td>全平台</td></tr><tr><td>MagicDNS</td><td>✅ 内置</td><td>❌ 需自建</td><td>❌ 需自建</td></tr><tr><td>上手难度</td><td>注册即用</td><td>需要理解密钥交换</td><td>需要管理证书</td></tr></tbody></table><p>WireGuard 性能没话说，但你得自己搞密钥交换、自己配路由、自己处理 NAT 穿透，折腾完一圈头都大了。OpenVPN 就更不用提了，证书管理那一套，光配就能劝退一半人。</p><p>Tailscale 底层就是 WireGuard，性能和加密都继承了，但上面做了一整套自动化——密钥交换、NAT 穿透、路由管理全给你包了。装上就能用，真的省心。</p><h2 id="为什么要自建-DERP-节点？"><a href="#为什么要自建-DERP-节点？" class="headerlink" title="为什么要自建 DERP 节点？"></a>为什么要自建 DERP 节点？</h2><p>Tailscale 大部分时候能直接打洞成功，两台设备点对点直连，体验很好。但总有打洞失败的时候，这时候流量就得走 <strong>DERP（Designated Encrypted Relay for Packets）</strong> 中继服务器绕一下。</p><p>问题来了：<strong>官方的 DERP 节点基本全在境外</strong>。</p><p>国内的网络环境大家都懂，中继流量绕一圈海外再回来，延迟上百毫秒是常态，还不稳定，丢包也多。这体验能好才怪。</p><p>所以如果你在国内有台服务器，自建一个 DERP 节点是很有必要的。打洞失败的时候走国内中继，延迟直接从上百毫秒降到个位数，体验天差地别。</p><hr><h2 id="部署环境"><a href="#部署环境" class="headerlink" title="部署环境"></a>部署环境</h2><p>先交代一下本文的环境：</p><ul><li><strong>操作系统</strong>：Debian 13 (Trixie)</li><li><strong>Go 版本</strong>：1.26.0</li><li><strong>示例域名</strong>：<code>derper.example.com</code>（请替换为你自己的域名）</li></ul><blockquote><p>⚠️ <strong>注意</strong>：本文中所有涉及域名的地方均使用 <code>derper.example.com</code> 作为示例，请在实际部署时替换为你自己的域名。</p></blockquote><hr><h2 id="第一步：安装-Go-语言环境"><a href="#第一步：安装-Go-语言环境" class="headerlink" title="第一步：安装 Go 语言环境"></a>第一步：安装 Go 语言环境</h2><p>derper 是 Go 写的，安装需要 Go 环境。一般来说 derper 都依赖最新版本的 Go，而各大发行版包管理器里的版本基本都是老的，所以老老实实用官方的二进制包装吧。</p><p>参考 <a href="https://go.dev/doc/install">Go 官方安装文档</a>：</p><figure class="highlight bash"><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><span class="line"><span class="comment"># 下载 Go 最新版本（以 1.26.0 为例，请根据实际情况调整版本号）</span></span><br><span class="line">wget https://go.dev/dl/go1.26.0.linux-amd64.tar.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># 删除旧版本并解压新版本</span></span><br><span class="line">sudo <span class="built_in">rm</span> -rf /usr/local/go</span><br><span class="line">sudo tar -C /usr/local -xzf go1.26.0.linux-amd64.tar.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># 配置环境变量（添加到 ~/.bashrc 或 ~/.zshrc）</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin&#x27;</span> &gt;&gt; ~/.bashrc</span><br><span class="line"><span class="built_in">source</span> ~/.bashrc</span><br><span class="line"></span><br><span class="line"><span class="comment"># 验证安装</span></span><br><span class="line">go version</span><br><span class="line"><span class="comment"># 输出：go version go1.26.0 linux/amd64</span></span><br></pre></td></tr></table></figure><h2 id="第二步：安装-DERP-服务端"><a href="#第二步：安装-DERP-服务端" class="headerlink" title="第二步：安装 DERP 服务端"></a>第二步：安装 DERP 服务端</h2><p>Go 装好了，装 derper 就一条命令的事：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go install tailscale.com/cmd/derper@latest</span><br></pre></td></tr></table></figure><blockquote><p>💡 <strong>国内加速</strong>：国内服务器拉 Go 模块可能连不上，配个 <a href="https://goproxy.cn/">GOPROXY.CN</a> 代理就好了：</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><span class="line"><span class="built_in">export</span> GOPROXY=https://goproxy.cn,direct</span><br><span class="line">go install tailscale.com/cmd/derper@latest</span><br></pre></td></tr></table></figure></blockquote><p>装完之后 derper 二进制文件在 <code>$HOME/go/bin/derper</code>。</p><h2 id="第三步：安装-Tailscale-客户端（安全加固）"><a href="#第三步：安装-Tailscale-客户端（安全加固）" class="headerlink" title="第三步：安装 Tailscale 客户端（安全加固）"></a>第三步：安装 Tailscale 客户端（安全加固）</h2><p>这步很关键。</p><p>默认情况下，<strong>只要有人知道你 DERP 服务器的 IP，就可以把它加到自己的 DERP map 里，然后白嫖你的带宽做中继</strong>。你辛辛苦苦搭的节点，结果给别人当免费中转站，想想就亏。</p><p>解决办法是在 DERP 服务器上同时跑一个 <code>tailscaled</code>，然后启动 derper 的时候加 <code>--verify-clients</code> 参数。这样 derper 会检查过来连接的客户端是不是你自己 tailnet 里的设备，不是的话直接拒绝。</p><p>装 Tailscale：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://tailscale.com/install.sh | sh</span><br></pre></td></tr></table></figure><p>然后加入你的 tailnet：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo tailscale up</span><br></pre></td></tr></table></figure><p>会给你一个 URL，浏览器打开登录授权就行。去 <a href="https://login.tailscale.com/admin/machines">Tailscale 管理面板</a> 看到这台机器了，就说明成了。</p><h2 id="第四步：配置-TLS-证书"><a href="#第四步：配置-TLS-证书" class="headerlink" title="第四步：配置 TLS 证书"></a>第四步：配置 TLS 证书</h2><p>DERP 是走 HTTPS 的，所以需要 TLS 证书。我们用 Let’s Encrypt 的免费证书，工具用 certbot。</p><h3 id="安装-Certbot"><a href="#安装-Certbot" class="headerlink" title="安装 Certbot"></a>安装 Certbot</h3><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><span class="line">sudo apt update</span><br><span class="line">sudo apt install -y certbot</span><br></pre></td></tr></table></figure><h3 id="申请证书"><a href="#申请证书" class="headerlink" title="申请证书"></a>申请证书</h3><p>先确保 <code>derper.example.com</code> 的 DNS 已经解析到这台服务器，然后：</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><span class="line"><span class="comment"># 使用 standalone 模式申请证书（需要临时占用 80 端口）</span></span><br><span class="line">sudo certbot certonly --standalone -d derper.example.com</span><br></pre></td></tr></table></figure><p>跟着提示填邮箱、同意条款就行。搞定后证书在这：</p><ul><li>证书：<code>/etc/letsencrypt/live/derper.example.com/fullchain.pem</code></li><li>私钥：<code>/etc/letsencrypt/live/derper.example.com/privkey.pem</code></li></ul><h3 id="创建证书软链接"><a href="#创建证书软链接" class="headerlink" title="创建证书软链接"></a>创建证书软链接</h3><p>这里有个坑：<strong>derper 只能指定证书目录，没法直接指定证书文件</strong>。它会在目录里按 <code>&lt;hostname&gt;.crt</code> 和 <code>&lt;hostname&gt;.key</code> 的规则去找。但 Let’s Encrypt 生成出来的叫 <code>fullchain.pem</code> 和 <code>privkey.pem</code>，名字对不上。</p><p>建个软链接就好了：</p><figure class="highlight bash"><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><span class="line"><span class="comment"># 创建证书目录</span></span><br><span class="line">sudo <span class="built_in">mkdir</span> -p /var/lib/derper/certs/</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建软链接</span></span><br><span class="line">sudo <span class="built_in">ln</span> -s /etc/letsencrypt/live/derper.example.com/fullchain.pem \</span><br><span class="line">    /var/lib/derper/certs/derper.example.com.crt</span><br><span class="line">sudo <span class="built_in">ln</span> -s /etc/letsencrypt/live/derper.example.com/privkey.pem \</span><br><span class="line">    /var/lib/derper/certs/derper.example.com.key</span><br></pre></td></tr></table></figure><h2 id="第五步：启动-DERP-服务"><a href="#第五步：启动-DERP-服务" class="headerlink" title="第五步：启动 DERP 服务"></a>第五步：启动 DERP 服务</h2><p>准备工作搞定了，来启动 derper。先看下每个参数啥意思：</p><table><thead><tr><th>参数</th><th>说明</th></tr></thead><tbody><tr><td><code>-hostname</code></td><td>DERP 服务器的域名</td></tr><tr><td><code>-certdir</code></td><td>TLS 证书所在目录</td></tr><tr><td><code>-certmode=manual</code></td><td>手动管理证书（我们用 certbot，不让 derper 自己申请）</td></tr><tr><td><code>-http-port=-1</code></td><td>关掉 HTTP 端口（只用 HTTPS）</td></tr><tr><td><code>-a=:4501</code></td><td>HTTPS 监听端口</td></tr><tr><td><code>-stun-port=4502</code></td><td>STUN 端口（辅助 NAT 穿透）</td></tr><tr><td><code>-verify-clients</code></td><td>只允许你自己 tailnet 的设备用</td></tr></tbody></table><p>启动：</p><figure class="highlight bash"><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><span class="line">/root/go/bin/derper \</span><br><span class="line">    -hostname=derper.example.com \</span><br><span class="line">    -certdir=/var/lib/derper/certs/ \</span><br><span class="line">    -certmode=manual \</span><br><span class="line">    -http-port=-1 \</span><br><span class="line">    -a=:4501 \</span><br><span class="line">    -stun-port=4502 \</span><br><span class="line">    -verify-clients</span><br></pre></td></tr></table></figure><h3 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h3><p>浏览器打开 <code>https://derper.example.com:4501</code>，看到下面这行字就成了：</p><blockquote><p><strong>This is a Tailscale DERP server.</strong></p></blockquote><hr><h2 id="第六步：自动化运维"><a href="#第六步：自动化运维" class="headerlink" title="第六步：自动化运维"></a>第六步：自动化运维</h2><p>总不能一直手动跑 derper 吧，搞成系统服务，再加上自动更新。</p><h3 id="创建-Systemd-Service"><a href="#创建-Systemd-Service" class="headerlink" title="创建 Systemd Service"></a>创建 Systemd Service</h3><p>我们使用 systemd 的模板实例功能。</p><p>创建 <code>/etc/systemd/system/tailscale-derp@.service</code>：</p><figure class="highlight ini"><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></pre></td><td class="code"><pre><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Tailscale DERP Server</span><br><span class="line"><span class="attr">After</span>=network.target tailscaled.service</span><br><span class="line"><span class="attr">Requires</span>=tailscaled.service</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Type</span>=simple</span><br><span class="line"><span class="attr">ExecStart</span>=/root/go/bin/derper \</span><br><span class="line">    <span class="attr">-hostname</span>=%i \</span><br><span class="line">    <span class="attr">-certdir</span>=/var/lib/derper/certs/ \</span><br><span class="line">    <span class="attr">-certmode</span>=manual \</span><br><span class="line">    <span class="attr">-http-port</span>=-<span class="number">1</span> \</span><br><span class="line">    <span class="attr">-a</span>=:<span class="number">4501</span> \</span><br><span class="line">    <span class="attr">-stun-port</span>=<span class="number">4502</span> \</span><br><span class="line">    -verify-clients</span><br><span class="line"><span class="attr">Restart</span>=always</span><br><span class="line"><span class="attr">RestartSec</span>=<span class="number">5</span></span><br><span class="line"><span class="attr">StandardOutput</span>=journal</span><br><span class="line"><span class="attr">StandardError</span>=journal</span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br></pre></td></tr></table></figure><p>启用：</p><figure class="highlight bash"><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><span class="line"><span class="comment"># 启动服务（将 derper.example.com 替换为你的域名）</span></span><br><span class="line">sudo systemctl daemon-reload</span><br><span class="line">sudo systemctl <span class="built_in">enable</span> --now tailscale-derp@derper.example.com</span><br><span class="line">    </span><br><span class="line"><span class="comment"># 检查运行状态</span></span><br><span class="line">sudo systemctl status tailscale-derp@derper.example.com</span><br></pre></td></tr></table></figure><h3 id="创建自动更新脚本"><a href="#创建自动更新脚本" class="headerlink" title="创建自动更新脚本"></a>创建自动更新脚本</h3><p>derper 和 tailscale 都在持续更新，为了不落后版本太多，写个自动更新脚本。</p><p>创建 <code>/usr/local/bin/update-tailscale-derp.sh</code>：</p><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="built_in">set</span> -euo pipefail</span><br><span class="line"></span><br><span class="line">LOG_TAG=<span class="string">&quot;tailscale-derp-updater&quot;</span></span><br><span class="line">GOPROXY=<span class="string">&quot;https://goproxy.cn,direct&quot;</span></span><br><span class="line"><span class="built_in">export</span> GOPROXY</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">log</span></span>() &#123;</span><br><span class="line">    logger -t <span class="string">&quot;<span class="variable">$LOG_TAG</span>&quot;</span> <span class="string">&quot;<span class="variable">$@</span>&quot;</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;[<span class="subst">$(date &#x27;+%Y-%m-%d %H:%M:%S&#x27;)</span>] $*&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">update_derper</span></span>() &#123;</span><br><span class="line">    <span class="built_in">log</span> <span class="string">&quot;检查 derper 更新...&quot;</span></span><br><span class="line">    </span><br><span class="line">    <span class="built_in">local</span> current_version</span><br><span class="line">    current_version=$(/root/go/bin/derper --version 2&gt;&amp;1 || <span class="built_in">echo</span> <span class="string">&quot;unknown&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">timeout</span> 60 go install tailscale.com/cmd/derper@latest 2&gt;&amp;1; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">local</span> new_version</span><br><span class="line">        new_version=$(/root/go/bin/derper --version 2&gt;&amp;1 || <span class="built_in">echo</span> <span class="string">&quot;unknown&quot;</span>)</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> [ <span class="string">&quot;<span class="variable">$current_version</span>&quot;</span> != <span class="string">&quot;<span class="variable">$new_version</span>&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">            <span class="built_in">log</span> <span class="string">&quot;derper 已从 <span class="variable">$current_version</span> 更新至 <span class="variable">$new_version</span>&quot;</span></span><br><span class="line">            <span class="built_in">return</span> 0</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            <span class="built_in">log</span> <span class="string">&quot;derper 已是最新版本: <span class="variable">$current_version</span>&quot;</span></span><br><span class="line">        <span class="keyword">fi</span></span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        <span class="built_in">log</span> <span class="string">&quot;derper 更新失败&quot;</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">    <span class="built_in">return</span> 1</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">update_tailscale</span></span>() &#123;</span><br><span class="line">    <span class="built_in">log</span> <span class="string">&quot;检查 Tailscale 更新...&quot;</span></span><br><span class="line">    </span><br><span class="line">    <span class="built_in">local</span> current_version</span><br><span class="line">    current_version=$(tailscale version 2&gt;&amp;1 | <span class="built_in">head</span> -1)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">timeout</span> 60 bash -c <span class="string">&#x27;apt-get update &amp;&amp; apt-get install -y --only-upgrade tailscale&#x27;</span> 2&gt;&amp;1; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">local</span> new_version</span><br><span class="line">        new_version=$(tailscale version 2&gt;&amp;1 | <span class="built_in">head</span> -1)</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> [ <span class="string">&quot;<span class="variable">$current_version</span>&quot;</span> != <span class="string">&quot;<span class="variable">$new_version</span>&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">            <span class="built_in">log</span> <span class="string">&quot;Tailscale 已从 <span class="variable">$current_version</span> 更新至 <span class="variable">$new_version</span>&quot;</span></span><br><span class="line">            <span class="built_in">return</span> 0</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            <span class="built_in">log</span> <span class="string">&quot;Tailscale 已是最新版本: <span class="variable">$current_version</span>&quot;</span></span><br><span class="line">        <span class="keyword">fi</span></span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        <span class="built_in">log</span> <span class="string">&quot;Tailscale 更新失败&quot;</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">    <span class="built_in">return</span> 1</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">main</span></span>() &#123;</span><br><span class="line">    <span class="built_in">log</span> <span class="string">&quot;===== 开始自动更新 =====&quot;</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> update_derper; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">log</span> <span class="string">&quot;重启 derper 服务...&quot;</span></span><br><span class="line">        systemctl restart <span class="string">&#x27;tailscale-derp@*&#x27;</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> update_tailscale; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">log</span> <span class="string">&quot;重启 tailscaled 服务...&quot;</span></span><br><span class="line">        systemctl restart tailscaled</span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">    </span><br><span class="line">    <span class="built_in">log</span> <span class="string">&quot;===== 自动更新完成 =====&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">main <span class="string">&quot;<span class="variable">$@</span>&quot;</span></span><br></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><span class="line">sudo <span class="built_in">chmod</span> +x /usr/local/bin/update-tailscale-derp.sh</span><br></pre></td></tr></table></figure><h3 id="配置定时任务"><a href="#配置定时任务" class="headerlink" title="配置定时任务"></a>配置定时任务</h3><p>用 systemd timer 每天凌晨 4 点跑一次更新。</p><p>创建 <code>/etc/systemd/system/update-tailscale-derp.service</code>：</p><figure class="highlight ini"><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><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Update Tailscale and DERP Server</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Type</span>=<span class="literal">on</span>eshot</span><br><span class="line"><span class="attr">ExecStart</span>=/usr/local/bin/update-tailscale-derp.sh</span><br><span class="line"><span class="attr">Environment</span>=<span class="string">&quot;PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&quot;</span></span><br><span class="line"><span class="attr">Environment</span>=<span class="string">&quot;HOME=/root&quot;</span></span><br></pre></td></tr></table></figure><p>创建 <code>/etc/systemd/system/update-tailscale-derp.timer</code>：</p><figure class="highlight ini"><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><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Daily Update for Tailscale and DERP Server</span><br><span class="line"></span><br><span class="line"><span class="section">[Timer]</span></span><br><span class="line"><span class="attr">OnCalendar</span>=*-*-* <span class="number">04</span>:<span class="number">00</span>:<span class="number">00</span></span><br><span class="line"><span class="attr">Persistent</span>=<span class="literal">false</span></span><br><span class="line"><span class="attr">RandomizedDelaySec</span>=<span class="number">300</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=timers.target</span><br></pre></td></tr></table></figure><p>启用：</p><figure class="highlight bash"><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><span class="line">sudo systemctl daemon-reload</span><br><span class="line">sudo systemctl <span class="built_in">enable</span> --now update-tailscale-derp.timer</span><br><span class="line"></span><br><span class="line"><span class="comment"># 验证定时器状态</span></span><br><span class="line">sudo systemctl list-timers update-tailscale-derp.timer</span><br></pre></td></tr></table></figure><hr><h2 id="防火墙配置"><a href="#防火墙配置" class="headerlink" title="防火墙配置"></a>防火墙配置</h2><p>别忘了放行端口：</p><figure class="highlight bash"><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><span class="line"><span class="comment"># 如果使用 ufw</span></span><br><span class="line">sudo ufw allow 4501/tcp   <span class="comment"># HTTPS (DERP)</span></span><br><span class="line">sudo ufw allow 4502/udp   <span class="comment"># STUN</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 如果使用 iptables</span></span><br><span class="line">sudo iptables -A INPUT -p tcp --dport 4501 -j ACCEPT</span><br><span class="line">sudo iptables -A INPUT -p udp --dport 4502 -j ACCEPT</span><br></pre></td></tr></table></figure><h2 id="在-Tailscale-中配置自定义-DERP-节点"><a href="#在-Tailscale-中配置自定义-DERP-节点" class="headerlink" title="在 Tailscale 中配置自定义 DERP 节点"></a>在 Tailscale 中配置自定义 DERP 节点</h2><p>DERP 跑起来了还不够，得告诉 Tailscale 它的存在。</p><p>去 <a href="https://login.tailscale.com/admin/acls">Tailscale ACL 配置页面</a>，加上 <code>derpMap</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><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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;derpMap&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;OmitDefaultRegions&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;Regions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;900&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">                <span class="attr">&quot;RegionID&quot;</span><span class="punctuation">:</span> <span class="number">900</span><span class="punctuation">,</span></span><br><span class="line">                <span class="attr">&quot;RegionCode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;myderp&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="attr">&quot;RegionName&quot;</span><span class="punctuation">:</span> <span class="string">&quot;My DERP Server&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="attr">&quot;Nodes&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">                    <span class="punctuation">&#123;</span></span><br><span class="line">                        <span class="attr">&quot;Name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;myderp-1&quot;</span><span class="punctuation">,</span></span><br><span class="line">                        <span class="attr">&quot;RegionID&quot;</span><span class="punctuation">:</span> <span class="number">900</span><span class="punctuation">,</span></span><br><span class="line">                        <span class="attr">&quot;HostName&quot;</span><span class="punctuation">:</span> <span class="string">&quot;derper.example.com&quot;</span><span class="punctuation">,</span></span><br><span class="line">                        <span class="attr">&quot;DERPPort&quot;</span><span class="punctuation">:</span> <span class="number">4501</span><span class="punctuation">,</span></span><br><span class="line">                        <span class="attr">&quot;STUNPort&quot;</span><span class="punctuation">:</span> <span class="number">4502</span></span><br><span class="line">                    <span class="punctuation">&#125;</span></span><br><span class="line">                <span class="punctuation">]</span></span><br><span class="line">            <span class="punctuation">&#125;</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><code>OmitDefaultRegions</code> 设成 <code>false</code>，保留官方节点当备选。<code>RegionID</code> 用 900 以上的数字，别跟官方的撞了。</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><span class="line">tailscale netcheck</span><br></pre></td></tr></table></figure><p>能看到你的自定义节点和延迟信息就说明配置生效了。</p><hr><h2 id="未来展望"><a href="#未来展望" class="headerlink" title="未来展望"></a>未来展望</h2><p>说实话现在这套部署流程还是偏复杂了——装 Go、编译、搞证书、写一堆 systemd 文件，步骤不少。理想情况应该是 Docker 一把梭，再丢到 Nginx 后面，对外只开 80 和 443。</p><p>但目前 Tailscale 官方不太推荐反代 derper，因为 DERP 协议涉及 WebSocket 升级和长连接，反代容易出问题。所以这事暂时先放着，后面有新进展再更新。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;为什么需要-Tailscale？&quot;&gt;&lt;a href=&quot;#为什么需要-Tailscale？&quot; class=&quot;headerlink&quot; title=&quot;为什么需要 Tailscale？&quot;&gt;&lt;/a&gt;为什么需要 Tailscale？&lt;/h2&gt;&lt;p&gt;AI推动了硬件销售和软件迭代</summary>
      
    
    
    
    
    <category term="AI辅助" scheme="https://blog.jugg.xyz/tags/AI%E8%BE%85%E5%8A%A9/"/>
    
    <category term="Tailscale" scheme="https://blog.jugg.xyz/tags/Tailscale/"/>
    
    <category term="DERP" scheme="https://blog.jugg.xyz/tags/DERP/"/>
    
    <category term="WireGuard" scheme="https://blog.jugg.xyz/tags/WireGuard/"/>
    
  </entry>
  
  <entry>
    <title>破界与融合：Windows 下 Linux 兼容层的三十年技术迭代史</title>
    <link href="https://blog.jugg.xyz/2026/02/28/musings/breaking-boundaries-and-convergence-three-decade-technical-evolution-history-of-linux-compatibility-layer-on-windows/"/>
    <id>https://blog.jugg.xyz/2026/02/28/musings/breaking-boundaries-and-convergence-three-decade-technical-evolution-history-of-linux-compatibility-layer-on-windows/</id>
    <published>2026-02-28T06:58:19.000Z</published>
    <updated>2026-05-02T11:24:14.332Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><strong>摘要</strong>：Windows Subsystem for Linux（WSL）是 Microsoft 在 Windows 操作系统上提供原生 Linux 兼容性的技术方案。本文系统性地回顾了从 Windows NT 时代的 POSIX 子系统，到 Windows Services for UNIX（SFU）&#x2F;Interix，再到 WSL 1 系统调用翻译层和 WSL 2 轻量级虚拟机的完整技术演进历程。</p></blockquote><hr><h2 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h2><ol><li><a href="#1-%E5%BC%95%E8%A8%80">引言</a></li><li><a href="#2-Windows-NT-%E6%9E%B6%E6%9E%84%E4%B8%8E%E7%8E%AF%E5%A2%83%E5%AD%90%E7%B3%BB%E7%BB%9F%E6%A8%A1%E5%9E%8B">Windows NT 架构与环境子系统模型</a></li><li><a href="#3-Microsoft-POSIX-%E5%AD%90%E7%B3%BB%E7%BB%9F%EF%BC%881993%E2%80%942001%EF%BC%89">Microsoft POSIX 子系统（1993—2001）</a></li><li><a href="#4-Windows-Services-for-UNIX-%E4%B8%8E-Interix%EF%BC%881999%E2%80%942012%EF%BC%89">Windows Services for UNIX 与 Interix（1999—2012）</a></li><li><a href="#5-WSL-1%EF%BC%9A%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8%E7%BF%BB%E8%AF%91%E5%B1%82%E6%9E%B6%E6%9E%84%EF%BC%882016%E2%80%94%EF%BC%89">WSL 1：系统调用翻译层架构（2016—）</a></li><li><a href="#6-WSL-2%EF%BC%9A%E8%BD%BB%E9%87%8F%E7%BA%A7%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%9E%B6%E6%9E%84%EF%BC%882019%E2%80%94%EF%BC%89">WSL 2：轻量级虚拟机架构（2019—）</a></li><li><a href="#7-WSL-1-%E4%B8%8E-WSL-2-%E7%9A%84%E6%8A%80%E6%9C%AF%E5%AF%B9%E6%AF%94">WSL 1 与 WSL 2 的技术对比</a></li><li><a href="#8-WSL-%E7%94%9F%E6%80%81%E7%B3%BB%E7%BB%9F%E7%9A%84%E7%8E%B0%E4%BB%A3%E6%89%A9%E5%B1%95">WSL 生态系统的现代扩展</a></li><li><a href="#9-%E6%9C%AA%E6%9D%A5%E5%B1%95%E6%9C%9B%E4%B8%8E%E6%80%BB%E7%BB%93">未来展望与总结</a></li><li><a href="#10-%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE">参考文献</a></li></ol><hr><h2 id="1-引言"><a href="#1-引言" class="headerlink" title="1. 引言"></a>1. 引言</h2><p>在操作系统发展的漫长历史中，Windows 与 Unix&#x2F;Linux 两大阵营之间的鸿沟一直是开发者面临的核心挑战之一。企业级服务器运行 Linux，桌面用户使用 Windows，开发者不得不在两个生态系统之间频繁切换。这种割裂催生了数十年来多种跨平台兼容性方案的探索。</p><p>Microsoft 对 POSIX&#x2F;Unix 兼容性的投入可以追溯到 Windows NT 操作系统的最初设计阶段。从 1993 年 Windows NT 3.1 中内置的 POSIX 子系统，到 1999 年收购 Softway Systems 获得 Interix 技术并推出 Windows Services for UNIX（SFU），再到 2016 年革命性的 Windows Subsystem for Linux（WSL）的发布以及 2019 年 WSL 2 的架构重构，这条技术演进路线长达三十余年，反映了 Microsoft 对 Unix&#x2F;Linux 兼容性的战略思考从被动合规到主动拥抱的深刻转变。</p><p>本文将系统性地梳理这一技术演进的完整脉络，深入分析每个阶段的架构设计原理、关键技术实现与工程权衡，为读者呈现 Windows 平台上 Linux 兼容性技术发展的全景图。</p><hr><h2 id="2-Windows-NT-架构与环境子系统模型"><a href="#2-Windows-NT-架构与环境子系统模型" class="headerlink" title="2. Windows NT 架构与环境子系统模型"></a>2. Windows NT 架构与环境子系统模型</h2><h3 id="2-1-分层架构设计"><a href="#2-1-分层架构设计" class="headerlink" title="2.1 分层架构设计"></a>2.1 分层架构设计</h3><p>要理解 WSL 的技术渊源，必须首先理解 Windows NT 的架构设计。Windows NT 采用分层的混合微内核（Hybrid Kernel）架构，将系统划分为用户模式（User Mode）和内核模式（Kernel Mode）两个特权层次。</p><p><strong>内核模式</strong>包含以下核心组件：</p><ul><li><strong>微内核（Microkernel）</strong>：负责一级中断处理、线程调度和同步原语等最底层功能</li><li><strong>执行体（Executive）</strong>：包括对象管理器（Object Manager）、进程管理器（Process Manager）、虚拟内存管理器（VMM）、I&#x2F;O 管理器、安全引用监视器（Security Reference Monitor）和本地过程调用（LPC）设施等</li><li><strong>硬件抽象层（HAL）</strong>：将硬件差异抽象化，实现内核的跨平台可移植性</li></ul><p>这些内核模式组件被编译进单一的 <code>ntoskrnl.exe</code> 映像文件中，它们之间通过函数调用实现高效通信，而非采用纯微内核架构中进程间通信的方式。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/breaking-boundaries-and-convergence-three-decade-technical-evolution-history-of-linux-compatibility-layer-on-windows/nt-architecture.svg" alt="Windows NT 分层架构" title="">                </div>                <div class="image-caption">Windows NT 分层架构</div>            </figure><h3 id="2-2-环境子系统模型"><a href="#2-2-环境子系统模型" class="headerlink" title="2.2 环境子系统模型"></a>2.2 环境子系统模型</h3><p>Windows NT 架构最具前瞻性的设计之一是 <strong>环境子系统（Environment Subsystem）</strong> 模型。该模型允许多个操作系统的 API 层并行运行于用户模式之上，每个子系统实现不同的编程接口集合。用户模式应用程序不直接访问 NT 内核服务，而是通过子系统特定的动态链接库（DLL）进行调用，这些 DLL 将 API 调用翻译为相应的、未公开的 Windows NT 系统服务调用。</p><p>Windows NT 最初设计了三个环境子系统：</p><table><thead><tr><th align="left">子系统</th><th align="left">说明</th><th align="left">生命周期</th></tr></thead><tbody><tr><td align="left"><strong>Win32</strong></td><td align="left">主要的 Windows 应用程序接口</td><td align="left">NT 3.1 至今</td></tr><tr><td align="left"><strong>POSIX</strong></td><td align="left">提供 POSIX.1 标准兼容环境</td><td align="left">NT 3.1 — Windows 2000</td></tr><tr><td align="left"><strong>OS&#x2F;2</strong></td><td align="left">兼容 IBM OS&#x2F;2 应用程序</td><td align="left">NT 3.1 — Windows 2000</td></tr></tbody></table><p>这种多子系统设计使得 Windows NT 在理论上能够为不同操作系统的应用程序提供原生运行环境，奠定了日后 POSIX 兼容性乃至 WSL 技术的根基。</p><hr><h2 id="3-Microsoft-POSIX-子系统（1993—2001）"><a href="#3-Microsoft-POSIX-子系统（1993—2001）" class="headerlink" title="3. Microsoft POSIX 子系统（1993—2001）"></a>3. Microsoft POSIX 子系统（1993—2001）</h2><h3 id="3-1-历史背景与动机"><a href="#3-1-历史背景与动机" class="headerlink" title="3.1 历史背景与动机"></a>3.1 历史背景与动机</h3><p>Microsoft 在 Windows NT 中内置 POSIX 子系统的直接推动力来自美国联邦政府的采购要求。1988 年，美国国家标准与技术研究院（NIST）发布了联邦信息处理标准 FIPS 151-2，要求政府采购的操作系统必须符合 POSIX 标准。Microsoft 为了确保 Windows NT 能够参与利润丰厚的政府合同竞标，将 POSIX 子系统作为标配组件纳入了 Windows NT。</p><p>Windows NT 3.5、3.51 和 4.0 均通过了 FIPS 151-2 认证，满足了政府采购的合规要求。</p><h3 id="3-2-技术实现"><a href="#3-2-技术实现" class="headerlink" title="3.2 技术实现"></a>3.2 技术实现</h3><p>POSIX 子系统的核心组件包括：</p><ul><li><strong><code>PSXSS.EXE</code></strong>：POSIX 子系统服务器进程，作为 Win32 二进制程序运行，是 NT 默认子系统之一</li><li><strong><code>PSXDLL.DLL</code></strong>：动态链接库，负责处理对 POSIX 子系统的调用，将 POSIX API 翻译为 NT 原生系统调用</li></ul><p>该子系统遵循 POSIX.1 标准（IEEE Std 1003.1-1988, ISO&#x2F;IEC 9945-1:1990），涵盖以下 C 级 API 功能域：</p><ul><li>进程创建与控制（<code>fork</code>、<code>exec</code> 系列）</li><li>信号处理（<code>signal</code>、<code>kill</code>）</li><li>文件系统 I&#x2F;O（<code>open</code>、<code>read</code>、<code>write</code>、<code>close</code>）</li><li>标准 C 库函数</li></ul><h3 id="3-3-局限性分析"><a href="#3-3-局限性分析" class="headerlink" title="3.3 局限性分析"></a>3.3 局限性分析</h3><p>POSIX 子系统的实现被广泛认为是一个”最低限度合规”（bare minimum compliance）的方案，其设计目标是通过认证而非提供实用的 Unix 环境。主要局限包括：</p><ol><li><strong>仅实现 POSIX.1 标准</strong>：不支持 POSIX.2（shell 与工具集）、POSIX 线程（pthreads）或 POSIX 进程间通信（IPC）</li><li><strong>几乎不包含用户级工具</strong>：开箱即用仅提供 <code>pax</code>（POSIX 归档工具）等极少数工具，Windows NT 4.0 Resource Kit 中额外提供了少量 Unix 命令</li><li><strong>与 Win32 子系统完全隔离</strong>：运行在 POSIX 子系统中的程序无法访问 Win32 特性（如 Win32 ACL）</li><li><strong>无网络编程支持</strong>：不提供 BSD 套接字接口</li><li><strong>无图形界面支持</strong>：完全是命令行环境</li></ol><h3 id="3-4-历史评价"><a href="#3-4-历史评价" class="headerlink" title="3.4 历史评价"></a>3.4 历史评价</h3><p>POSIX 子系统本质上是一个合规性驱动的政策产物。它成功地帮助 Microsoft 满足了政府采购要求，但从未成为实际的 Unix 开发或运行平台。随着政府采购要求的变化和更完善替代方案的出现，该子系统在 Windows XP 中被移除。</p><hr><h2 id="4-Windows-Services-for-UNIX-与-Interix（1999—2012）"><a href="#4-Windows-Services-for-UNIX-与-Interix（1999—2012）" class="headerlink" title="4. Windows Services for UNIX 与 Interix（1999—2012）"></a>4. Windows Services for UNIX 与 Interix（1999—2012）</h2><h3 id="4-1-Interix-的起源"><a href="#4-1-Interix-的起源" class="headerlink" title="4.1 Interix 的起源"></a>4.1 Interix 的起源</h3><p>POSIX 子系统的局限性催生了第三方解决方案的需求。1996 年，Softway Systems 公司开发了名为 <strong>OpenNT</strong> 的产品（后更名为 Interix），这是一个在 Windows NT 上运行的完整 POSIX 兼容 Unix 子系统。Interix 充分利用了 Windows NT 的环境子系统架构，作为与 Win32 并列的原生子系统运行，而非简单的仿真层。这一设计赋予了它远超传统兼容方案的性能和稳定性优势。</p><p>1999 年，Microsoft 收购了 Softway Systems，将 Interix 技术整合到自身的产品线中。</p><h3 id="4-2-SFU-的版本演进"><a href="#4-2-SFU-的版本演进" class="headerlink" title="4.2 SFU 的版本演进"></a>4.2 SFU 的版本演进</h3><p>Windows Services for UNIX（SFU）经历了以下版本迭代：</p><table><thead><tr><th align="left">版本</th><th align="left">发布时间</th><th align="left">关键特性</th></tr></thead><tbody><tr><td align="left"><strong>SFU 1.0</strong></td><td align="left">1999 年 2 月</td><td align="left">使用 MKS Toolkit 提供 Unix 功能</td></tr><tr><td align="left"><strong>SFU 2.0</strong></td><td align="left">2000 年</td><td align="left">继续基于 MKS Toolkit</td></tr><tr><td align="left"><strong>Interix 2.2</strong></td><td align="left">2001 年</td><td align="left">作为独立产品发布</td></tr><tr><td align="left"><strong>SFU 3.0</strong></td><td align="left">2002 年 5 月</td><td align="left"><strong>整合 Interix 子系统</strong>，取代 MKS Toolkit</td></tr><tr><td align="left"><strong>SFU 3.5</strong></td><td align="left">2004 年 1 月</td><td align="left">最后的独立版本，免费下载，增加国际化和 pthreads 支持</td></tr><tr><td align="left"><strong>SUA</strong></td><td align="left">2005 年起</td><td align="left">集成到 Windows Server 2003 R2 及后续版本，更名为 Subsystem for UNIX-based Applications</td></tr></tbody></table><p>SFU 3.0 是一个重要的转折点——Interix 子系统取代了此前的 MKS Toolkit，标志着 Microsoft 从授权第三方工具转向提供真正的原生 Unix 子系统。</p><h3 id="4-3-Interix-技术架构"><a href="#4-3-Interix-技术架构" class="headerlink" title="4.3 Interix 技术架构"></a>4.3 Interix 技术架构</h3><p>Interix 作为 Windows NT 内核之上的原生环境子系统运行，其技术架构具有以下特点：</p><p><strong>核心运行机制</strong>：</p><p>Interix 与 Win32 子系统并行运行于 Windows NT 内核之上。Unix 应用程序通过 Interix 子系统 DLL 将 POSIX&#x2F;Unix API 调用翻译为 NT 原生系统服务调用。这种原生子系统的设计确保了良好的性能表现，避免了仿真层的额外开销。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/breaking-boundaries-and-convergence-three-decade-technical-evolution-history-of-linux-compatibility-layer-on-windows/interix-architecture.svg" alt="Interix 技术架构" title="">                </div>                <div class="image-caption">Interix 技术架构</div>            </figure><p><strong>功能特性（Interix 3.5）</strong>：</p><ul><li><strong>Unix 工具集</strong>：提供超过 350 个 Unix 工具（<code>vi</code>、<code>ksh</code>、<code>csh</code>、<code>ls</code>、<code>cat</code>、<code>awk</code>、<code>grep</code>、<code>kill</code> 等）</li><li><strong>开发工具链</strong>：GCC 3.3 编译器、GNU 调试器（GDB），以及 Microsoft Visual Studio 命令行 C&#x2F;C++ 编译器的 <code>cc</code> 兼容封装</li><li><strong>API 兼容性</strong>：支持超过 2,000 个 Unix API</li><li><strong>Shell 环境</strong>：KornShell（ksh）和 C Shell（csh）</li><li><strong>脚本语言</strong>：Perl、awk、sed、Tcl&#x2F;Tk</li><li><strong>进程管理</strong>：支持 <code>fork()</code>、<code>pthreads</code>、作业控制、信号、套接字和共享内存</li><li><strong>文件系统</strong>：支持单根文件系统模型；提供 NFS 服务器和客户端</li><li><strong>安全特性</strong>：支持 Unix <code>setuid</code>、<code>root</code> 权限模型</li><li><strong>图形支持</strong>：支持 X11 客户端应用程序和库（不包含 X Server）</li><li><strong>混合模式（5.2 版起）</strong>：允许 Unix 程序链接 Windows DLL，支持 64 位 CPU</li></ul><h3 id="4-4-SUA-的衰落"><a href="#4-4-SUA-的衰落" class="headerlink" title="4.4 SUA 的衰落"></a>4.4 SUA 的衰落</h3><p>随着 Windows Server 2003 R2 将 SFU 组件整合为 Subsystem for UNIX-based Applications（SUA），该技术经历了以下衰落过程：</p><ul><li>Windows Vista&#x2F;7：仅在 Enterprise 和 Ultimate 版本中可用</li><li>Windows 8&#x2F;Server 2012：降级为已弃用的独立下载</li><li>Windows 10：完全不可用</li></ul><p>SUA 的衰落与 Linux 生态系统的崛起密不可分——开发者不再满足于在 Windows 上运行移植的 Unix 应用程序，而是需要真正的 Linux 兼容性。这为 WSL 的诞生铺平了道路。</p><hr><h2 id="5-WSL-1：系统调用翻译层架构（2016—）"><a href="#5-WSL-1：系统调用翻译层架构（2016—）" class="headerlink" title="5. WSL 1：系统调用翻译层架构（2016—）"></a>5. WSL 1：系统调用翻译层架构（2016—）</h2><h3 id="5-1-开发背景"><a href="#5-1-开发背景" class="headerlink" title="5.1 开发背景"></a>5.1 开发背景</h3><p>2016 年，Microsoft 在 Build 大会上发布了 Windows Subsystem for Linux（WSL）—— 一种全新的架构方案，允许<strong>未经修改的 Linux ELF64 二进制文件</strong>直接在 Windows 10 上运行。与此前的 POSIX 子系统和 SFU&#x2F;Interix 不同，WSL 的目标不再是移植 Unix 工具到 Windows，而是让原生 Linux 二进制程序无需任何修改即可直接执行。</p><p>要理解 WSL 1 的技术架构，需要追溯其两项核心技术——<strong>Pico 进程（Pico Process）</strong> 和 <strong>Pico 提供程序驱动（Pico Provider Driver）</strong>——的起源。2011 年，Microsoft Research 发布了 <strong>Drawbridge</strong> 研究项目，旨在探索一种资源开销远低于传统虚拟机的轻量级应用沙箱方案。Drawbridge 提出了两个关键概念：一是 <strong>Pico 进程</strong>，一种极简的进程容器，剥离了传统操作系统进程中的大部分内核服务，仅保留约 45 个固定语义的内核下行调用（downcall）；二是 <strong>Library OS（库操作系统）</strong>，即将 OS personality（操作系统个性——应用程序所依赖的操作系统接口与行为）封装为一个库，运行在 Pico 进程内部，为应用程序提供其所期望的运行环境。这一设计以极低的资源开销实现了安全隔离与兼容性保障。</p><p>Drawbridge 的研究成果随后被产品化并集成到 Windows 内核中，其首次产品化应用是 <strong>Project Astoria</strong>（约 2015 年）。该项目利用 Pico 进程和 Pico 提供程序驱动，在 Windows 内核之上承载 Android 运行时，使 Android 应用能够在 Windows 10 Mobile 上运行。然而，随着 Windows Phone 战略的终止，Project Astoria 于 2016 年初被取消。项目虽未延续，但它在 Windows 内核中建立的 Pico 进程基础设施被完整保留了下来。</p><p>WSL 团队正是在此基础上进行了关键创新：编写了全新的 Pico 提供程序驱动 <code>lxcore.sys</code>，将 Pico 进程的用途从承载 Android 运行时转变为<strong>翻译 Linux 系统调用</strong>，从而实现了在 Windows 内核上直接运行未经修改的 Linux ELF64 二进制程序。</p><h3 id="5-2-核心技术：Pico-进程"><a href="#5-2-核心技术：Pico-进程" class="headerlink" title="5.2 核心技术：Pico 进程"></a>5.2 核心技术：Pico 进程</h3><p>Pico 进程是 WSL 1 架构的基石。它是一种特殊类型的 Windows 进程，具有以下关键特征：</p><ol><li><strong>最小化的内核数据结构</strong>：Pico 进程拥有精简的进程和线程数据结构，Windows 内核不直接管理其用户模式地址空间</li><li><strong>系统调用重定向</strong>：来自 Pico 进程用户模式的所有系统调用和异常都被 Windows 内核传递给其指定的 <strong>Pico 提供程序</strong> 处理</li><li><strong>轻量级隔离</strong>：Pico 进程将应用程序及其操作系统依赖从宿主 Windows 系统中解耦，在单一进程的用户模式地址空间中运行，资源消耗远低于虚拟机</li></ol><p>从 Windows 内核的视角来看，Pico 进程是一个”空壳”——内核仅负责进程的基本管理（创建、调度、终止），而将所有 Linux 特定的语义处理委托给 Pico 提供程序驱动。</p><h3 id="5-3-系统调用翻译机制"><a href="#5-3-系统调用翻译机制" class="headerlink" title="5.3 系统调用翻译机制"></a>5.3 系统调用翻译机制</h3><p>WSL 1 的核心能力由两个内核模式驱动程序提供：</p><ul><li><strong><code>lxss.sys</code></strong>：WSL 会话管理驱动，负责管理 Linux 实例的生命周期</li><li><strong><code>lxcore.sys</code></strong>：Linux 内核接口的核心实现驱动（Pico 提供程序驱动）</li></ul><p>这两个驱动程序是 Linux 兼容内核接口的<strong>全新实现（clean-room implementation）</strong>，不包含任何 Linux 内核代码。其系统调用翻译流程如下：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/breaking-boundaries-and-convergence-three-decade-technical-evolution-history-of-linux-compatibility-layer-on-windows/wsl1-syscall-flow.svg" alt="WSL 1 系统调用翻译流程" title="">                </div>                <div class="image-caption">WSL 1 系统调用翻译流程</div>            </figure><p><strong>翻译过程的详细步骤</strong>：</p><ol><li>Linux 应用程序在 Pico 进程中执行 Linux 系统调用（如 <code>int 0x80</code> 或 <code>syscall</code> 指令）</li><li>Windows NT 内核拦截该系统调用请求，识别其来源为 Pico 进程</li><li>内核将请求转发给注册的 Pico 提供程序驱动 <code>lxcore.sys</code></li><li><code>lxcore.sys</code> 将 Linux 系统调用翻译为对应的 Windows NT 内核 API 调用</li><li>对于无直接对应的系统调用（如 <code>fork</code>），<code>lxcore.sys</code> 在内核模式中直接实现其语义</li></ol><h3 id="5-4-fork-系统调用的翻译实现"><a href="#5-4-fork-系统调用的翻译实现" class="headerlink" title="5.4 fork 系统调用的翻译实现"></a>5.4 <code>fork</code> 系统调用的翻译实现</h3><p><code>fork</code> 是 Unix&#x2F;Linux 进程模型的核心原语，而 Windows 原生不提供等价机制。<code>lxcore.sys</code> 对 <code>fork</code> 的实现策略如下：</p><ol><li><strong>准备阶段</strong>：为新进程的创建初始化必要的数据结构</li><li><strong>进程创建</strong>：调用 NT 内核内部 API 创建新的 Pico 进程</li><li><strong>地址空间复制</strong>：将父进程的用户模式地址空间复制到子进程</li><li><strong>上下文复制</strong>：复制寄存器状态、文件描述符表等进程上下文</li><li><strong>返回值设置</strong>：按照 POSIX 语义，在父进程中返回子进程 PID，在子进程中返回 0</li></ol><p>这种实现虽然功能正确，但由于需要完整复制地址空间，其性能开销显著高于 Linux 内核的原生 <code>fork</code> 实现（后者利用写时复制 &#x2F; Copy-on-Write 优化）。</p><h3 id="5-5-文件系统层"><a href="#5-5-文件系统层" class="headerlink" title="5.5 文件系统层"></a>5.5 文件系统层</h3><p>WSL 1 实现了两种文件系统：</p><ul><li><strong>VolFS</strong>：提供完整的 Linux 文件系统语义，支持大小写敏感的文件名、Unix 权限位、inode、硬链接和符号链接等。WSL 的 Linux 根文件系统挂载于此</li><li><strong>DrvFS</strong>：将 Windows 驱动器（如 <code>C:</code>）映射为 <code>/mnt/c</code>，允许 Linux 进程访问 Windows 文件系统，尽管 NTFS 的语义与 Linux 文件系统存在差异</li></ul><p>文件系统 I&#x2F;O 是 WSL 1 最显著的性能瓶颈之一。由于所有文件系统操作都需要经过系统调用翻译层，I&#x2F;O 密集型操作（如 <code>git clone</code>、<code>npm install</code>）的性能显著低于原生 Linux。</p><h3 id="5-6-Linux-实例管理"><a href="#5-6-Linux-实例管理" class="headerlink" title="5.6 Linux 实例管理"></a>5.6 Linux 实例管理</h3><p>WSL 1 中的 Linux 实例由 <code>lxss.sys</code> 驱动管理。每个 WSL 实例包含：</p><ul><li><strong>init 进程</strong>：WSL 自己的 init 进程（非 systemd 或 SysV init）</li><li><strong>进程树</strong>：完整的 Linux 进程层次结构</li><li><strong>虚拟文件系统</strong>：包括 <code>/proc</code>、<code>/sys</code> 等 Linux 虚拟文件系统的实现</li><li><strong>网络栈</strong>：共享 Windows 的网络栈，而非独立的 Linux 网络栈</li></ul><h3 id="5-7-局限性"><a href="#5-7-局限性" class="headerlink" title="5.7 局限性"></a>5.7 局限性</h3><p>WSL 1 的系统调用翻译架构存在以下固有局限：</p><ol><li><strong>不完整的系统调用覆盖</strong>：并非所有约 400 个 Linux 系统调用都得到了实现，导致某些应用程序无法运行</li><li><strong>文件系统性能低下</strong>：I&#x2F;O 密集型操作因翻译层开销而严重受限</li><li><strong>无真正的 Linux 内核</strong>：内核模块（如 FUSE、eBPF）无法加载</li><li><strong>Docker 不兼容</strong>：Docker 依赖的 <code>cgroups</code>、<code>namespaces</code> 等内核特性无法通过翻译层实现</li><li><strong>无 GPU 支持</strong>：不支持 GPU 直通或硬件加速</li></ol><hr><h2 id="6-WSL-2：轻量级虚拟机架构（2019—）"><a href="#6-WSL-2：轻量级虚拟机架构（2019—）" class="headerlink" title="6. WSL 2：轻量级虚拟机架构（2019—）"></a>6. WSL 2：轻量级虚拟机架构（2019—）</h2><h3 id="6-1-架构革新"><a href="#6-1-架构革新" class="headerlink" title="6.1 架构革新"></a>6.1 架构革新</h3><p>WSL 2 在 2019 年发布，代表了一次根本性的架构转变。Microsoft 放弃了系统调用翻译方案，转而采用<strong>轻量级虚拟机（Lightweight Utility VM）</strong> 方案——在一个高度优化的 Hyper-V 虚拟机中运行<strong>真正的 Linux 内核</strong>。</p><p>这一架构决策的核心动机在于：</p><ul><li>实现 <strong>100% 的系统调用兼容性</strong>，解决 WSL 1 的兼容性瓶颈</li><li>大幅提升文件系统 I&#x2F;O 性能</li><li>支持 Docker、Kubernetes 等依赖 Linux 内核特性的容器技术</li><li>为 GPU 直通等高级功能提供基础</li></ul><h3 id="6-2-虚拟化架构"><a href="#6-2-虚拟化架构" class="headerlink" title="6.2 虚拟化架构"></a>6.2 虚拟化架构</h3><p>WSL 2 利用 Windows 的 <strong>Virtual Machine Platform</strong>（虚拟机平台）组件——这是 Hyper-V 技术在所有 Windows 版本上可用的子集——来创建和管理轻量级虚拟机。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/breaking-boundaries-and-convergence-three-decade-technical-evolution-history-of-linux-compatibility-layer-on-windows/wsl2-vm-architecture.svg" alt="WSL 2 轻量级虚拟机架构" title="">                </div>                <div class="image-caption">WSL 2 轻量级虚拟机架构</div>            </figure><h3 id="6-3-Linux-内核"><a href="#6-3-Linux-内核" class="headerlink" title="6.3 Linux 内核"></a>6.3 Linux 内核</h3><p>WSL 2 运行的是由 Microsoft 定制调优的真正 Linux 内核，其关键特性包括：</p><ul><li><strong>版本更新</strong>：2024 年 7 月更新至 Linux 内核 6.6 LTS 版本</li><li><strong>定制优化</strong>：针对 WSL 使用场景优化了内核大小和启动性能，减少了 out-of-tree 补丁的需求</li><li><strong>标准更新渠道</strong>：通过 Windows Update 或 <code>wsl --update</code> 命令更新</li><li><strong>可定制性</strong>：用户可以编译和使用自定义 Linux 内核</li></ul><p>WSL 2 的 Linux 内核支持完整的内核特性集，包括：</p><ul><li><strong>容器支持</strong>：<code>cgroups v1/v2</code>、<code>namespaces</code>（PID、网络、挂载、用户等）</li><li><strong>内核模块</strong>：可加载内核模块支持</li><li><strong>eBPF</strong>：扩展的伯克利包过滤器支持</li><li><strong>FUSE</strong>：用户空间文件系统支持</li></ul><h3 id="6-4-WSL-System-Distro"><a href="#6-4-WSL-System-Distro" class="headerlink" title="6.4 WSL System Distro"></a>6.4 WSL System Distro</h3><p>WSL 2 引入了 <strong>WSL System Distro</strong>——一个基于 Azure Linux（前身为 CBL-Mariner）的轻量级、不可变 Linux 发行版。该系统负责：</p><ul><li>管理 WSL 虚拟机的生命周期</li><li>协调用户发行版和系统服务</li><li>简化自定义内核的构建和部署流程</li><li>提供 systemd 等基础服务的集成点</li></ul><h3 id="6-5-轻量级虚拟机的优化"><a href="#6-5-轻量级虚拟机的优化" class="headerlink" title="6.5 轻量级虚拟机的优化"></a>6.5 轻量级虚拟机的优化</h3><p>与传统虚拟机不同，WSL 2 的轻量级虚拟机在以下方面进行了深度优化：</p><table><thead><tr><th align="left">对比维度</th><th align="left">传统虚拟机</th><th align="left">WSL 2 轻量级 VM</th></tr></thead><tbody><tr><td align="left">启动时间</td><td align="left">数十秒至数分钟</td><td align="left">约 1-2 秒</td></tr><tr><td align="left">内存占用</td><td align="left">预分配固定大小</td><td align="left">动态分配，按需增长&#x2F;收缩</td></tr><tr><td align="left">磁盘占用</td><td align="left">完整 OS 镜像</td><td align="left">精简内核 + 最小文件系统</td></tr><tr><td align="left">与宿主集成</td><td align="left">需要额外配置</td><td align="left">文件系统、网络、剪贴板自动互通</td></tr><tr><td align="left">管理复杂度</td><td align="left">完整 VM 管理</td><td align="left">透明运行，用户无感知</td></tr></tbody></table><h3 id="6-6-内存管理"><a href="#6-6-内存管理" class="headerlink" title="6.6 内存管理"></a>6.6 内存管理</h3><p>WSL 2 实现了智能化的内存管理机制：</p><ul><li><strong>动态内存分配</strong>：虚拟机根据工作负载需求动态调整内存占用</li><li><strong>自动内存回收（Automatic Memory Reclaim）</strong>：2024 年 5 月成为稳定功能。WSL 自动将未使用的内存释放回 Windows 宿主系统，对内存受限的系统尤为重要</li><li><strong>自动磁盘回收（Automatic Disk Reclaim）</strong>：实验性功能，优化磁盘空间利用</li></ul><h3 id="6-7-网络架构"><a href="#6-7-网络架构" class="headerlink" title="6.7 网络架构"></a>6.7 网络架构</h3><p>WSL 2 提供两种网络模式：</p><p><strong>NAT 模式（默认）</strong>：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/breaking-boundaries-and-convergence-three-decade-technical-evolution-history-of-linux-compatibility-layer-on-windows/wsl2-nat-network.svg" alt="WSL 2 NAT 网络模式" title="">                </div>                <div class="image-caption">WSL 2 NAT 网络模式</div>            </figure><ul><li>WSL 环境拥有独立的、动态分配的 IP 地址</li><li>所有网络流量通过 Windows 宿主的网络栈路由</li><li>外部访问 WSL 服务需要端口转发</li></ul><p><strong>镜像网络模式（Mirrored Networking Mode）</strong>：</p><p>Windows 11 22H2 及更新版本引入的新模式，WSL 直接共享 Windows 的网络接口：</p><ul><li>原生 IPv6 支持</li><li>简化服务访问——WSL 和 Windows 共享相同的 IP 地址</li><li>改善 VPN 兼容性</li><li>通过 <code>.wslconfig</code> 文件中设置 <code>networkingMode=mirrored</code> 启用</li></ul><p>此外，<strong>DNS 隧道（DNS Tunneling）</strong> 在 2024 年的 Windows 11 中默认启用，增强了 VPN 环境下的 DNS 解析兼容性。</p><h3 id="6-8-文件系统互操作"><a href="#6-8-文件系统互操作" class="headerlink" title="6.8 文件系统互操作"></a>6.8 文件系统互操作</h3><p>WSL 2 的文件系统架构支持完全双向互操作：</p><ul><li><strong>Linux → Windows</strong>：通过 9P 协议文件服务器访问 Windows 文件系统（挂载于 <code>/mnt/c</code> 等）</li><li><strong>Windows → Linux</strong>：通过 <code>\\wsl$\&lt;发行版名&gt;</code> 网络路径访问 Linux 文件系统</li></ul><blockquote><p><strong>性能建议</strong>：为获得最佳 I&#x2F;O 性能，项目文件应存储在 Linux 文件系统（如 <code>/home/user</code>）中。跨操作系统边界的文件访问会因 9P 协议的开销而引入延迟。</p></blockquote><hr><h2 id="7-WSL-1-与-WSL-2-的技术对比"><a href="#7-WSL-1-与-WSL-2-的技术对比" class="headerlink" title="7. WSL 1 与 WSL 2 的技术对比"></a>7. WSL 1 与 WSL 2 的技术对比</h2><table><thead><tr><th align="left">特性</th><th align="left">WSL 1</th><th align="left">WSL 2</th></tr></thead><tbody><tr><td align="left"><strong>架构</strong></td><td align="left">系统调用翻译层</td><td align="left">轻量级虚拟机 + 真正的 Linux 内核</td></tr><tr><td align="left"><strong>Linux 内核</strong></td><td align="left">无（翻译模拟）</td><td align="left">真正的 Linux 内核（6.6 LTS）</td></tr><tr><td align="left"><strong>系统调用兼容性</strong></td><td align="left">部分（约 200-300 个）</td><td align="left">完全（与原生 Linux 一致）</td></tr><tr><td align="left"><strong>文件系统性能（Linux FS）</strong></td><td align="left">较慢</td><td align="left">极快（原生 ext4）</td></tr><tr><td align="left"><strong>跨文件系统性能</strong></td><td align="left">较快（直接访问 NTFS）</td><td align="left">较慢（通过 9P 协议）</td></tr><tr><td align="left"><strong>内存占用</strong></td><td align="left">较低</td><td align="left">较高（虚拟机开销）</td></tr><tr><td align="left"><strong>启动时间</strong></td><td align="left">即时</td><td align="left">约 1-2 秒</td></tr><tr><td align="left"><strong>Docker 支持</strong></td><td align="left">❌</td><td align="left">✅</td></tr><tr><td align="left"><strong>GPU 支持</strong></td><td align="left">❌</td><td align="left">✅</td></tr><tr><td align="left"><strong>内核模块</strong></td><td align="left">❌</td><td align="left">✅</td></tr><tr><td align="left"><strong>systemd</strong></td><td align="left">❌</td><td align="left">✅</td></tr><tr><td align="left"><strong>GUI 应用</strong></td><td align="left">需要第三方 X Server</td><td align="left">原生支持（WSLg）</td></tr><tr><td align="left"><strong>网络栈</strong></td><td align="left">共享 Windows 网络栈</td><td align="left">独立虚拟网络（支持镜像模式）</td></tr><tr><td align="left"><strong>IP 地址</strong></td><td align="left">与 Windows 相同</td><td align="left">独立 IP（NAT）或共享（镜像模式）</td></tr></tbody></table><p>WSL 1 与 WSL 2 可在同一系统上共存，用户可以根据具体工作负载选择最适合的架构。WSL 1 在跨文件系统访问性能和更低的资源占用方面仍有优势，而 WSL 2 在兼容性、Linux 文件系统性能和高级功能支持方面全面领先。</p><hr><h2 id="8-WSL-生态系统的现代扩展"><a href="#8-WSL-生态系统的现代扩展" class="headerlink" title="8. WSL 生态系统的现代扩展"></a>8. WSL 生态系统的现代扩展</h2><h3 id="8-1-WSLg：Linux-GUI-应用支持"><a href="#8-1-WSLg：Linux-GUI-应用支持" class="headerlink" title="8.1 WSLg：Linux GUI 应用支持"></a>8.1 WSLg：Linux GUI 应用支持</h3><p><strong>WSLg</strong>（Windows Subsystem for Linux GUI）为 Linux 图形用户界面应用程序提供了原生 Windows 集成支持，消除了手动配置 X Server 的需要。</p><p><strong>架构设计</strong>：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/breaking-boundaries-and-convergence-three-decade-technical-evolution-history-of-linux-compatibility-layer-on-windows/wslg-architecture.svg" alt="WSLg 架构" title="">                </div>                <div class="image-caption">WSLg 架构</div>            </figure><p>WSLg 的关键技术组件：</p><ul><li><strong>Weston Compositor</strong>：使用扩展的 RDP 后端作为 Wayland 和 X11 的合成器</li><li><strong>RAIL&#x2F;VAIL Shell</strong>：Remote&#x2F;Virtual Applications Integrated Locally，将 Linux 应用窗口作为独立窗口显示在 Windows 桌面上</li><li><strong>RDP 虚拟通道</strong>：在 Linux VM 中的 Weston RDP Server 和 Windows 上的 <code>mstsc</code> RDP Client 之间建立自定义通信通道</li><li><strong>应用快捷方式集成</strong>：自动枚举已安装的 Linux GUI 应用程序，并在 Windows 开始菜单中创建快捷方式</li><li><strong>GPU 加速</strong>：通过虚拟 GPU（vGPU）支持 OpenGL 和 Vulkan 硬件加速</li></ul><h3 id="8-2-GPU-计算支持"><a href="#8-2-GPU-计算支持" class="headerlink" title="8.2 GPU 计算支持"></a>8.2 GPU 计算支持</h3><p>WSL 2 提供完整的 GPU 计算支持，为机器学习、人工智能和科学计算等工作负载提供硬件加速：</p><ul><li><strong>NVIDIA CUDA</strong>：直接支持 CUDA 工具链和 GPU 计算</li><li><strong>DirectML</strong>：Microsoft 的机器学习加速 API</li><li><strong>OpenGL &#x2F; OpenCL</strong>：通过 <code>d3d12</code> Gallium 驱动提供硬件加速的 OpenGL 和 OpenCL</li><li><strong>框架支持</strong>：PyTorch、TensorFlow、ONNX Runtime 等主流 ML 框架</li></ul><p>GPU 直通机制的核心在于 Windows 宿主上的 GPU 驱动通过虚拟化层将 GPU 资源暴露给 WSL 2 虚拟机。用户需要安装 Windows GPU 驱动（包含 WSL 的 CUDA 和 DirectML 支持），但<strong>不应</strong>在 WSL 内部安装 Linux GPU 驱动，以避免与直通机制冲突。</p><h3 id="8-3-systemd-集成"><a href="#8-3-systemd-集成" class="headerlink" title="8.3 systemd 集成"></a>8.3 systemd 集成</h3><p>WSL 2 从 0.67.6 版本开始正式支持 <code>systemd</code>——Linux 生态系统中广泛使用的初始化系统和服务管理器。该集成是提升 WSL 与标准 Linux 发行版兼容性的关键一步。</p><p><strong>启用方式</strong>：</p><p>在 <code>/etc/wsl.conf</code> 中配置：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[boot]</span></span><br><span class="line"><span class="attr">systemd</span>=<span class="literal">true</span></span><br></pre></td></tr></table></figure><p>通过 <code>wsl --install</code> 安装的 Ubuntu 发行版默认启用 systemd。</p><p><strong>架构变化</strong>：</p><p>启用 systemd 后，WSL 的 init 进程成为 systemd 的子进程，systemd 以 PID 1 运行，接管系统服务管理。这使得以下依赖 systemd 的技术可以正常运行：</p><ul><li><strong>Snap 包管理器</strong></li><li><strong>microk8s</strong>（轻量级 Kubernetes）</li><li><strong>Docker（systemd 模式）</strong></li><li><strong>标准 systemctl 服务管理</strong></li></ul><h3 id="8-4-企业级功能"><a href="#8-4-企业级功能" class="headerlink" title="8.4 企业级功能"></a>8.4 企业级功能</h3><p>WSL 在企业部署和安全方面的能力持续增强：</p><ul><li><strong>Microsoft Intune 设备合规性集成</strong>（2024 年 11 月 GA）：IT 管理员可强制执行 WSL 发行版和版本的选择策略</li><li><strong>Microsoft Entra ID 集成</strong>（预览版）：零信任体验，自动化 Entra 令牌传递和 Linux 进程认证</li><li><strong>Microsoft Defender for Endpoint</strong>：安全防护集成</li><li><strong>Tar-Based WSL 发行版架构</strong>（2024 年 11 月）：简化企业环境下 WSL 发行版的创建、分发和自动化部署</li></ul><h3 id="8-5-开源化"><a href="#8-5-开源化" class="headerlink" title="8.5 开源化"></a>8.5 开源化</h3><p>2025 年 5 月，Microsoft 将 WSL 的大部分代码库以开源软件形式发布，这是 Microsoft 拥抱开源战略的又一重要举措，也标志着 WSL 从封闭产品向开放平台的转型。</p><hr><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>从 1993 年的 POSIX 子系统到 2025 年的 WSL 2，Microsoft 在 Windows 平台上的 Unix&#x2F;Linux 兼容性技术经历了四个重要阶段：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/breaking-boundaries-and-convergence-three-decade-technical-evolution-history-of-linux-compatibility-layer-on-windows/timeline-evolution.svg" alt="Windows 平台 Unix/Linux 兼容性技术演进" title="">                </div>                <div class="image-caption">Windows 平台 Unix/Linux 兼容性技术演进</div>            </figure><p>每个阶段都反映了 Microsoft 对市场需求的回应和技术路线的调整：</p><table><thead><tr><th align="left">阶段</th><th align="left">核心动机</th><th align="left">技术路线</th></tr></thead><tbody><tr><td align="left">POSIX 子系统</td><td align="left">政府采购合规</td><td align="left">最小化标准实现</td></tr><tr><td align="left">SFU&#x2F;Interix</td><td align="left">Unix 互操作需求</td><td align="left">NT 原生子系统</td></tr><tr><td align="left">WSL 1</td><td align="left">开发者生态吸引</td><td align="left">系统调用翻译</td></tr><tr><td align="left">WSL 2</td><td align="left">完全 Linux 兼容</td><td align="left">轻量级虚拟化</td></tr></tbody></table><h3 id="9-2-持续演进方向"><a href="#9-2-持续演进方向" class="headerlink" title="9.2 持续演进方向"></a>9.2 持续演进方向</h3><p>WSL 仍在积极发展中，可预见的演进方向包括：</p><ol><li><strong>WSL Settings GUI</strong>：图形化的 WSL 设置管理界面，降低配置门槛</li><li><strong>网络模式优化</strong>：镜像网络模式的稳定化以及更多网络功能的支持</li><li><strong>性能持续优化</strong>：内存回收、磁盘回收等机制的进一步完善</li><li><strong>容器化深度集成</strong>：Windows Server 上 Linux 容器的 WSL 2 运行选项</li><li><strong>企业安全增强</strong>：零信任架构、设备合规性和身份管理的持续深化</li><li><strong>开源社区协作</strong>：利用开源社区的力量加速 WSL 的发展</li></ol><h3 id="9-3-结论"><a href="#9-3-结论" class="headerlink" title="9.3 结论"></a>9.3 结论</h3><p>Windows Subsystem for Linux 的发展历程是一部精彩的技术演化史。从 POSIX 子系统的合规性驱动，到 Interix 的功能导向，再到 WSL 的开发者体验导向，Microsoft 对 Unix&#x2F;Linux 兼容性的态度经历了从”不得不做”到”主动拥抱”的根本转变。</p><p>WSL 2 的轻量级虚拟机架构是一个优雅的工程方案——它承认了完美翻译层的不可行性，转而利用成熟的虚拟化技术来提供真正的 Linux 内核兼容性，同时通过深度优化保持了与 Windows 的无缝集成体验。这一架构选择不仅解决了 WSL 1 的兼容性和性能问题，更为 GPU 计算、容器化、GUI 应用等高级功能提供了坚实的技术基础。</p><p>随着 2025 年 WSL 代码库的开源化，这一技术的发展将进入新的阶段。可以预见，WSL 将继续模糊 Windows 与 Linux 之间的边界，为开发者提供真正统一的跨平台开发体验。</p><hr><h2 id="10-参考文献"><a href="#10-参考文献" class="headerlink" title="10. 参考文献"></a>10. 参考文献</h2><ol><li>Microsoft. “Windows Subsystem for Linux Documentation.” <em>Microsoft Learn</em>, 2024. <a href="https://learn.microsoft.com/en-us/windows/wsl/">https://learn.microsoft.com/en-us/windows/wsl/</a></li><li>Microsoft. “Comparing WSL Versions.” <em>Microsoft Learn</em>, 2024. <a href="https://learn.microsoft.com/en-us/windows/wsl/compare-versions">https://learn.microsoft.com/en-us/windows/wsl/compare-versions</a></li><li>Microsoft. “WSL 2 FAQ.” <em>Microsoft Learn</em>, 2024. <a href="https://learn.microsoft.com/en-us/windows/wsl/faq">https://learn.microsoft.com/en-us/windows/wsl/faq</a></li><li>Microsoft. “Pico Process Overview.” <em>Windows Subsystem for Linux Blog</em>, 2016.</li><li>Microsoft. “Windows Subsystem for Linux: Syscall Translation.” <em>MSDN Blog</em>, 2016.</li><li>Wikipedia. “Architecture of Windows NT.” <em>Wikipedia</em>, 2024. <a href="https://en.wikipedia.org/wiki/Architecture_of_Windows_NT">https://en.wikipedia.org/wiki/Architecture_of_Windows_NT</a></li><li>Wikipedia. “Windows Subsystem for Linux.” <em>Wikipedia</em>, 2025. <a href="https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux">https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux</a></li><li>Wikipedia. “Microsoft POSIX Subsystem.” <em>Wikipedia</em>, 2024. <a href="https://en.wikipedia.org/wiki/Microsoft_POSIX_subsystem">https://en.wikipedia.org/wiki/Microsoft_POSIX_subsystem</a></li><li>Wikipedia. “Windows Services for UNIX.” <em>Wikipedia</em>, 2024. <a href="https://en.wikipedia.org/wiki/Windows_Services_for_UNIX">https://en.wikipedia.org/wiki/Windows_Services_for_UNIX</a></li><li>Wikipedia. “Interix.” <em>Wikipedia</em>, 2024. <a href="https://en.wikipedia.org/wiki/Interix">https://en.wikipedia.org/wiki/Interix</a></li><li>Microsoft. “WSLg Architecture.” <em>GitHub - microsoft&#x2F;wslg</em>, 2024. <a href="https://github.com/microsoft/wslg">https://github.com/microsoft/wslg</a></li><li>Microsoft. “Systemd Support in WSL.” <em>Microsoft Learn</em>, 2024. <a href="https://learn.microsoft.com/en-us/windows/wsl/systemd">https://learn.microsoft.com/en-us/windows/wsl/systemd</a></li><li>NVIDIA. “CUDA on WSL User Guide.” <em>NVIDIA Developer</em>, 2024. <a href="https://docs.nvidia.com/cuda/wsl-user-guide/">https://docs.nvidia.com/cuda/wsl-user-guide/</a></li><li>Microsoft. “What’s new in WSL in 2024.” <em>Microsoft Build</em>, 2024.</li><li>Solomon, D.A. &amp; Russinovich, M.E. <em>Windows Internals</em>. Microsoft Press, 7th Edition.</li><li>Microsoft. “WSL Kernel Release Notes.” <em>GitHub - microsoft&#x2F;WSL2-Linux-Kernel</em>, 2024. <a href="https://github.com/microsoft/WSL2-Linux-Kernel">https://github.com/microsoft/WSL2-Linux-Kernel</a></li><li>Microsoft. “Rethinking the Library OS from the Top Down” <em>Microsoft Research</em>, 2011. <a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/asplos2011-drawbridge.pdf">https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/asplos2011-drawbridge.pdf</a></li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;摘要&lt;/strong&gt;：Windows Subsystem for Linux（WSL）是 Microsoft 在 Windows 操作系统上提供原生 Linux 兼容性的技术方案。本文系统性地回顾了从 Windows NT 时代的</summary>
      
    
    
    
    
    <category term="Linux" scheme="https://blog.jugg.xyz/tags/Linux/"/>
    
    <category term="Windows" scheme="https://blog.jugg.xyz/tags/Windows/"/>
    
    <category term="WSL" scheme="https://blog.jugg.xyz/tags/WSL/"/>
    
    <category term="musings" scheme="https://blog.jugg.xyz/tags/musings/"/>
    
    <category term="AI辅助" scheme="https://blog.jugg.xyz/tags/AI%E8%BE%85%E5%8A%A9/"/>
    
  </entry>
  
  <entry>
    <title>镜像迁移</title>
    <link href="https://blog.jugg.xyz/2024/04/07/ops/migrating-docker-images/"/>
    <id>https://blog.jugg.xyz/2024/04/07/ops/migrating-docker-images/</id>
    <published>2024-04-07T02:52:35.000Z</published>
    <updated>2026-05-02T11:24:14.333Z</updated>
    
    <content type="html"><![CDATA[<p>迁移镜像有三种场景 ：</p><ol><li><strong>在线迁移</strong>：通常是有一台机器能同时连通源镜像仓库和目标镜像仓库就算在线迁移</li><li><strong>离线迁移</strong>：只要源镜像仓库和目标镜像仓库无法在一台机器上同时联通就算离线迁移</li><li><strong>备份</strong>：没错，备份也是一种迁移</li></ol><h2 id="前置准备"><a href="#前置准备" class="headerlink" title="前置准备"></a>前置准备</h2><p>迁移之前首先需要有一份迁移的目标，这里我会以部署 k8s 所需要的容器为例。首先需要获取一份所有的镜像清单 <code>images.list</code>，这里通过 kubespray 的<a href="https://github.com/kubernetes-sigs/kubespray/blob/release-2.24/contrib/offline/generate_list.sh">离线脚本</a>自动生成。</p><p><strong>另外需要提前声明一点</strong></p><p><strong>以下的所有迁移，包括备份，都是以一次性迁移所有架构的镜像为目标。</strong></p><figure class="highlight shell"><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cat</span> images.list</span></span><br><span class="line">docker.io/mirantis/k8s-netchecker-server:v1.2.2</span><br><span class="line">docker.io/mirantis/k8s-netchecker-agent:v1.2.2</span><br><span class="line">quay.io/coreos/etcd:v3.5.10</span><br><span class="line">quay.io/cilium/cilium:v1.13.4</span><br><span class="line">quay.io/cilium/operator:v1.13.4</span><br><span class="line">quay.io/cilium/hubble-relay:v1.13.4</span><br><span class="line">quay.io/cilium/certgen:v0.1.8</span><br><span class="line">quay.io/cilium/hubble-ui:v0.11.0</span><br><span class="line">quay.io/cilium/hubble-ui-backend:v0.11.0</span><br><span class="line">docker.io/envoyproxy/envoy:v1.22.5</span><br><span class="line">ghcr.io/k8snetworkplumbingwg/multus-cni:v3.8</span><br><span class="line">docker.io/flannel/flannel:v0.22.0</span><br><span class="line">docker.io/flannel/flannel-cni-plugin:v1.1.2</span><br><span class="line">quay.io/calico/node:v3.26.4</span><br><span class="line">quay.io/calico/cni:v3.26.4</span><br><span class="line">quay.io/calico/pod2daemon-flexvol:v3.26.4</span><br><span class="line">quay.io/calico/kube-controllers:v3.26.4</span><br><span class="line">quay.io/calico/typha:v3.26.4</span><br><span class="line">quay.io/calico/apiserver:v3.26.4</span><br><span class="line">docker.io/weaveworks/weave-kube:2.8.1</span><br><span class="line">docker.io/weaveworks/weave-npc:2.8.1</span><br><span class="line">docker.io/kubeovn/kube-ovn:v1.11.5</span><br><span class="line">docker.io/cloudnativelabs/kube-router:v2.0.0</span><br><span class="line">registry.k8s.io/pause:3.9</span><br><span class="line">ghcr.io/kube-vip/kube-vip:v0.5.12</span><br><span class="line">docker.io/library/nginx:1.25.2-alpine</span><br><span class="line">docker.io/library/haproxy:2.8.2-alpine</span><br><span class="line">registry.k8s.io/coredns/coredns:v1.10.1</span><br><span class="line">registry.k8s.io/dns/k8s-dns-node-cache:1.22.28</span><br><span class="line">registry.k8s.io/cpa/cluster-proportional-autoscaler:v1.8.8</span><br><span class="line">docker.io/library/registry:2.8.1</span><br><span class="line">registry.k8s.io/metrics-server/metrics-server:v0.6.4</span><br><span class="line">registry.k8s.io/sig-storage/local-volume-provisioner:v2.5.0</span><br><span class="line">quay.io/external_storage/cephfs-provisioner:v2.1.0-k8s1.11</span><br><span class="line">quay.io/external_storage/rbd-provisioner:v2.1.1-k8s1.11</span><br><span class="line">docker.io/rancher/local-path-provisioner:v0.0.24</span><br><span class="line">registry.k8s.io/ingress-nginx/controller:v1.9.4</span><br><span class="line">docker.io/amazon/aws-alb-ingress-controller:v1.1.9</span><br><span class="line">quay.io/jetstack/cert-manager-controller:v1.13.2</span><br><span class="line">quay.io/jetstack/cert-manager-cainjector:v1.13.2</span><br><span class="line">quay.io/jetstack/cert-manager-webhook:v1.13.2</span><br><span class="line">registry.k8s.io/sig-storage/csi-attacher:v3.3.0</span><br><span class="line">registry.k8s.io/sig-storage/csi-provisioner:v3.0.0</span><br><span class="line">registry.k8s.io/sig-storage/csi-snapshotter:v5.0.0</span><br><span class="line">registry.k8s.io/sig-storage/snapshot-controller:v4.2.1</span><br><span class="line">registry.k8s.io/sig-storage/csi-resizer:v1.3.0</span><br><span class="line">registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.4.0</span><br><span class="line">docker.io/k8scloudprovider/cinder-csi-plugin:v1.22.0</span><br><span class="line">docker.io/amazon/aws-ebs-csi-driver:v0.5.0</span><br><span class="line">docker.io/kubernetesui/dashboard:v2.7.0</span><br><span class="line">docker.io/kubernetesui/metrics-scraper:v1.0.8</span><br><span class="line">quay.io/metallb/speaker:v0.13.9</span><br><span class="line">quay.io/metallb/controller:v0.13.9</span><br><span class="line">registry.k8s.io/kube-apiserver:v1.28.6</span><br><span class="line">registry.k8s.io/kube-controller-manager:v1.28.6</span><br><span class="line">registry.k8s.io/kube-scheduler:v1.28.6</span><br><span class="line">registry.k8s.io/kube-proxy:v1.28.6</span><br></pre></td></tr></table></figure><h2 id="在线迁移"><a href="#在线迁移" class="headerlink" title="在线迁移"></a>在线迁移</h2><p>在线迁移是最简单的场景，首先我们确认源镜像，这里已经准备好了，就是上一步中生成的 <code>images.list</code>，其次我们确定目标镜像仓库，迁移目标都是按具体需求来定，这里我们以 <code>uhub.service.ucloud.cn/k8s-use/</code> 为例。</p><p>在线迁移最简单的工具就是 <code>skopeo</code>，安装可以参考<a href="https://github.com/containers/skopeo/blob/main/install.md">官方文档</a>，这里推荐使用包管理器安装，如果包管理器默认版本太老或者包管理器版本有 bug，就需要<a href="https://github.com/containers/skopeo/blob/main/install.md#building-from-source">编译安装</a>。</p><p>安装完 <code>skopeo</code> 之后，一个 for 循环即可解决。</p><figure class="highlight shell"><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><span class="line">for src in $(cat images.list); do</span><br><span class="line">    image=$&#123;src#*/&#125;</span><br><span class="line">    dest=&quot;uhub.service.ucloud.cn/k8s-use/$&#123;image&#125;&quot;</span><br><span class="line">    skopeo --insecure-policy copy --all  docker://$&#123;src&#125; docker://$&#123;dest&#125;</span><br><span class="line">done</span><br></pre></td></tr></table></figure><p>这里用到的两个参数：</p><p><code>--insecure-policy</code> 是 <code>skopeo</code> 的参数，因为我本地没有配置 <code>policy</code>，必须要手动指定一个。</p><p><code>--all</code> 是 <code>skopeo copy</code> 的参数，作用是迁移所有架构的镜像。如果需要迁移非当前系统架构，则需要使用参数 <code>--override-os</code>,<code>--override-arch</code>来指定。例如: <code>--override-os linux --override-arch arm64</code>。注意，只要不是使用 <code>--all</code> 参数，都只支持一种架构，要么是当前默认使用当前系统的架构，要么使用通过 <code>--override-*</code> 所指定的架构。只同步部分架构的需求，skoepo官方<a href="https://github.com/containers/skopeo/issues/1694">尚未实现，也不在规划中</a>，如果有这种需求的话目前还只能通过自己编写 manifest 实现。</p><p>迁移到其他自建的 harbor 也都是一样的操作，但是如果你的 harbor 证书不可信的话还需要在 <code>copy</code> 命令后面加上 <code>--dest-tls-verify=false</code> 来关闭 tls 验证，如果源仓库的证书也不可信的话还需要使用<code>--src-tls-verify=false</code>。</p><p>如果是迁移到其他云服务商的话，还需要注意一个三级路径的问题，多数云服务商的镜像仓库都需要企业版才提供多级路径的功能。所以要么用企业版，要么就需要对镜像重命名。以上面的 for 循环为例，可以把所有的 <code>/</code> 都替换为 <code>-</code>：<code>image=$(echo $&#123;src&#125;| sed &#39;s#^[^/]*/##;s/\//-/g&#39;)</code>。</p><p>另外，在线迁移同样可以使用 <code>skopeo sync</code> 来解决，为了能多水点字，sync 就放到下个场景介绍了。</p><h2 id="离线迁移"><a href="#离线迁移" class="headerlink" title="离线迁移"></a>离线迁移</h2><p>在私有化场景中通常都是需要离线同步镜像的，第一步，需要将镜像转成文件，然后再传入离线环境中再导入，这里同样可以用 <code>skopeo</code>。</p><p><code>skopeo</code> 的安装方法同在线迁移场景中一样，这里不多缀叙。离线迁移用到的是 <code>skopeo sync</code> 命令。离线迁移的步骤会稍微复杂一点，所以我们先写一个脚本：</p><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">save_images</span></span>() &#123;</span><br><span class="line">  <span class="built_in">local</span> image_file=<span class="variable">$1</span></span><br><span class="line">  <span class="built_in">local</span> src</span><br><span class="line">  <span class="keyword">for</span> src <span class="keyword">in</span> $(<span class="built_in">cat</span> <span class="variable">$&#123;image_file&#125;</span>); <span class="keyword">do</span></span><br><span class="line">      image=<span class="variable">$&#123;src#*/&#125;</span></span><br><span class="line">      [[ <span class="string">&quot;<span class="variable">$image</span>&quot;</span> =~ <span class="string">&quot;/&quot;</span> ]] &amp;&amp; image_path=<span class="variable">$&#123;image%%/*&#125;</span> || image_path=<span class="string">&quot;root&quot;</span></span><br><span class="line">      skopeo --insecure-policy <span class="built_in">sync</span> --all --src docker --dest <span class="built_in">dir</span> <span class="variable">$&#123;src&#125;</span> images/<span class="variable">$&#123;image_path&#125;</span></span><br><span class="line">  <span class="keyword">done</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">load_images</span></span>() &#123;</span><br><span class="line">  <span class="built_in">local</span> image_file=<span class="variable">$1</span></span><br><span class="line">  <span class="built_in">local</span> src</span><br><span class="line">  dest_repo=<span class="variable">$2</span></span><br><span class="line">  <span class="keyword">for</span> src <span class="keyword">in</span> $(<span class="built_in">cat</span> <span class="variable">$&#123;image_file&#125;</span>); <span class="keyword">do</span></span><br><span class="line">    image=<span class="variable">$&#123;src#*/&#125;</span></span><br><span class="line">    [[ <span class="string">&quot;<span class="variable">$image</span>&quot;</span> =~ <span class="string">&quot;/&quot;</span> ]] &amp;&amp; image_path=<span class="variable">$&#123;image%%/*&#125;</span> || image_path=<span class="string">&quot;root&quot;</span></span><br><span class="line">    [[ <span class="string">&quot;<span class="variable">$image_path</span>&quot;</span> == <span class="string">&quot;root&quot;</span> ]] &amp;&amp; <span class="built_in">unset</span> dest_name || dest_name=<span class="variable">$&#123;image_path&#125;</span></span><br><span class="line">    skopeo --insecure-policy <span class="built_in">sync</span> --all --src <span class="built_in">dir</span> --dest docker images/<span class="variable">$&#123;image_path&#125;</span> <span class="variable">$&#123;dest_repo&#125;</span>/<span class="variable">$&#123;dest_name&#125;</span></span><br><span class="line">  <span class="keyword">done</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">case</span> <span class="string">&quot;<span class="variable">$1</span>&quot;</span> <span class="keyword">in</span></span><br><span class="line">  <span class="string">&quot;save&quot;</span>)</span><br><span class="line">    <span class="built_in">shift</span></span><br><span class="line">    save_images <span class="string">&quot;<span class="variable">$@</span>&quot;</span></span><br><span class="line">    ;;</span><br><span class="line">  <span class="string">&quot;load&quot;</span>)</span><br><span class="line">    <span class="built_in">shift</span></span><br><span class="line">    load_images <span class="string">&quot;<span class="variable">$@</span>&quot;</span></span><br><span class="line">    ;;</span><br><span class="line">  *)</span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;Invalid command: <span class="variable">$1</span>&quot;</span> &gt;&amp;2</span><br><span class="line">    <span class="built_in">exit</span> 1</span><br><span class="line">    ;;</span><br><span class="line"><span class="keyword">esac</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这个脚本也很简单，只有两个函数，一个 <code>save_images()</code>,一个 <code>load_images()</code>,首先我们需要做的就是将镜像下载到本地，我们将脚本保存为 <code>images.sh</code> 并且赋予可执行权限，然后执行以下命令：</p><figure class="highlight shell"><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">./images.sh save images.list</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">等待执行完成之后即同步成功了，下载下来的目录如下</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">tree -d</span></span><br><span class="line">.</span><br><span class="line">└── images</span><br><span class="line">    ├── amazon</span><br><span class="line">    │   ├── aws-alb-ingress-controller:v1.1.9</span><br><span class="line">    │   └── aws-ebs-csi-driver:v0.5.0</span><br><span class="line">    ├── calico</span><br><span class="line">    │   ├── apiserver:v3.26.4</span><br><span class="line">    │   ├── cni:v3.26.4</span><br><span class="line">    │   ├── kube-controllers:v3.26.4</span><br><span class="line">    │   ├── node:v3.26.4</span><br><span class="line">    │   ├── pod2daemon-flexvol:v3.26.4</span><br><span class="line">    │   └── typha:v3.26.4</span><br><span class="line">    ├── cilium</span><br><span class="line">    │   ├── certgen:v0.1.8</span><br><span class="line">    │   ├── cilium:v1.13.4</span><br><span class="line">    │   ├── hubble-relay:v1.13.4</span><br><span class="line">    │   ├── hubble-ui-backend:v0.11.0</span><br><span class="line">    │   ├── hubble-ui:v0.11.0</span><br><span class="line">    │   └── operator:v1.13.4</span><br><span class="line">    ├── cloudnativelabs</span><br><span class="line">    │   └── kube-router:v2.0.0</span><br><span class="line">    ├── coredns</span><br><span class="line">    │   └── coredns:v1.10.1</span><br><span class="line">    ├── coreos</span><br><span class="line">    │   └── etcd:v3.5.10</span><br><span class="line">    ├── cpa</span><br><span class="line">    │   └── cluster-proportional-autoscaler:v1.8.8</span><br><span class="line">    ├── dns</span><br><span class="line">    │   └── k8s-dns-node-cache:1.22.28</span><br><span class="line">    ├── envoyproxy</span><br><span class="line">    │   └── envoy:v1.22.5</span><br><span class="line">    ├── external_storage</span><br><span class="line">    │   ├── cephfs-provisioner:v2.1.0-k8s1.11</span><br><span class="line">    │   └── rbd-provisioner:v2.1.1-k8s1.11</span><br><span class="line">    ├── flannel</span><br><span class="line">    │   ├── flannel-cni-plugin:v1.1.2</span><br><span class="line">    │   └── flannel:v0.22.0</span><br><span class="line">    ├── ingress-nginx</span><br><span class="line">    │   └── controller:v1.9.4</span><br><span class="line">    ├── jetstack</span><br><span class="line">    │   ├── cert-manager-cainjector:v1.13.2</span><br><span class="line">    │   ├── cert-manager-controller:v1.13.2</span><br><span class="line">    │   └── cert-manager-webhook:v1.13.2</span><br><span class="line">    ├── k8scloudprovider</span><br><span class="line">    │   └── cinder-csi-plugin:v1.22.0</span><br><span class="line">    ├── k8snetworkplumbingwg</span><br><span class="line">    │   └── multus-cni:v3.8</span><br><span class="line">    ├── kubeovn</span><br><span class="line">    │   └── kube-ovn:v1.11.5</span><br><span class="line">    ├── kubernetesui</span><br><span class="line">    │   ├── dashboard:v2.7.0</span><br><span class="line">    │   └── metrics-scraper:v1.0.8</span><br><span class="line">    ├── kube-vip</span><br><span class="line">    │   └── kube-vip:v0.5.12</span><br><span class="line">    ├── library</span><br><span class="line">    │   ├── haproxy:2.8.2-alpine</span><br><span class="line">    │   ├── nginx:1.25.2-alpine</span><br><span class="line">    │   └── registry:2.8.1</span><br><span class="line">    ├── metallb</span><br><span class="line">    │   ├── controller:v0.13.9</span><br><span class="line">    │   └── speaker:v0.13.9</span><br><span class="line">    ├── metrics-server</span><br><span class="line">    │   └── metrics-server:v0.6.4</span><br><span class="line">    ├── mirantis</span><br><span class="line">    │   ├── k8s-netchecker-agent:v1.2.2</span><br><span class="line">    │   └── k8s-netchecker-server:v1.2.2</span><br><span class="line">    ├── rancher</span><br><span class="line">    │   └── local-path-provisioner:v0.0.24</span><br><span class="line">    ├── root</span><br><span class="line">    │   ├── kube-apiserver:v1.28.6</span><br><span class="line">    │   ├── kube-controller-manager:v1.28.6</span><br><span class="line">    │   ├── kube-proxy:v1.28.6</span><br><span class="line">    │   ├── kube-scheduler:v1.28.6</span><br><span class="line">    │   └── pause:3.9</span><br><span class="line">    ├── sig-storage</span><br><span class="line">    │   ├── csi-attacher:v3.3.0</span><br><span class="line">    │   ├── csi-node-driver-registrar:v2.4.0</span><br><span class="line">    │   ├── csi-provisioner:v3.0.0</span><br><span class="line">    │   ├── csi-resizer:v1.3.0</span><br><span class="line">    │   ├── csi-snapshotter:v5.0.0</span><br><span class="line">    │   ├── local-volume-provisioner:v2.5.0</span><br><span class="line">    │   └── snapshot-controller:v4.2.1</span><br><span class="line">    └── weaveworks</span><br><span class="line">        ├── weave-kube:2.8.1</span><br><span class="line">        └── weave-npc:2.8.1</span><br><span class="line"></span><br><span class="line">85 directories</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>下载之后就可以进行推送了，执行以下命令：</p><figure class="highlight shell"><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><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">./images.sh load images.list uhub.service.ucloud.cn/k8s-use</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">注意，这里的 uhub.service.ucloud.cn/k8s-use 仅是我的示例</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">等待执行完毕之后，即代表推送成功</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>打开推送的目标仓库进行验证。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/uhub.png" alt="uhub" title="">                </div>                <div class="image-caption">uhub</div>            </figure><h2 id="备份"><a href="#备份" class="headerlink" title="备份"></a>备份</h2><p>备份其实是一种很小众的场景，某种意义上来说上面的离线迁移中，将目标镜像全部存到本地就是一种备份了，所以这种常规备份也没有单独介绍的必要了，这里讲一下 <code>harbor</code> 的迁移时将目标全部备份到本地的操作。</p><p>首先放脚本：</p><figure class="highlight bash"><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><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"></span><br><span class="line">username=<span class="string">&quot;yourname&quot;</span></span><br><span class="line">password=<span class="string">&quot;yourpassword&quot;</span></span><br><span class="line">harbor_url=<span class="string">&quot;harbor.your.url&quot;</span></span><br><span class="line"></span><br><span class="line">images_dir=<span class="string">&quot;images&quot;</span></span><br><span class="line">images_list=<span class="string">&quot;images.list&quot;</span></span><br><span class="line"></span><br><span class="line">project_images_url=<span class="string">&quot;https://<span class="variable">$&#123;harbor_url&#125;</span>/api/v2.0/repositories?page=1&amp;page_size=100&quot;</span></span><br><span class="line">images=$(curl -k -s -X GET -H <span class="string">&quot;Authorization: Basic <span class="subst">$(echo -n <span class="string">&quot;<span class="variable">$username</span>:<span class="variable">$password</span>&quot;</span> | base64)</span>&quot;</span> <span class="string">&quot;<span class="variable">$project_images_url</span>&quot;</span> | jq -r <span class="string">&#x27;.[] | .name&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="built_in">mkdir</span> -p <span class="string">&quot;<span class="variable">$images_dir</span>&quot;</span></span><br><span class="line">&gt;<span class="string">&quot;<span class="variable">$images_dir</span>/<span class="variable">$images_list</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> <span class="string">&quot;<span class="variable">$images_dir</span>&quot;</span></span><br><span class="line"><span class="keyword">for</span> image <span class="keyword">in</span> <span class="variable">$images</span>; <span class="keyword">do</span></span><br><span class="line">    project_name=$(<span class="built_in">echo</span> <span class="variable">$&#123;image&#125;</span> | sed <span class="string">&#x27;s/\/.*//&#x27;</span>)</span><br><span class="line">    repository_name=$(<span class="built_in">echo</span> <span class="variable">$&#123;image&#125;</span> | sed <span class="string">&#x27;s/^[^\/]*\///&#x27;</span>)</span><br><span class="line">    repository_name_recode=$(<span class="built_in">echo</span> <span class="variable">$&#123;repository_name&#125;</span> | sed <span class="string">&#x27;s/\//%252F/g&#x27;</span>)</span><br><span class="line">    artifacts_url=<span class="string">&quot;https://<span class="variable">$&#123;harbor_url&#125;</span>/api/v2.0/projects/<span class="variable">$&#123;project_name&#125;</span>/repositories/<span class="variable">$&#123;repository_name_recode&#125;</span>/artifacts?page=1&amp;page_size=100&amp;with_tag=true&quot;</span></span><br><span class="line">    tags=$(curl -k -s -X GET -H <span class="string">&quot;Authorization: Basic <span class="subst">$(echo -n <span class="string">&quot;<span class="variable">$username</span>:<span class="variable">$password</span>&quot;</span> | base64)</span>&quot;</span> <span class="string">&quot;<span class="variable">$&#123;artifacts_url&#125;</span>&quot;</span> | jq -r <span class="string">&#x27;.[].tags[].name&#x27;</span>)</span><br><span class="line">    <span class="keyword">for</span> tag <span class="keyword">in</span> <span class="variable">$tags</span>; <span class="keyword">do</span></span><br><span class="line">        image_name=<span class="variable">$&#123;harbor_url&#125;</span>/<span class="variable">$&#123;project_name&#125;</span>/<span class="variable">$&#123;repository_name&#125;</span>:<span class="variable">$&#123;tag&#125;</span></span><br><span class="line">        docker pull <span class="variable">$&#123;image_name&#125;</span></span><br><span class="line">        image_arch=$(docker inspect <span class="variable">$&#123;image_name&#125;</span> | jq -r <span class="string">&#x27;.[0].Architecture&#x27;</span>)</span><br><span class="line">        docker save <span class="variable">$&#123;image_name&#125;</span> &gt;$(<span class="built_in">echo</span> <span class="variable">$&#123;image_name&#125;</span> | awk <span class="string">&#x27;&#123;gsub(/[.\/:]/,&quot;_&quot;)&#125;1&#x27;</span>).<span class="variable">$&#123;image_arch&#125;</span>.tar.xz</span><br><span class="line">        <span class="built_in">echo</span> <span class="variable">$&#123;image_name&#125;</span> &gt;&gt;images.list</span><br><span class="line">    <span class="keyword">done</span></span><br><span class="line"><span class="keyword">done</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> -</span><br><span class="line">tar -c --use-compress-program=<span class="string">&quot;xz -9 -T0&quot;</span> -f images.tar.xz images/</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>简单讲一下备份的思路，首先通过 api 获取所有的仓库，然后遍历所有仓库的 tag，arch，按照最小单位 arch 进行下载镜像打包，最后压缩一下。</p><p>这里之所以没有用上一个场景离线迁移中使用的 <code>skopeo sync</code> ，其实是因为最开始的时候项目被推着上线，就直接弄了一个 arm64 harbor ，一个 x86 harbor。等演示完毕之后有喘息时间了我突然想要把两个 harbor 合成一个，于是就开始这个场景的操作，先将 x86 harbor 的镜像统一备份下来，然后再构建 manifest 往 arm64 的 harbor 上推。这个场景小众到几乎不可能重复，但是这个脚本甚至是我整篇博客最先完成的部分，我几乎是为了这碟醋包了盘饺子🌚，所以还是必须水出来了。</p><p>正好也为下一篇水一下如何手动构建 manifest 做一个铺垫，完美！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;迁移镜像有三种场景 ：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;在线迁移&lt;/strong&gt;：通常是有一台机器能同时连通源镜像仓库和目标镜像仓库就算在线迁移&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;离线迁移&lt;/strong&gt;：只要源镜像仓库和目标镜像仓库无法在一台机器上同时联通就</summary>
      
    
    
    
    <category term="运维" scheme="https://blog.jugg.xyz/categories/%E8%BF%90%E7%BB%B4/"/>
    
    
    <category term="docker" scheme="https://blog.jugg.xyz/tags/docker/"/>
    
    <category term="harbor" scheme="https://blog.jugg.xyz/tags/harbor/"/>
    
  </entry>
  
  <entry>
    <title>跨平台构建docker镜像</title>
    <link href="https://blog.jugg.xyz/2024/03/12/ops/cross-platform-image-building-with-docker-buildx/"/>
    <id>https://blog.jugg.xyz/2024/03/12/ops/cross-platform-image-building-with-docker-buildx/</id>
    <published>2024-03-12T06:47:39.000Z</published>
    <updated>2026-05-02T11:24:14.333Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>业务需要同时运行在 x86_64 和 arm64 两种架构的机器上，贫穷让公司选择了跨平台构建。</p></blockquote><h2 id="安装-buildx"><a href="#安装-buildx" class="headerlink" title="安装 buildx"></a>安装 buildx</h2><p>buildx 是作为 docker cli 的一个插件存在的，所以如果已经在系统中配置了 docker 仓库的话，直接用系统的包管理器安装 <code>docker-buildx-plugin</code> 即可，这也是最推荐的方案。</p><p>如果非要手动安装，可以自行从 <a href="https://github.com/docker/buildx/releases/latest"> docker buildx github releases </a> 进行下载，然后拷贝到 <code>/usr/local/lib/docker/cli-plugins</code> 目录下即可。仅当前用户使用的话也可以放到 <code>$HOME/.docker/cli-plugins</code> 目录。</p><h2 id="初始化构建器"><a href="#初始化构建器" class="headerlink" title="初始化构建器"></a>初始化构建器</h2><p>docker buildx 跨平台打包镜像有两种方案，第一种使用不同架构的机器来做 worker ，但是如果有这种资源我还是比较倾向用 <a href="https://github.com/GoogleContainerTools/kaniko">kaniko</a>，所以这里暂不赘叙，有需求可以看一下<a href="https://docs.docker.com/build/building/multi-platform/#multiple-native-nodes">官方文档</a>。这里主要介绍一下第二种，通过 binfmt_misc 和 QEMU 来实现跨平台构建。</p><p>buildx 的跨平台构建，实际上是通过 QEMU 作为构建器后端，然后再利用 binfmt_misc 模块注册 QEMU 作为其他CPU架构可执行文件的处理程序来实现的。我们首先需要先启用 binfmt_misc，这里可以通过 <a href="https://github.com/tonistiigi/binfmt">tonistiigi&#x2F;binfmt</a> 这个脚本镜像直接实现：</p><figure class="highlight shell"><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><span class="line"><span class="meta prompt_"># </span><span class="language-bash">这里最后的 --install all 是安装所有支持的架构，如果只有特定需求也可以手动指定</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">例如 --install arm64,riscv64,arm</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker run --privileged --<span class="built_in">rm</span> tonistiigi/binfmt --install all</span></span><br></pre></td></tr></table></figure><p>执行完毕之后，可以通过这两种方法验证是否已经成功开启 binfmt_misc ：</p><figure class="highlight shell"><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><span class="line"><span class="meta prompt_"># </span><span class="language-bash">该命令可以查看当前所有可用构建器，如未启用则 PLATFORMS 应该仅支持 amd64 和 386 相关的架构</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker buildx <span class="built_in">ls</span></span></span><br><span class="line">NAME/NODE     DRIVER/ENDPOINT   STATUS    BUILDKIT   PLATFORMS</span><br><span class="line">default*      docker</span><br><span class="line"> \_ default    \_ default       running   v0.12.5    linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">该命令可以查看 binfmt_misc 的开启状态</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cat</span> /proc/sys/fs/binfmt_misc/status</span></span><br></pre></td></tr></table></figure><p>确认无误后，即可开始创建构建器：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker buildx create --name mybuilder --bootstrap --use</span></span><br></pre></td></tr></table></figure><p>创建完之后就有了一个可用的，名称叫 mybuilder 构建器，这里再详细介绍一些参数：</p><figure class="highlight shell"><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><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker buildx create \</span></span><br><span class="line"><span class="language-bash">  --name mybuilder \</span></span><br><span class="line"><span class="language-bash">  --driver=docker-container \</span></span><br><span class="line"><span class="language-bash">  --driver-opt <span class="string">&quot;network=host&quot;</span> \</span></span><br><span class="line"><span class="language-bash">  --config /etc/buildkitd.toml \</span></span><br><span class="line"><span class="language-bash">  --bootstrap \</span></span><br><span class="line"><span class="language-bash">  --use</span></span><br></pre></td></tr></table></figure><p>第一个参数 –name 就不用讲了，我们从第二个开始。</p><blockquote><p>–driver&#x3D;docker-container</p></blockquote><p>其实在安装 docker buildx 之后，就可以通过 docker buildx ls 看到一个 default 构建器，但是这个构建器并不可用到跨平台构建上，即使启用了 binfmt_misc 。原因就是因为这个 driver。docker driver 的构建器只会有一个 default ，无法单独指定创建，不使用 buildx 的时候的 docker 操作，其实都是在使用这个构建器。</p><p>docker driver 考虑的更多的是简单和易用两个方面，而我们这里指定的 docker-container driver 比 dokcer driver 要灵活很多，可以指定 BuildKit 版本、通过 QEMU 进行跨平台构建以及更高级的缓存导出功能（ps.这里的更高级指的是相对于 docker drvier 只能将缓存导出到镜像中而言）。同时也是 <code>docker buildx create</code> 默认 driver，所以也基本不用指定。</p><p>除了 docker 和 docker-container 之外还有 kubernetes 和 remote 两个 driver，主要差异在于使用场景上，这里不多缀述，有兴趣可以看一下<a href="https://docs.docker.com/build/drivers/">官方文档</a>。</p><blockquote><p>–driver-opt “network&#x3D;host”</p></blockquote><p>构建器实际上是打开一个 buildkit 容器进行构建，如果对这个容器有任何的配置需求，基本都是写在这个参数中，包括资源限制、启动的镜像、重启策略、环境变量以及我们这里写的网络的配置，具体可选配置可参考<a href="https://docs.docker.com/build/drivers/docker-container/#synopsis">该表格</a>。</p><p>这里单独使用了一个 network&#x3D;host 是因为遇到过构建时网络情况和预期不一致的情况。譬如拥有两个dns，一个用于内网一个用于外网，如果不使用 host 网络，则构建时就会使用 docker 内部 dns。作为一个构建镜像的基础服务，为了规避可能存在的干扰，还是建议使用 host 网络。</p><blockquote><p>–config &#x2F;etc&#x2F;buildkitd.toml</p></blockquote><p>与所有服务一样的，buildkit 也是有配置文件的，具体的配置内容可以参考<a href="https://docs.docker.com/build/buildkit/toml-configuration/">官方的配置文件</a>。当然了，这里的大多数配置都是无须更改的，这里单独将 config 拎出来，是因为有一种比较常见的情况是，构建的 base image 来自私有仓库，使用 http 或者自签 SSL 证书。</p><p>因为构建时并不在物理机而是在 buildkit 镜像内，所以它也没办法读 <code>/etc/docker/daemon.json</code> 的配置，必须要单独配置，例如：</p><figure class="highlight toml"><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><span class="line"><span class="section">[registry.&quot;192.168.189.102:5000&quot;]</span></span><br><span class="line"><span class="attr">http</span> = <span class="literal">true</span></span><br><span class="line"><span class="attr">insecure</span> = <span class="literal">true</span></span><br></pre></td></tr></table></figure><blockquote><p>–bootstrap –use</p></blockquote><p>bootstrap 就是创建的同时启动这个构建器使用的 buildkit 容器，use 就是创建的同时切换到这个构建器。</p><h2 id="开始构建"><a href="#开始构建" class="headerlink" title="开始构建"></a>开始构建</h2><p>前置准备虽然漫长，但是实践总是很简单的，首先准备一个Dockerfile：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> alpine:<span class="number">3.19</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apk add curl</span></span><br></pre></td></tr></table></figure><p>然后执行构建命令：</p><figure class="highlight shell"><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><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t ccr.ccs.tencentyun.com/openimage/alpine:3.19 --push .</span></span><br><span class="line"></span><br><span class="line">...</span><br><span class="line">(以下省略一堆输出)</span><br><span class="line"> =&gt; =&gt; pushing manifest for ccr.ccs.tencentyun.com/openimage/alpine:3.19@sha256:d2ca9689374079757822ac8a9bd1e298a96fd10fc7648c4a5043499f61a1e432                                                            2.1s</span><br><span class="line"> =&gt; [auth] openimage/alpine:pull,push token for ccr.ccs.tencentyun.com</span><br></pre></td></tr></table></figure><p>命令执行成功，构建完成，当你使用 <code>--push</code> 参数的时候，buildx 甚至自动创建了 manifest ，保证了多个架构镜像共用一个 tag，可以通过 <code>docker buildx imagetools inspect</code> 进行验证。</p><figure class="highlight shell"><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker buildx imagetools inspect ccr.ccs.tencentyun.com/openimage/alpine:3.19</span></span><br><span class="line"></span><br><span class="line">Name:      ccr.ccs.tencentyun.com/openimage/alpine:3.19</span><br><span class="line">MediaType: application/vnd.oci.image.index.v1+json</span><br><span class="line">Digest:    sha256:d2ca9689374079757822ac8a9bd1e298a96fd10fc7648c4a5043499f61a1e432</span><br><span class="line"></span><br><span class="line">Manifests:</span><br><span class="line">  Name:        ccr.ccs.tencentyun.com/openimage/alpine:3.19@sha256:dc6b015793c2ac537f61ec80348a7f34739cfa6580813f24e813d653fc37faf3</span><br><span class="line">  MediaType:   application/vnd.oci.image.manifest.v1+json</span><br><span class="line">  Platform:    linux/amd64</span><br><span class="line"></span><br><span class="line">  Name:        ccr.ccs.tencentyun.com/openimage/alpine:3.19@sha256:a3df735a618653a5bb73c1f09fe73feb30f352c19760326155bebf29172a7de5</span><br><span class="line">  MediaType:   application/vnd.oci.image.manifest.v1+json</span><br><span class="line">  Platform:    linux/arm64</span><br><span class="line"></span><br><span class="line">  Name:        ccr.ccs.tencentyun.com/openimage/alpine:3.19@sha256:1cf066565331d3feb598c1d7fb5a181a151bd6a965ffd891f9ab459e0a708afb</span><br><span class="line">  MediaType:   application/vnd.oci.image.manifest.v1+json</span><br><span class="line">  Platform:    linux/arm/v7</span><br><span class="line"></span><br><span class="line">  Name:        ccr.ccs.tencentyun.com/openimage/alpine:3.19@sha256:d43c80d6216bc643f283163c89b6f3d5813d02e8fa76f5a86115bcf8b16d10a2</span><br><span class="line">  MediaType:   application/vnd.oci.image.manifest.v1+json</span><br><span class="line">  Platform:    unknown/unknown</span><br><span class="line">  Annotations:</span><br><span class="line">    vnd.docker.reference.digest: sha256:dc6b015793c2ac537f61ec80348a7f34739cfa6580813f24e813d653fc37faf3</span><br><span class="line">    vnd.docker.reference.type:   attestation-manifest</span><br><span class="line"></span><br><span class="line">  Name:        ccr.ccs.tencentyun.com/openimage/alpine:3.19@sha256:c2e29cae1de1f6d4f285ed5055c871c745862054c8693b2e4d51fa9c3cb49954</span><br><span class="line">  MediaType:   application/vnd.oci.image.manifest.v1+json</span><br><span class="line">  Platform:    unknown/unknown</span><br><span class="line">  Annotations:</span><br><span class="line">    vnd.docker.reference.digest: sha256:a3df735a618653a5bb73c1f09fe73feb30f352c19760326155bebf29172a7de5</span><br><span class="line">    vnd.docker.reference.type:   attestation-manifest</span><br><span class="line"></span><br><span class="line">  Name:        ccr.ccs.tencentyun.com/openimage/alpine:3.19@sha256:b593bc641ba6ea8d046ff4d6e78f73ac4be1422c1ad3db0fc23dfc07950d049b</span><br><span class="line">  MediaType:   application/vnd.oci.image.manifest.v1+json</span><br><span class="line">  Platform:    unknown/unknown</span><br><span class="line">  Annotations:</span><br><span class="line">    vnd.docker.reference.digest: sha256:1cf066565331d3feb598c1d7fb5a181a151bd6a965ffd891f9ab459e0a708afb</span><br><span class="line">    vnd.docker.reference.type:   attestation-manifest</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;业务需要同时运行在 x86_64 和 arm64 两种架构的机器上，贫穷让公司选择了跨平台构建。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;安装-buildx&quot;&gt;&lt;a href=&quot;#安装-buildx&quot; class=&quot;headerlink</summary>
      
    
    
    
    <category term="运维" scheme="https://blog.jugg.xyz/categories/%E8%BF%90%E7%BB%B4/"/>
    
    
    <category term="docker" scheme="https://blog.jugg.xyz/tags/docker/"/>
    
    <category term="buildx" scheme="https://blog.jugg.xyz/tags/buildx/"/>
    
  </entry>
  
  <entry>
    <title>为什么国内大厂很难在toB领域出成绩</title>
    <link href="https://blog.jugg.xyz/2020/12/10/repost/Why-is-it-hard-for-the-big-domestic-manufacturers-to-make-a-mark-in-the-toB-field/"/>
    <id>https://blog.jugg.xyz/2020/12/10/repost/Why-is-it-hard-for-the-big-domestic-manufacturers-to-make-a-mark-in-the-toB-field/</id>
    <published>2020-12-10T12:33:33.000Z</published>
    <updated>2026-05-02T11:24:14.335Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文转载自<a href="https://twitter.com/passluo?s=20"> Passluo </a>的<a href="https://twitter.com/passluo/status/1290750619624411136?s=20"> 推文 </a></p></blockquote><h2 id="一、领导层的困境"><a href="#一、领导层的困境" class="headerlink" title="一、领导层的困境"></a>一、领导层的困境</h2><p>国内大厂业务多以toC或广告为主要收入来源，行业规模大、增长方法简单、来钱快，领导层长期都是在追求如何短平快地增长。然而toB侧重垂直领域或专业方向，目标企业绝对数量不大，无法像toC那样舍弃客户。只能将需求和应用场景逐一覆盖，所以注定是个慢工出细活、长期投入的事情。</p><p>从企业角度出发，toB产品或服务的采购决策更加理性、周期也相对较长。除非是行业刚需的革命性产品（比如AI替代人工打骚扰电话），否则增长根本快不起来。大多需要依靠市场和销售人员不断去接触客户、了解市场需求、宣传产品和品牌、教育企业用户，从而最终培育出销售机会。</p><p>现在市场上能见到的比较大的toB软件产品大多都是从 2012-2015 这一波做起来的，有些甚至还更早。北森03年、帆软06年创业，金蝶、用友、金山这些就更不用说了。这些企业都是花了十多年、几十年的时间才积累了行业地位、品牌和足够的商业壁垒，并且现在还不断在被后起之秀挑战。</p><p>那一个toC出身的大佬哪儿能耐得住性子花几年去摸市场、磨产品、看用户、造品牌，最后出成绩？你耐得住，上面的那些CEO、董事会也耐不住。他们不会给你那么多时间，因为试错时间成本太高。并且toB产品或服务营收规模都不会太大，年收入能过10亿的赛道屈指可数。但10亿对于toC大厂算个啥？抖音3天的广告收入？</p><p>事情难做、时间成本高、ROI低、在集团内被看不起，这大概就是toB领导层的困境。所以我们看到大厂的toB团队大多都是些低level的Leader在带。他们常常急于求成，按toC的打法和要求去管理toB团队，瞎折腾。最后产品很难做好，部门出不了成绩，团队心力交瘁、气势低落、怨声载道、离职率居高不下。</p><p>阿里云当然是特例，它很难得得克服了上述困境。首先阿里有toB基因。其次是当年马云力排众议力挺王坚。</p><p>这是非常大的决心和赌注，除了因为云基建这条赛道市场前进足够大，也是因为除了钱以外的战略意义。即便如此，2019年财报看，阿里云也只是阿里电商营收的10%而已。</p><p>钉钉就不用提了，赔钱货。</p><h2 id="二、人才的困境"><a href="#二、人才的困境" class="headerlink" title="二、人才的困境"></a>二、人才的困境</h2><p>总的来说toB人才会比toC要求高一点，特别是产品层面。这里面有个矛盾的点就在于有toB领域经验的人不懂互联网，不具备互联网产品人需要的能力，而互联网大部分产品人不懂垂直领域，不懂toB的游戏规则和套路，不知道怎么切入领域。</p><p>这是目前toB人才困境的主要原因，也是在这一章节我主要想和大家讨论的。先来看看toB和toC产品工作的区别。</p><p>大家平时讨论最多的都是toC互联网，听到最多的一个词是「风口」。为什么toC那么在意风口？因为toC强调创新和需求体量。toC爆发通常靠两点：更好地解决需求 &#x2F; 创造新需求。这个过程需要不断试错，费时费力费钱。相比之下更聪明的做法肯定是抄作业、抢风口。</p><p>既然是抢风口，比的就是谁快。什么鸡巴精益创业、敏捷开发、弹性架构、人月神话，只要业务能跑起来、让运营去做增长，管你是PHP、Python还是易语言写出来的代码，能Run就行。而且初期系统挂的越多越好，挂的多说明你业务增长快，说明你火爆。越挂越有人想注册，去投资人那这理由还能加钱。</p><p>在这种氛围的长期熏陶下，toC产品人越发重视细节、重视核心想法的表达、越发去抓大放小、越发忽略系统的顶层架构和长远战略。另外由于toC病毒传播的可行性强，产品人会觉得只要发点优惠券烧钱、广告轰炸烧钱、做足微信传播，用户自然就能指数增长。</p><p>当他们涉足toB领域时，发现这些套路根本不Work。</p><p>反观toB，机会真的遍地都是。</p><p>记得有个投资人说过「数字经济下，几乎每个行业都值得信息化改造」。但toB垂直领域体量一般都很小。比如法律领域，全中国持证律师不超过50w人，律所不超过5w家，愿意付钱的可能就几万人、几千家律所。在这样的情况下，不稳扎稳打、深耕细作，是不可能有所起色的。</p><p>对于toC来说，一些需求没被满足不要紧，你可以舍弃这波用户，反正中国有14亿人。但对于toB来说，潜在用户一共就那么多，这里舍弃点、那里舍弃点，你还有多少用户？你还做个毛线？所以必须深耕细作，争取把行业通吃，toB里面赢家通吃是很常见的。</p><p>而深耕细作依赖行业理解。如果你没有参与过销售管理，你就很难明白为什么CRM里需要那么复杂的销售线索分配机制。然而现在的互联网产品人，大多一毕业就进入互联网圈，没有接触行业一线的机会，也不愿意去了解。</p><p>互联网来钱太容易，PM都干不了脏活。不信你问问身边的，有几个敢去主动给用户打电话？而那些在行业里经验丰富的人呢？互联网公司嫌弃他们又土又穷、不懂互联网，很少给他们转业的机会。这些人因为专业、技能、经验和学历的原因，不太容易进入互联网行业；即便进入了，也不可能担任重要角色。可以说很大一部分想法和创新都被封闭和埋没在了领域内部。</p><p>这么说肯定有点太抬高领域人才而贬低PM们了。事实上你让一个行业大佬来做互联网，大概率难有起色。无讼的创始人是全国顶级律师，产品一坨屎；iCourt创始人是搞律师培训的，产品年收入破亿。toB产品人需要把互联网和行业知识相结合，打造完整的产品研发和服务团队。有这能力的人，凤毛麟角。</p><p>公司之所以是公司，毕竟还是人的事情。大公司成型以后，人才建设和储备基本固化，toB专业人才输入的问题从根本上就被卡死，没办法解决。一个毛都不懂的Leader，带领一群想当然的PM，去折腾一群老实巴交的程序员、运营和销售。这样的团队，能在toB领域做出成绩，那可真是彗星撞地球哦。</p><h2 id="三、组织管理的困境"><a href="#三、组织管理的困境" class="headerlink" title="三、组织管理的困境"></a>三、组织管理的困境</h2><p>公司大到一定规模，就没办法再像小团队那样进行协作。</p><p>360 人才评估、绩效、OKR、KPI、季度考核、年末考核、晋升答辩……管理者们发明了一大堆制度和工具，妄想能让团队继续保持高效协作。殊不知，这些方法和工具只是在降低管理者的工作难度，对于一线效率是大大的负增益</p><p>一个 toB 小公司的 PM 洞察到一个改进点。简单地在草稿纸上画了一下草图，利用中午吃饭时间和关系好的研发小哥们简单说一下，吃完饭回来找前端快速出一个交互和界面。到了晚上 10 点班车发布后，PM 的微信上就收到消息 “那个玩意儿已经发布了，你快去线上测下看看”</p><p>与此同时大厂又是怎么样的风貌呢？PM 首先需要撰写一篇狗屁不通的 PRD，产品总监需要像上朝一样一一评审，设计得出一套保高保原型，然后找人调研测试，因为只有这样研发才肯开搞，技术方案和文档一写就是一两天。好不容易代码交付，又是一堆测试和发布流程。周报上收获满满，客户那两周干着急。</p><p>我知道看到这里有人要坐不住了：</p><p>「不这么做，怎么保证质量？」</p><p>「软件作坊那样做很不符合规范」</p><p>「这样搞就是为以后埋坑」</p><p>「北大青鸟培训班出来的才这么开发」</p><p>「听你这么说，就知道你没系统学习过软件工程」</p><p>来我先统一回复一下：你没理解我说的意思。</p><p>我当然不反对科学、规范、系统地进行软件研发工作。互联网圈很多都是高学历的聪明人，你们都知道什么才是理想化的协作方式，然而在大公司，这一切都变味了。美国有「政治正确」，而互联网公司有「流程正确」，规范和方法成了避免背锅的工具，让本来应该简单敏捷的迭代过程变得复杂而冗余。非常不敏捷的「敏捷开发流程」只是一个例子。</p><p>除此之外，大厂还有一万种方法来拖累组织协作效率。一个季度就 13 周，大厂需要在上季末花费 2 周来总结复盘，再在季度初花 2 周来确定计划目标，接下来的 2 周通常是目标的拆解和确认。你算算吧，真正能干活儿的时间还有多少？</p><p>为了管理庞大的组织体系，大厂花费了非常多的心血在制定组织协作规范上，而这样的规范正是让公司变得臃肿低效的元凶。当大厂的 toB 团队面对市场时，沿用下来的、不可更改的协作规范变成了束缚他们的枷锁。市场的声音、用户的反馈都在淹没在了无尽的 meeting schedule 里。</p><p>你说，这仗你怎么打得过？</p><h2 id="四、赛道选型的困境"><a href="#四、赛道选型的困境" class="headerlink" title="四、赛道选型的困境"></a>四、赛道选型的困境</h2><p>很少听说大厂在 toB 领域有一些跨界且出色的产品对不对？</p><p>阿里的语雀、腾讯的 TAPD、字节的 Lark，这些其实都脱胎于内部需求。虽然面向企业用户、工作场景，但产品形态和运营方式上更加接近于 toC。</p><p>而在 toB 领域一些市值更高的常规赛道，比如：</p><ul><li>供应链管理</li><li>生产管理</li><li>销售管理</li><li>组织管理</li><li>客户服务</li><li>流程管理</li></ul><p>大厂一直没有切进去，或者说切进去了也没有做的很好。大厂在赛道选型的问题上，相比其他创业公司有着诸多劣势，视野容易受到限制。大多只能看到和自己业务相关联的领域，或者从内部需求开始孵化，再逐渐外延至商业化。而以大厂自己的需求去开发商业化产品，很容易跑偏走型。</p><p>大厂能叫大厂，本身就说明了他们的独一无二性，满足大厂需求的产品注定是不普适的。而 toB 产品，特别是 toB SaaS 强调的是边际成本递减，说白了就是标准化和规模化获客。大型企业一共就那么点儿，哪儿来的规模化让你实现边际成本递减呢？</p><p>当然聪明的你已经发现了，其实这并不绝对。对的，Saleforce 和 Workday 这些 toB 软件服务的巨头确实特殊。他们 80% 的收入都是来自于世界 500 强，并不需要太多客户就能达到一个非常高的收入规模。但这些企业的选型之初也并不是因为内发的需求，而是在不断尝试的过程中发现了一个不错的赛道深挖下去。</p><p>不管是中国还是西方国家，中小企业肯定是占据了市场主流，他们虽然无法提供规模宏大的收益、无法提供媲美 500 强的预算稳定性。但是 SMB 能提供规模宏大的客户群，客户群给了你丰富的市场反馈和试错验证的机会。这对于 toB 创业的早起来说，远比现金流的回报，意义要大得多。</p><p>toB 大厂里的员工长期接收的都是内部的声音，很容易将某种需求扩大化，从而选择了不是那么有市场价值的赛道。即便你看到了一个比较好的赛道，你的 Leader 们可能也很难支持你去做，因为你可能不是很懂，并且你的 Leader 也不是很懂，很难控制风险。对于大厂来说，控制风险比寻找机会重要的多。</p><p>那些从大厂里面跳出来去创业、并且有所成绩的，很多都是这种情况。这样的例子非常多，并且换到 toC 赛道也依然适用。否则，按逻辑，每个大厂都应该成为制霸全球的霸主才对。因为不管从人才、资金还是资源上，他们拥有的都比 startups 多太多。</p><p>那大厂就不能等一个 toB 赛道初具规模、被验证过了是可行道路以后，再使劲砸进去？目前看，很难，原因就是上述的 一、二、三点。</p><p>成功的例子恐怕也只有阿里云了。</p><h2 id="五、写在最后的话"><a href="#五、写在最后的话" class="headerlink" title="五、写在最后的话"></a>五、写在最后的话</h2><p>所以说，怕什么呢朋友们？</p><p>在今天的中国进行 toB 创业真的是绝好的机会。又有市场、又不用担心巨头的侵扰，是非常有潜力的一个领域。</p><p>我写这么多，除了吐槽大厂的不争气，其实更多也是鼓励大家不要害怕。互联网的魅力所在，就是永远有巨大的机会等你去挖掘！</p><p>谢谢各位，祝好。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本文转载自&lt;a href=&quot;https://twitter.com/passluo?s=20&quot;&gt; Passluo &lt;/a&gt;的&lt;a href=&quot;https://twitter.com/passluo/status/129075061962441113</summary>
      
    
    
    
    <category term="转载" scheme="https://blog.jugg.xyz/categories/%E8%BD%AC%E8%BD%BD/"/>
    
    
    <category term="startups" scheme="https://blog.jugg.xyz/tags/startups/"/>
    
  </entry>
  
  <entry>
    <title>使用 ceph-deploy 部署 Ceph 集群</title>
    <link href="https://blog.jugg.xyz/2020/07/02/ops/Ceph-Deploy/"/>
    <id>https://blog.jugg.xyz/2020/07/02/ops/Ceph-Deploy/</id>
    <published>2020-07-02T18:16:33.000Z</published>
    <updated>2026-05-02T11:24:14.332Z</updated>
    
    <content type="html"><![CDATA[<h1 id="部署-Ceph-存储集群"><a href="#部署-Ceph-存储集群" class="headerlink" title="部署 Ceph 存储集群"></a>部署 Ceph 存储集群</h1><h2 id="前置条件"><a href="#前置条件" class="headerlink" title="前置条件"></a>前置条件</h2><ol><li>管理节点下需要将所有节点对应的主机名和 IP 写入 hosts 方便批量操作（所有节点需要用不同的主机名）</li><li>所有节点配置免密登录方便管理节点使用 ceph-deploy 部署(用户需要有sudo权限，并且是NOPASSWD)</li></ol><h2 id="重置环境（仅在原环境上操作需要使用）"><a href="#重置环境（仅在原环境上操作需要使用）" class="headerlink" title="重置环境（仅在原环境上操作需要使用）"></a>重置环境（仅在原环境上操作需要使用）</h2><p>该操作需要在集群的管理节点的 ceph-deploy 目录下操作。</p><ol><li><p>删除所有节点的ceph软件 </p><pre><code> $ ceph-deploy purge {ceph-node} [{ceph-node}]</code></pre></li><li><p>删除所有节点的ceph软件数据</p><pre><code> $ ceph-deploy purgedata {ceph-node} [{ceph-node}]</code></pre></li><li><p>删除所有节点的keys</p><pre><code> $ ceph-deploy forgetkeys</code></pre></li><li><p>删除ceph配置文件</p><pre><code> $ rm ceph.*</code></pre></li></ol><h2 id="自动部署（ceph-deploy）"><a href="#自动部署（ceph-deploy）" class="headerlink" title="自动部署（ceph-deploy）"></a>自动部署（ceph-deploy）</h2><h3 id="安装-ceph-deploy"><a href="#安装-ceph-deploy" class="headerlink" title="安装 ceph-deploy"></a>安装 ceph-deploy</h3><pre><code># pip install ceph-deploy</code></pre><hr><h3 id="存储集群"><a href="#存储集群" class="headerlink" title="存储集群"></a>存储集群</h3><p>由于官方的源再国内访问所以需要使用第国内供的软件源，通过配置环境变量使用 aliyun 提供的 ceph-mimic 版本的仓库。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash"><span class="built_in">export</span> CEPH_DEPLOY_REPO_URL=https://mirrors.aliyun.com/ceph/debian-mimic</span></span><br></pre></td></tr></table></figure><h4 id="创建集群"><a href="#创建集群" class="headerlink" title="创建集群"></a>创建集群</h4><p>先在管理节点上创建一个目录，用于保存 ceph-deploy 生成的配置文件和密钥对。</p><pre><code># mkdir my-cluster# cd my-cluster</code></pre><p>然后开始创建监控节点：</p><pre><code># ceph-deploy new 监控节点主机名</code></pre><p>这里需要把所有设备配置好免密登录，以及把所有设备的主机名和IP的对应关系写入管理节点的 hosts ，防止网络发现没有生效。另外因为 ceph-deploy 是直接调用 ssh 远程操作节点，而且会使用 sudo ，而且不支持 root 登录（官方的口径是 root 不支持 sudo ，但是实际上是可以用的。），所以要配置好用户的 sudo 免密操作。</p><h4 id="配置文件"><a href="#配置文件" class="headerlink" title="配置文件"></a>配置文件</h4><p>ceph 自带的容灾配置，默认是保存三份副本，可以把 <code>osd pool default size = 3</code> 写入 Ceph 配置文件的 <code>[global]</code> 段下。更改为其他数量。</p><p>另外如果你有多个网卡，也可以可以把 public network 写入 Ceph 配置文件的 <code>[global]</code> 段下。</p><p>以下是我的配置文件：</p><figure class="highlight shell"><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><span class="line">[global]</span><br><span class="line">fsid = aa7852b8-dd0b-40d4-a2f4-bd1f38c6dc69</span><br><span class="line">mon_initial_members = node3, node4</span><br><span class="line">mon_host = 192.168.1.3,192.168.1.4</span><br><span class="line">auth_cluster_required = cephx</span><br><span class="line">auth_service_required = cephx</span><br><span class="line">auth_client_required = cephx</span><br><span class="line">osd pool default pg num = 4096</span><br><span class="line">osd pool default pgp num = 4096</span><br><span class="line">osd pool default size = 3</span><br><span class="line">mon_allow_pool_delete = true</span><br><span class="line">mon clock drift allowed = 0.1</span><br><span class="line">mon data avail warn = 10</span><br><span class="line">osd pool default min size = 1</span><br></pre></td></tr></table></figure><h4 id="为所有节点安装-Ceph"><a href="#为所有节点安装-Ceph" class="headerlink" title="为所有节点安装 Ceph"></a>为所有节点安装 Ceph</h4><pre><code># ceph-deploy install 所有节点主机名（包括管理节点）</code></pre><h4 id="配置初始-monitor-s-、并收集所有密钥"><a href="#配置初始-monitor-s-、并收集所有密钥" class="headerlink" title="配置初始 monitor(s)、并收集所有密钥"></a>配置初始 monitor(s)、并收集所有密钥</h4><pre><code># ceph-deploy mon create-initial</code></pre><p>如有报错，请检查管理节点的 &#x2F;etc&#x2F;hosts、~&#x2F;.ssh&#x2F;config 里的所有主机名和 IP 与监控节点的主机名和 IP 名一致。另外还有多个节点</p><hr><h3 id="创建集群-1"><a href="#创建集群-1" class="headerlink" title="创建集群"></a>创建集群</h3><h4 id="分发管理节点的配置以及密钥"><a href="#分发管理节点的配置以及密钥" class="headerlink" title="分发管理节点的配置以及密钥"></a>分发管理节点的配置以及密钥</h4><pre><code># ceph-deploy admin  所有节点名</code></pre><h4 id="为监控节点部署管理器守护程序"><a href="#为监控节点部署管理器守护程序" class="headerlink" title="为监控节点部署管理器守护程序"></a>为监控节点部署管理器守护程序</h4><pre><code># ceph-deploy mgr create 所有监控节点</code></pre><h4 id="创建-OSD"><a href="#创建-OSD" class="headerlink" title="创建 OSD"></a>创建 OSD</h4><pre><code># ceph-deploy osd create --data {device} {ceph-node}示例：ceph-deploy osd create --data /dev/sdb node30</code></pre><p>OSD 创建完毕后再检查一些集群的健康情况(当使用ceph时需要使用管理员权限)：</p><pre><code># ceph health</code></pre><p>也可以用 <code>ceph -s</code> 查看集群的详细信息。</p><figure class="highlight shell"><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></pre></td><td class="code"><pre><span class="line">vm@Lotus-Master ~/my-cluster % sudo ceph -s</span><br><span class="line">  cluster:</span><br><span class="line">    id:     8d24cae1-07a9-4c3c-a74c-d0b56e4ad243</span><br><span class="line">    health: HEALTH_OK</span><br><span class="line"></span><br><span class="line">  services:</span><br><span class="line">    mon: 2 daemons, quorum node3,node4 (age 2m)</span><br><span class="line">    mgr: node7(active, since 43m), standbys: node8, node9, node10, node11, node12, node15, node16, node6, node14, node18, Lotus-Master, node3, node5, node4</span><br><span class="line">    osd: 14 osds: 14 up (since 2m), 14 in (since 15m)</span><br><span class="line"></span><br><span class="line">  data:</span><br><span class="line">    pools:   0 pools, 0 pgs</span><br><span class="line">    objects: 0 objects, 0 B</span><br><span class="line">    usage:   14 GiB used, 114 TiB / 114 TiB avail</span><br><span class="line">    pgs:</span><br></pre></td></tr></table></figure><hr><h2 id="创建-Cephfs"><a href="#创建-Cephfs" class="headerlink" title="创建 Cephfs"></a>创建 Cephfs</h2><h3 id="创建-mds-节点"><a href="#创建-mds-节点" class="headerlink" title="创建 mds 节点"></a>创建 mds 节点</h3><pre><code># ceph-deploy mds create {ceph-node}</code></pre><p>CephFS 中的所有元数据操作都通过 mds 进行，因此至少需要一台 mds 节点。为了防止单点故障，在这里可以配置多个mds 服务器。默认的</p><h3 id="创建-pool"><a href="#创建-pool" class="headerlink" title="创建 pool"></a>创建 pool</h3><pre><code># ceph osd pool create cephfs_data 2048# ceph osd pool create cephfs_meta 512</code></pre><p>创建了一个名为 cephfs_data 和一个名为 cephfs_meta 的 pool 。关于 pg 数量的配置公式如下。当使用公式计算时，pg 数一定要是贴近计算值的2的指数值。譬如有两百个 OSD ，两个 pool ，三个副本，则计算值为200*100&#x2F;2&#x2F;3&#x3D;4000，pg 值应设置为最接近 4000 的 2 的指数值 4096。其中 cephfs 的 data pool 和 metadata pool 的 pg 数按 <a href="http://lists.ceph.com/pipermail/ceph-users-ceph.com/2017-March/016965.html">ceph官方邮件</a> 讨论中来说，建议是 4：1 的比例。</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><span class="line">                         (OSDs * 100)</span><br><span class="line">Total PGs =  ---------------------------------------</span><br><span class="line">              (pool count) * (max replication count)</span><br></pre></td></tr></table></figure><p>另外可通过 <code>sudo ceph osd lspools</code> 来查看已有的 pool，如果要删除 pool 可以使用以下命令：</p><pre><code>#  ceph osd pool delete  cephfs_data cephfs_data --yes-i-really-really-mean-it</code></pre><h3 id="创建-cephfs"><a href="#创建-cephfs" class="headerlink" title="创建 cephfs"></a>创建 cephfs</h3><pre><code># ceph fs new mycephfs cephfs_meta cephfs_data</code></pre><h3 id="挂载-cephfs"><a href="#挂载-cephfs" class="headerlink" title="挂载 cephfs"></a>挂载 cephfs</h3><p>监控节点的密钥可以通过在监控节点执行 “sudo cat &#x2F;etc&#x2F;ceph&#x2F;ceph.client.admin.keyring|grep key|cut -d” “ -f3” 获取。</p><pre><code># mount -t ceph 监控节点IP:6789:/ 目录挂载点 -o name=admin,secret=&quot;监控节点密钥&quot;,acl,async,rw,noexec,nodev,noatime,nodiratime</code></pre><p>示例：</p><pre><code># mount -t ceph 192.168.1.30:6789,192.168.1.32:6789,192.168.1.34:6789,192.168.1.35:6789,192.168.1.37:6789:/ /lotus -o name=admin,secret=&quot;AQCyVdde8Y5EDBAAQCrFCJYVsmXZHAZC+4mAJQ==&quot;,acl,async,rw,noexec,nodev,noatime,nodiratime</code></pre><hr><h2 id="实施中遇到的其他问题"><a href="#实施中遇到的其他问题" class="headerlink" title="实施中遇到的其他问题"></a>实施中遇到的其他问题</h2><ol><li><p>用parted分区后的分区表不会立即生效，需要重启。如果不想重启，可以使用 <code>partprobe</code> 命令，此命令可以让系统内核重新读取分区表信息，就不用重新启动电脑。</p></li><li><p>如果硬盘在使用之前已经有了 GPT 分区创建 OSD 的时候就会报报 <code>error: GPT headers found</code> ，这时可以直接用 dd 将 GPT 分区删掉：</p> <figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/zero of=/dev/sdb bs=512K count=1</span></span><br></pre></td></tr></table></figure><p> 如果需要添加的硬盘在已存在的lvm卷组里，譬如重置环境后需要加入新的集群的 osd ，可以用以下命令先脱离 lvm ，再删除GPT分区：</p> <figure class="highlight shell"><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><span class="line"><span class="meta prompt_"># </span><span class="language-bash">lvremove -vf `lvdisplay|grep <span class="string">&quot;LV Path&quot;</span>|awk <span class="string">&#x27;&#123;print $3&#125;&#x27;</span>`</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">vgremove -vf `vgdisplay|grep <span class="string">&quot;VG Name&quot;</span>|awk <span class="string">&#x27;&#123;print $3&#125;&#x27;</span>`</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">pvremove -vf `pvdisplay|grep <span class="string">&quot;PV Name&quot;</span>|awk <span class="string">&#x27;&#123;print $3&#125;&#x27;</span>`</span></span><br></pre></td></tr></table></figure></li><li><p>修改 ceph.conf后要使用 <code>ceph-deploy --overwrite-conf config push 所有节点名称</code> 来把新配置推送到所有节点上。</p></li><li><p>如果删除 pool 报错 <code>Error EPERM: pool deletion is disabled; you must first set the mon_allow_pool_delete config option to true before you can destroy a pool</code> ，则可以添加 <code>mon_allow_pool_delete = true</code> 到 ceph.conf ，然后执行 <code>ceph osd pool delete  lotus lotus --yes-i-really-really-mean-it</code> ，或者用下面的方法</p> <figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">ceph tell mon.\* injectargs <span class="string">&#x27;--mon-allow-pool-delete=true&#x27;</span></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">ceph osd pool delete  lotus lotus --yes-i-really-really-mean-it</span></span><br></pre></td></tr></table></figure></li><li><p>错误：</p><pre><code> # rbd: create error: (33) Numerical argument out of domain # 2020-03-09 22:38:32.177 7fe1a39ecf40 -1 librbd::image::CreateRequest: validate_order: order must be in the range [12, 25]</code></pre><p> 原因：<code>rbd_default_order</code> 配置过高，这个配置项决定了集群内切块的大小，默认是22，范围是[12, 25]，不在这个范围都会报错。</p></li><li><p>错误：集群大小与实际存储容量不符合</p><p>原因：RBD 进行数据删除的时候实际上并不会直接删除对象，而是给对象打上已删除的标记，所以删除内容还是在集群内占用空间。这里需要通过文件系统去主动释放被标记为已删除的对象 <code>sudo fstrim rbd挂载目录</code> 。</p></li><li><p>错误：pg 数设置错误，需要调整</p><p>解决方法（另外这里需要注意，pg 数不能一次性扩大太多，每次调整的大小尽量不要超过100%）：</p><pre><code> # ceph osd pool set cephfs_data pg_num 4096 # ceph osd pool set cephfs_data pgp_num 4096</code></pre></li><li><p>错误：监控节点数量添加少了，需要调整</p><p>解决方法：</p><pre><code> # ceph-deploy mon add 新增监视器主机名</code></pre><p> 这里有一点要注意，所有监视器一定要设置 NTP 同步，尽量选择同一个 NTP 服务器进行同步。</p><p> 而一旦添加了新的Ceph监视器，Ceph将开始同步监视器并形成仲裁。可以通过执行以下操作检查仲裁状态：</p><pre><code> # ceph quorum_status --format json-pretty</code></pre></li><li><p>错误：cephfs出现错误需要重建，删除pool却显示pool正在被cephfs使用中</p><p>解决方法：</p><p> 首先停止mds的服务</p><pre><code> # systemctl stop ceph-mds@$HOSTNAME</code></pre><p> 然后 设置mds为失败</p><pre><code> # ceph mds fail 0    </code></pre><p> 接着删除cephfs</p><pre><code> # ceph fs rm mycephfs --yes-i-really-mean-it  </code></pre><p> 然后删除cephfs使用的pool</p><pre><code> # ceph osd pool delete  lotus lotus --yes-i-really-really-mean-it</code></pre></li></ol><hr>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;部署-Ceph-存储集群&quot;&gt;&lt;a href=&quot;#部署-Ceph-存储集群&quot; class=&quot;headerlink&quot; title=&quot;部署 Ceph 存储集群&quot;&gt;&lt;/a&gt;部署 Ceph 存储集群&lt;/h1&gt;&lt;h2 id=&quot;前置条件&quot;&gt;&lt;a href=&quot;#前置条件&quot; cla</summary>
      
    
    
    
    <category term="运维" scheme="https://blog.jugg.xyz/categories/%E8%BF%90%E7%BB%B4/"/>
    
    
    <category term="Linux" scheme="https://blog.jugg.xyz/tags/Linux/"/>
    
    <category term="Ceph" scheme="https://blog.jugg.xyz/tags/Ceph/"/>
    
  </entry>
  
  <entry>
    <title>记一次 Python 脚本坑</title>
    <link href="https://blog.jugg.xyz/2019/10/09/ops/Python-Exercises/"/>
    <id>https://blog.jugg.xyz/2019/10/09/ops/Python-Exercises/</id>
    <published>2019-10-09T17:09:42.000Z</published>
    <updated>2026-05-02T11:24:14.333Z</updated>
    
    <content type="html"><![CDATA[<h2 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h2><p>公司有百来台矿机接入 f2pool 在挖 eth ，但是系统用的并不是我们自己的，也没办法提供权限，所以监控上有一点麻烦。还好 f2pool 也算是比较大的矿池了，看了一下，有提供 api 给用户使用，就写一个监控脚本吧。蛤？ f2pool 还提供微信推送监控？emmmm………别人的哪有自己写的好（其实是因为没有需求也要创造需求（逃</p><hr><h2 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h2><ul><li>记录掉线矿机 ID 以及掉线时间</li><li>矿机掉线推送消息到 telegram channel </li><li>矿机上线后消息自动删除，防止 channel 内消息过多</li></ul><hr><h2 id="设计"><a href="#设计" class="headerlink" title="设计"></a>设计</h2><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">main</span>():</span><br><span class="line">    通过 getData() 函数获得推送消息的输出内容以及是否推送的判断，调用 saveLog() 函数并将推送消息内容作为传参，同时将接收系统当前时间的返回值，通过 <span class="keyword">if</span> 判断消息是否推送，如推送则调用 postMessages()函数开始推送。</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">getData</span>(): </span><br><span class="line">    requests.get 获取访问 api ，通过 decode 方法解码返回值的 content 属性内容，得到完整的 json 数据。然后通过 json.loads 将 json 数据转换成 <span class="built_in">dict</span> 数据，通过 get 方法取得矿机总数，总算力以及所有矿机详细数据。</span><br><span class="line">        再通过循环取所有矿机的最后一次提交时间，将矿机带时区格式的最后提交时间转换成时间戳格式与当前时间比对，超过十分钟的就算掉线矿机。同时为避免下架矿机频繁推送造成影响，选择掉线超过八小时则默认掉线设备，不予统计。将所有掉线设备计入列表，并综合所有数据产生输出以及是否推送的判断作为返回值。</span><br><span class="line">    </span><br><span class="line"><span class="keyword">def</span> <span class="title function_">deleteMessage</span>(<span class="params">messageID</span>):</span><br><span class="line">    接受传递过来的消息 ID ，通过 api 删除该消息。</span><br><span class="line">    </span><br><span class="line"><span class="keyword">def</span> <span class="title function_">postMessages</span>(<span class="params">minerData</span>):</span><br><span class="line">    通过api推送消息，并记录返回值，将推送消息 ID 存下，二十分钟后调用 deleteMessage() 函数自动删除该消息，防止 channel 内消息过多，影响查看。</span><br><span class="line">    </span><br><span class="line"><span class="keyword">def</span> <span class="title function_">saveLog</span>(<span class="params">minerData</span>):</span><br><span class="line">    接受推送内容参数并记录到 log 文件，同时将当前设备时间作为返回值。</span><br></pre></td></tr></table></figure><hr><h2 id="坑"><a href="#坑" class="headerlink" title="坑"></a>坑</h2><p>我是比较喜欢新鲜技术的人，俗称小白鼠，所以脚本定时推送选了 systemd timer （其实也并不新，已经沦为主流了），结果推送几乎每两天就挂掉一次，于是开始排查。</p><p>第一个版本我写入日志用的是 open() 函数，但是我没有写 close() ，我怀疑是不是文件流打开没有关闭导致的推送卡住，于是我学会了 with-as 语句。然而现实是残酷的，没两天推送又挂了。我开始怀疑是 systemd 的锅，尝试了各种配置方案，无果。最终，偶然在手动调试的时候发现 api 有时候不会返回数据，而且会一直卡在这，我立刻醒悟过来，可能是 requests 拿不到数据反而把自己耽误了，于是加上 timeout 终于了结了。</p><p>其实这里我还是有点疑惑，我 systemd 的配置写的是 oneshot ，按照 systemd 的说明，这个选项应该是无论程序执行结果，都会去执行下一次任务，但是却也被卡住了，也许我还没有找到真正的原因……</p><hr><h2 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h2><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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">#!/bin/env python</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> requests</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"><span class="keyword">from</span> threading <span class="keyword">import</span> Timer</span><br><span class="line"></span><br><span class="line">ISOTIMEFORMAT = <span class="string">&#x27;%Y-%m-%d %H:%M:%S&#x27;</span></span><br><span class="line">LINE = <span class="string">&quot;\n-------------------------------------------------------------------------------------------------------&quot;</span></span><br><span class="line">DELETETIME = <span class="number">1190</span></span><br><span class="line">URL = &#123;<span class="string">&#x27;sendMessage&#x27;</span>: <span class="string">&#x27;**YOURTGBOTAPIKEY**/sendMessage&#x27;</span>,</span><br><span class="line">       <span class="string">&#x27;deleteMessage&#x27;</span>: <span class="string">&#x27;**YOURTGBOTAPIKEY**/deleteMessage&#x27;</span>&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">getData</span>():</span><br><span class="line">    <span class="comment"># 从 f2pool api 获取矿机实时数据</span></span><br><span class="line">    ethminer = requests.get(</span><br><span class="line">        url=<span class="string">&#x27;**YOURURL**&#x27;</span>, timeout=<span class="number">30</span>)</span><br><span class="line">    ethminerDict = json.loads(ethminer.content.decode())</span><br><span class="line">    minerNumber = ethminerDict.get(<span class="string">&#x27;worker_length&#x27;</span>)</span><br><span class="line">    minerWorkers = ethminerDict.get(<span class="string">&#x27;workers&#x27;</span>)</span><br><span class="line">    minerHashrate = (ethminerDict.get(<span class="string">&#x27;hashrate&#x27;</span>)/<span class="number">1000000</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 统计掉线矿机 ID 及掉线时间</span></span><br><span class="line">    dict0 = &#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(minerNumber):</span><br><span class="line">        minerTime = (minerWorkers[i][<span class="number">6</span>])</span><br><span class="line">        minerTimeArray = time.strptime(minerTime, <span class="string">&quot;%Y-%m-%dT%H:%M:%S.%fZ&quot;</span>)</span><br><span class="line">        minerTimeStamp = time.mktime(minerTimeArray)</span><br><span class="line">        timeNow = time.mktime(time.gmtime(time.time()))</span><br><span class="line">        timeDiff = (timeNow-minerTimeStamp)</span><br><span class="line">        <span class="keyword">if</span> <span class="number">28800</span> &gt; timeDiff &gt; <span class="number">600</span>:</span><br><span class="line">            dict0[minerWorkers[i][<span class="number">0</span>]] = <span class="string">&quot;已掉线&quot;</span>+<span class="built_in">str</span>(timeDiff//<span class="number">60</span>)+<span class="string">&quot;分钟&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 判断是否有矿机掉线或者算力不正常</span></span><br><span class="line">    offlineNumber = <span class="built_in">len</span>(dict0)</span><br><span class="line">    <span class="keyword">if</span> dict0 <span class="keyword">or</span> minerHashrate &lt; <span class="number">30000</span>:</span><br><span class="line">        judge = <span class="literal">True</span></span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        judge = <span class="literal">False</span></span><br><span class="line">        dict0 = <span class="string">&quot;无&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 回传判断与推送消息</span></span><br><span class="line">    minerData = <span class="string">&quot;不得了了,矿机掉线了！&quot;</span>+<span class="string">&quot;\r\n&quot;</span>+<span class="string">&quot;目前掉线&quot;</span> + \</span><br><span class="line">        <span class="built_in">str</span>(offlineNumber)+<span class="string">&quot;台,掉线设备ID为:&quot;</span>+<span class="string">&quot;\r\n&quot;</span>+<span class="built_in">str</span>(dict0) + \</span><br><span class="line">        <span class="string">&quot;\r\n&quot;</span>+<span class="string">&quot;所有矿机总算力为&quot;</span>+<span class="built_in">str</span>(minerHashrate)+<span class="string">&quot;MH/s&quot;</span></span><br><span class="line">    <span class="built_in">print</span>(judge,minerData)</span><br><span class="line">    <span class="keyword">return</span> judge, minerData</span><br><span class="line"></span><br><span class="line"><span class="comment"># 自动删除消息推送</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">deleteMessage</span>(<span class="params">messageID</span>):</span><br><span class="line">    deletePost = requests.post(URL[<span class="string">&#x27;deleteMessage&#x27;</span>],</span><br><span class="line">                               data=&#123;<span class="string">&#x27;chat_id&#x27;</span>: <span class="string">&#x27;-1001241741624&#x27;</span>, <span class="string">&#x27;message_id&#x27;</span>: messageID&#125;)</span><br><span class="line">    <span class="built_in">print</span>(deletePost)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 推送消息</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">postMessages</span>(<span class="params">minerData</span>):</span><br><span class="line">    pushPost = requests.post(URL[<span class="string">&#x27;sendMessage&#x27;</span>],</span><br><span class="line">                             data=&#123;<span class="string">&#x27;chat_id&#x27;</span>: <span class="string">&#x27;-1001241741624&#x27;</span>, <span class="string">&#x27;text&#x27;</span>: minerData&#125;)</span><br><span class="line">    pushReturn = json.loads(pushPost.text)</span><br><span class="line">    messageID = pushReturn[<span class="string">&quot;result&quot;</span>][<span class="string">&quot;message_id&quot;</span>]</span><br><span class="line">    sleepSometime = Timer(DELETETIME, deleteMessage, [messageID])</span><br><span class="line">    sleepSometime.start()</span><br><span class="line"></span><br><span class="line"><span class="comment"># 保存推送内容</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">saveLog</span>(<span class="params">minerData</span>):</span><br><span class="line">    timeNowStr = time.strftime(ISOTIMEFORMAT, time.localtime(time.time()))</span><br><span class="line">    <span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">&#x27;/var/log/push/log&#x27;</span>, <span class="string">&#x27;a+&#x27;</span>) <span class="keyword">as</span> getLog:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;现在是UTC时间:&quot;</span>, timeNowStr, minerData, LINE, file=getLog)</span><br><span class="line">    <span class="keyword">return</span> timeNowStr</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">main</span>():</span><br><span class="line">    judge, minerData = getData()</span><br><span class="line">    timeNowStr = saveLog(minerData)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> judge:</span><br><span class="line">        postMessages(minerData)</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        <span class="built_in">print</span>(timeNowStr, <span class="string">&quot;我安澜，从不掉线&quot;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    main()</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>ethminer_push.service:</strong></p><figure class="highlight profile"><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><span class="line">[Unit]</span><br><span class="line">Description=ethminer push service</span><br><span class="line">Wants=ethminer_push.timer</span><br><span class="line"></span><br><span class="line">[Service]</span><br><span class="line">Type=oneshot</span><br><span class="line">ExecStart=/bin/python &quot;/usr/local/bin/push.py&quot;</span><br></pre></td></tr></table></figure><p><strong>ethminer_push.timer:</strong></p><figure class="highlight profile"><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><span class="line">[Unit]</span><br><span class="line">Description=every <span class="number">10</span> minute to get</span><br><span class="line"></span><br><span class="line">[Timer]</span><br><span class="line">OnBootSec=<span class="number">5</span>min</span><br><span class="line">OnCalendar=*:<span class="number">0</span>/<span class="number">5</span></span><br><span class="line">Persistent=true</span><br><span class="line"></span><br><span class="line">[Install]</span><br><span class="line">WantedBy=timers.target</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;起因&quot;&gt;&lt;a href=&quot;#起因&quot; class=&quot;headerlink&quot; title=&quot;起因&quot;&gt;&lt;/a&gt;起因&lt;/h2&gt;&lt;p&gt;公司有百来台矿机接入 f2pool 在挖 eth ，但是系统用的并不是我们自己的，也没办法提供权限，所以监控上有一点麻烦。还好 f2pool</summary>
      
    
    
    
    <category term="运维" scheme="https://blog.jugg.xyz/categories/%E8%BF%90%E7%BB%B4/"/>
    
    
    <category term="Python" scheme="https://blog.jugg.xyz/tags/Python/"/>
    
  </entry>
  
  <entry>
    <title>关于 Limits 的一些常识</title>
    <link href="https://blog.jugg.xyz/2019/09/22/ops/Common-Sense-About-The-Limits/"/>
    <id>https://blog.jugg.xyz/2019/09/22/ops/Common-Sense-About-The-Limits/</id>
    <published>2019-09-22T17:09:42.000Z</published>
    <updated>2026-05-02T11:24:14.333Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>最近部署的一个项目，启动十几个小时后就报错了，看了下日志输出是 <code>Too many open files</code> 。放到搜索引擎查了下，了解到是系统默认进程最大打开文件描述符数太小导致的，所以解决问题后顺便来记录一下。<del>（其实是为了水一篇博客）</del></p><h2 id="关于用户的限制"><a href="#关于用户的限制" class="headerlink" title="关于用户的限制"></a>关于用户的限制</h2><p><code>/etc/security/limits.conf</code> 文件是用来限制用户的资源使用，防止系统被fork炸弹占用所有资源的有效方法，具体配置信息可以在 <a href="https://jlk.fjfi.cvut.cz/arch/manpages/man/limits.conf.5">limit.conf (Arch manual pages)</a> 里详细了解，可配置项很多。我们日常要用到的主要是其中的两项：<code>nproc</code> 和 <code>nofile</code>。</p><p><code>nproc</code> 是用户最大可打开进程数。用 <code># ps -u &lt;user&gt;|grep -v PID|wc -l</code> 可以查看用户当然打开进程数。（ps，grep，wc这三个进程也包括在内）下面提供两个<code>nproc</code>的修改示例。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">*           hard    nproc      2048  # *匹配所有用户；%可以匹配组</span><br><span class="line">root        hard    nproc      65536 # hard是硬限制，是系统不允许用户启动进程超过的上限；soft是软限制，是由用户限制进程不得超过的上限</span><br></pre></td></tr></table></figure><p><code>nofile</code> 是进程最大可打开文件描述符数。用<code>ulimit -n</code>可以查看当前用户的限制，用 <code>$ cat /proc/&lt;PID&gt;/limits</code> 可以该 PID 的所有资源限制。下面提供两个<code>nofile</code>的修改示例。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">*           hard    nofile     8192 </span><br><span class="line">root        hard    nofile     unlimited # unlimited是不做限制</span><br></pre></td></tr></table></figure><h2 id="关于系统的限制"><a href="#关于系统的限制" class="headerlink" title="关于系统的限制"></a>关于系统的限制</h2><p>系统最大可打开的文件描述符数通过 <code>$ cat /proc/sys/fs/file-max</code> 查看，当前系统打开文件描述符总数可以通过 <code># cat /proc/sys/fs/file-nr</code> 查看，其中第一个数表示当前系统正在使用的文件描述符数，第二个数为目前不再使用的，第三个数则等于<code>/proc/sys/fs/file-max</code>。而如果要修改系统文件描述符限制则需要更改 <code>/etc/sysctl.conf</code> 文件，例如：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash"><span class="built_in">echo</span> <span class="string">&quot;fs.file-max = 1000000&quot;</span> &gt;&gt; /etc/sysctl.conf</span></span><br></pre></td></tr></table></figure><p>另外，file-max是限制系统内核可分配的最大文件数，而单个进程最大可分配的文件数则是用 <code>$ cat /proc/sys/fs/nr_open</code> 查看，修改方式则较为类似：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash"><span class="built_in">echo</span> <span class="string">&quot;fs.nr_open = 1000000&quot;</span> &gt;&gt; /etc/sysctl.conf <span class="comment"># nr_open不可大于file-max</span></span></span><br></pre></td></tr></table></figure><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>简单的介绍了一下关于设备资源的限制的常识，可以发现其中很多都会牵扯到 <code>/proc</code> 这个目录，这是一个让你能和内核内部数据结构进行交互，获取相关进程有用信息的目录，也是一个独立的文件系统，下次我们就来聊聊 <code>/proc</code> 吧。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;最近部署的一个项目，启动十几个小时后就报错了，看了下日志输出是 &lt;code&gt;Too many open files&lt;/code&gt; 。放到搜索</summary>
      
    
    
    
    <category term="运维" scheme="https://blog.jugg.xyz/categories/%E8%BF%90%E7%BB%B4/"/>
    
    
    <category term="Linux" scheme="https://blog.jugg.xyz/tags/Linux/"/>
    
    <category term="Limits" scheme="https://blog.jugg.xyz/tags/Limits/"/>
    
  </entry>
  
  <entry>
    <title>命令行的艺术</title>
    <link href="https://blog.jugg.xyz/2019/05/30/repost/the-art-of-command-line/"/>
    <id>https://blog.jugg.xyz/2019/05/30/repost/the-art-of-command-line/</id>
    <published>2019-05-30T18:58:47.000Z</published>
    <updated>2026-05-02T11:24:14.335Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文转载自 <a href="https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md">the-art-of-command-line</a> </p></blockquote><p>[<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://gitter.im/jlevy/the-art-of-command-line?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge" alt="Join the chat at https://gitter.im/jlevy/the-art-of-command-line](https://badges.gitter.im/Join%20Chat.svg)" title="">                </div>                <div class="image-caption">Join the chat at https://gitter.im/jlevy/the-art-of-command-line](https://badges.gitter.im/Join%20Chat.svg)</div>            </figure></p><ul><li><a href="#%E5%89%8D%E8%A8%80">前言</a></li><li><a href="#%E5%9F%BA%E7%A1%80">基础</a></li><li><a href="#%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8">日常使用</a></li><li><a href="#%E6%96%87%E4%BB%B6%E5%8F%8A%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86">文件及数据处理</a></li><li><a href="#%E7%B3%BB%E7%BB%9F%E8%B0%83%E8%AF%95">系统调试</a></li><li><a href="#%E5%8D%95%E8%A1%8C%E8%84%9A%E6%9C%AC">单行脚本</a></li><li><a href="#%E5%86%B7%E9%97%A8%E4%BD%86%E6%9C%89%E7%94%A8">冷门但有用</a></li><li><a href="#%E4%BB%85%E9%99%90-os-x-%E7%B3%BB%E7%BB%9F">仅限 OS X 系统</a></li><li><a href="#%E4%BB%85%E9%99%90-windows-%E7%B3%BB%E7%BB%9F">仅限 Windows 系统</a><ul><li><a href="#%E5%9C%A8-winodws-%E4%B8%8B%E8%8E%B7%E5%8F%96-unix-%E5%B7%A5%E5%85%B7">在 Winodws 下获取 Unix 工具</a></li><li><a href="#%E5%AE%9E%E7%94%A8-windows-%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7">实用 Windows 命令行工具</a></li><li><a href="#cygwin-%E6%8A%80%E5%B7%A7">Cygwin 技巧</a></li></ul></li><li><a href="#%E6%9B%B4%E5%A4%9A%E8%B5%84%E6%BA%90">更多资源</a></li><li><a href="#%E5%85%8D%E8%B4%A3%E5%A3%B0%E6%98%8E">免责声明</a></li><li><a href="#%E6%8E%88%E6%9D%83%E6%9D%A1%E6%AC%BE">授权条款</a></li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/cowsay.png" alt="cowsay" title="">                </div>                <div class="image-caption">cowsay</div>            </figure><p>熟练使用命令行是一种常常被忽视，或被认为难以掌握的技能，但实际上，它会提高你作为工程师的灵活性以及生产力。本文是一份我在 Linux 上工作时，发现的一些命令行使用技巧的摘要。有些技巧非常基础，而另一些则相当复杂，甚至晦涩难懂。这篇文章并不长，但当你能够熟练掌握这里列出的所有技巧时，你就学会了很多关于命令行的东西了。</p><p>这篇文章是<a href="AUTHORS.md">许多作者和译者</a>共同的成果。<br>这里的部分内容<a href="http://www.quora.com/What-are-some-lesser-known-but-useful-Unix-commands">首次</a><a href="http://www.quora.com/What-are-the-most-useful-Swiss-army-knife-one-liners-on-Unix">出现</a>于 <a href="http://www.quora.com/What-are-some-time-saving-tips-that-every-Linux-user-should-know">Quora</a>，但已经迁移到了 Github，并由众多高手做出了许多改进。如果你在本文中发现了错误或者存在可以改善的地方，请<a href="https://github.com/jlevy/the-art-of-command-line/blob/master/CONTRIBUTING.md"><strong>贡献你的一份力量</strong></a>。</p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>涵盖范围：</p><ul><li>这篇文章不仅能帮助刚接触命令行的新手，而且对具有经验的人也大有裨益。本文致力于做到<em>覆盖面广</em>（涉及所有重要的内容），<em>具体</em>（给出具体的最常用的例子），以及<em>简洁</em>（避免冗余的内容，或是可以在其他地方轻松查到的细枝末节）。在特定应用场景下，本文的内容属于基本功或者能帮助您节约大量的时间。</li><li>本文主要为 Linux 所写，但在<a href="#%E4%BB%85%E9%99%90-os-x-%E7%B3%BB%E7%BB%9F">仅限 OS X 系统</a>章节和<a href="#%E4%BB%85%E9%99%90-windows-%E7%B3%BB%E7%BB%9F">仅限 Windows 系统</a>章节中也包含有对应操作系统的内容。除去这两个章节外，其它的内容大部分均可在其他类 Unix 系统或 OS X，甚至 Cygwin 中得到应用。</li><li>本文主要关注于交互式 Bash，但也有很多技巧可以应用于其他 shell 和 Bash 脚本当中。</li><li>除去“标准的”Unix 命令，本文还包括了一些依赖于特定软件包的命令（前提是它们具有足够的价值）。</li></ul><p>注意事项：</p><ul><li>为了能在一页内展示尽量多的东西，一些具体的信息可以在引用的页面中找到。我们相信机智的你知道如何使用 Google 或者其他搜索引擎来查阅到更多的详细信息。文中部分命令需要您使用 <code>apt-get</code>，<code>yum</code>，<code>dnf</code>，<code>pacman</code>，<br><code>pip</code> 或 <code>brew</code>（以及其它合适的包管理器）来安装依赖的程序。</li><li>遇到问题的话，请尝试使用 <a href="http://explainshell.com/">Explainshell</a> 去获取相关命令、参数、管道等内容的解释。</li></ul><h2 id="基础"><a href="#基础" class="headerlink" title="基础"></a>基础</h2><ul><li><p>学习 Bash 的基础知识。具体地，在命令行中输入 <code>man bash</code> 并至少全文浏览一遍; 它理解起来很简单并且不冗长。其他的 shell 可能很好用，但 Bash 的功能已经足够强大并且到几乎总是可用的（ 如果你<em>只</em>学习 zsh，fish 或其他的 shell 的话，在你自己的设备上会显得很方便，但过度依赖这些功能会给您带来不便，例如当你需要在服务器上工作时）。</p></li><li><p>熟悉至少一个基于文本的编辑器。通常而言 Vim （<code>vi</code>） 会是你最好的选择，毕竟在终端中编辑文本时 Vim 是最好用的工具（甚至大部分情况下 Vim 要比 Emacs、大型 IDE 或是炫酷的编辑器更好用）。</p></li><li><p>学会如何使用 <code>man</code> 命令去阅读文档。学会使用 <code>apropos</code> 去查找文档。知道有些命令并不对应可执行文件，而是在 Bash 内置好的，此时可以使用 <code>help</code> 和 <code>help -d</code> 命令获取帮助信息。你可以用 <code>type 命令</code> 来判断这个命令到底是可执行文件、shell 内置命令还是别名。</p></li><li><p>学会使用 <code>&gt;</code> 和 <code>&lt;</code> 来重定向输出和输入，学会使用 <code>|</code> 来重定向管道。明白 <code>&gt;</code> 会覆盖了输出文件而 <code>&gt;&gt;</code> 是在文件末添加。了解标准输出 stdout 和标准错误 stderr。</p></li><li><p>学会使用通配符 <code>*</code> （或许再算上 <code>?</code> 和 <code>[</code>…<code>]</code>） 和引用以及引用中 <code>&#39;</code> 和 <code>&quot;</code> 的区别（后文中有一些具体的例子）。</p></li><li><p>熟悉 Bash 中的任务管理工具：<code>&amp;</code>，<strong>ctrl-z</strong>，<strong>ctrl-c</strong>，<code>jobs</code>，<code>fg</code>，<code>bg</code>，<code>kill</code> 等。</p></li><li><p>学会使用 <code>ssh</code> 进行远程命令行登录，最好知道如何使用 <code>ssh-agent</code>，<code>ssh-add</code> 等命令来实现基础的无密码认证登录。</p></li><li><p>学会基本的文件管理工具：<code>ls</code> 和 <code>ls -l</code> （了解 <code>ls -l</code> 中每一列代表的意义），<code>less</code>，<code>head</code>，<code>tail</code> 和 <code>tail -f</code> （甚至 <code>less +F</code>），<code>ln</code> 和 <code>ln -s</code> （了解硬链接与软链接的区别），<code>chown</code>，<code>chmod</code>，<code>du</code> （硬盘使用情况概述：<code>du -hs *</code>）。 关于文件系统的管理，学习 <code>df</code>，<code>mount</code>，<code>fdisk</code>，<code>mkfs</code>，<code>lsblk</code>。知道 inode 是什么（与 <code>ls -i</code> 和 <code>df -i</code> 等命令相关）。</p></li><li><p>学习基本的网络管理工具：<code>ip</code> 或 <code>ifconfig</code>，<code>dig</code>。</p></li><li><p>学习并使用一种版本控制管理系统，例如 <code>git</code>。</p></li><li><p>熟悉正则表达式，学会使用 <code>grep</code>／<code>egrep</code>，它们的参数中 <code>-i</code>，<code>-o</code>，<code>-v</code>，<code>-A</code>，<code>-B</code> 和 <code>-C</code> 这些是很常用并值得认真学习的。</p></li><li><p>学会使用 <code>apt-get</code>，<code>yum</code>，<code>dnf</code> 或 <code>pacman</code> （具体使用哪个取决于你使用的 Linux 发行版）来查找和安装软件包。并确保你的环境中有 <code>pip</code> 来安装基于 Python 的命令行工具 （接下来提到的部分程序使用 <code>pip</code> 来安装会很方便）。</p></li></ul><h2 id="日常使用"><a href="#日常使用" class="headerlink" title="日常使用"></a>日常使用</h2><ul><li><p>在 Bash 中，可以通过按 <strong>Tab</strong> 键实现自动补全参数，使用 <strong>ctrl-r</strong> 搜索命令行历史记录（按下按键之后，输入关键字便可以搜索，重复按下 <strong>ctrl-r</strong> 会向后查找匹配项，按下 <strong>Enter</strong> 键会执行当前匹配的命令，而按下右方向键会将匹配项放入当前行中，不会直接执行，以便做出修改）。</p></li><li><p>在 Bash 中，可以按下 <strong>ctrl-w</strong> 删除你键入的最后一个单词，<strong>ctrl-u</strong> 可以删除行内光标所在位置之前的内容，<strong>alt-b</strong> 和 <strong>alt-f</strong> 可以以单词为单位移动光标，<strong>ctrl-a</strong> 可以将光标移至行首，<strong>ctrl-e</strong> 可以将光标移至行尾，<strong>ctrl-k</strong> 可以删除光标至行尾的所有内容，<strong>ctrl-l</strong> 可以清屏。键入 <code>man readline</code> 可以查看 Bash 中的默认快捷键。内容有很多，例如 <strong>alt-.</strong> 循环地移向前一个参数，而 <strong>alt-</strong>* 可以展开通配符。</p></li><li><p>你喜欢的话，可以执行 <code>set -o vi</code> 来使用 vi 风格的快捷键，而执行 <code>set -o emacs</code> 可以把它改回来。</p></li><li><p>为了便于编辑长命令，在设置你的默认编辑器后（例如 <code>export EDITOR=vim</code>），<strong>ctrl-x</strong> <strong>ctrl-e</strong> 会打开一个编辑器来编辑当前输入的命令。在 vi 风格下快捷键则是 <strong>escape-v</strong>。</p></li><li><p>键入 <code>history</code> 查看命令行历史记录，再用 <code>!n</code>（<code>n</code> 是命令编号）就可以再次执行。其中有许多缩写，最有用的大概就是 <code>!$</code>， 它用于指代上次键入的参数，而 <code>!!</code> 可以指代上次键入的命令了（参考 man 页面中的“HISTORY EXPANSION”）。不过这些功能，你也可以通过快捷键 <strong>ctrl-r</strong> 和 <strong>alt-.</strong> 来实现。</p></li><li><p><code>cd</code> 命令可以切换工作路径，输入 <code>cd ~</code> 可以进入 home 目录。要访问你的 home 目录中的文件，可以使用前缀 <code>~</code>（例如 <code>~/.bashrc</code>）。在 <code>sh</code> 脚本里则用环境变量 <code>$HOME</code> 指代 home 目录的路径。</p></li><li><p>回到前一个工作路径：<code>cd -</code>。</p></li><li><p>如果你输入命令的时候中途改了主意，按下 <strong>alt-#</strong> 在行首添加 <code>#</code> 把它当做注释再按下回车执行（或者依次按下 <strong>ctrl-a</strong>， <strong>#</strong>， <strong>enter</strong>）。这样做的话，之后借助命令行历史记录，你可以很方便恢复你刚才输入到一半的命令。</p></li><li><p>使用 <code>xargs</code> （ 或 <code>parallel</code>）。他们非常给力。注意到你可以控制每行参数个数（<code>-L</code>）和最大并行数（<code>-P</code>）。如果你不确定它们是否会按你想的那样工作，先使用 <code>xargs echo</code> 查看一下。此外，使用 <code>-I&#123;&#125;</code> 会很方便。例如：</p></li></ul><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><span class="line">find . -name <span class="string">&#x27;*.py&#x27;</span> | xargs grep some_function</span><br><span class="line"><span class="built_in">cat</span> hosts | xargs -I&#123;&#125; ssh root@&#123;&#125; hostname</span><br></pre></td></tr></table></figure><ul><li><p><code>pstree -p</code> 以一种优雅的方式展示进程树。</p></li><li><p>使用 <code>pgrep</code> 和 <code>pkill</code> 根据名字查找进程或发送信号（<code>-f</code> 参数通常有用）。</p></li><li><p>了解你可以发往进程的信号的种类。比如，使用 <code>kill -STOP [pid]</code> 停止一个进程。使用 <code>man 7 signal</code> 查看详细列表。</p></li><li><p>使用 <code>nohup</code> 或 <code>disown</code> 使一个后台进程持续运行。</p></li><li><p>使用 <code>netstat -lntp</code> 或 <code>ss -plat</code> 检查哪些进程在监听端口（默认是检查 TCP 端口; 添加参数 <code>-u</code> 则检查 UDP 端口）或者 <code>lsof -iTCP -sTCP:LISTEN -P -n</code> (这也可以在 OS X 上运行)。</p></li><li><p><code>lsof</code> 来查看开启的套接字和文件。</p></li><li><p>使用 <code>uptime</code> 或 <code>w</code> 来查看系统已经运行多长时间。</p></li><li><p>使用 <code>alias</code> 来创建常用命令的快捷形式。例如：<code>alias ll=&#39;ls -latr&#39;</code> 创建了一个新的命令别名 <code>ll</code>。</p></li><li><p>可以把别名、shell 选项和常用函数保存在 <code>~/.bashrc</code>，具体看下这篇<a href="http://superuser.com/a/183980/7106">文章</a>。这样做的话你就可以在所有 shell 会话中使用你的设定。</p></li><li><p>把环境变量的设定以及登陆时要执行的命令保存在 <code>~/.bash_profile</code>。而对于从图形界面启动的 shell 和 <code>cron</code> 启动的 shell，则需要单独配置文件。</p></li><li><p>要想在几台电脑中同步你的配置文件（例如 <code>.bashrc</code> 和 <code>.bash_profile</code>），可以借助 Git。</p></li><li><p>当变量和文件名中包含空格的时候要格外小心。Bash 变量要用引号括起来，比如 <code>&quot;$FOO&quot;</code>。尽量使用 <code>-0</code> 或 <code>-print0</code> 选项以便用 NULL 来分隔文件名，例如 <code>locate -0 pattern | xargs -0 ls -al</code> 或 <code>find / -print0 -type d | xargs -0 ls -al</code>。如果 for 循环中循环访问的文件名含有空字符（空格、tab 等字符），只需用 <code>IFS=$&#39;\n&#39;</code> 把内部字段分隔符设为换行符。</p></li><li><p>在 Bash 脚本中，使用 <code>set -x</code> 去调试输出（或者使用它的变体 <code>set -v</code>，它会记录原始输入，包括多余的参数和注释）。尽可能地使用严格模式：使用 <code>set -e</code> 令脚本在发生错误时退出而不是继续运行；使用 <code>set -u</code> 来检查是否使用了未赋值的变量；试试 <code>set -o pipefail</code>，它可以监测管道中的错误。当牵扯到很多脚本时，使用 <code>trap</code> 来检测 ERR 和 EXIT。一个好的习惯是在脚本文件开头这样写，这会使它能够检测一些错误，并在错误发生时中断程序并输出信息：</p></li></ul><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><span class="line"><span class="built_in">set</span> -euo pipefail</span><br><span class="line"><span class="built_in">trap</span> <span class="string">&quot;echo &#x27;error: Script failed: see failed command above&#x27;&quot;</span> ERR</span><br></pre></td></tr></table></figure><ul><li>在 Bash 脚本中，子 shell（使用括号 <code>(...)</code>）是一种组织参数的便捷方式。一个常见的例子是临时地移动工作路径，代码如下：</li></ul><figure class="highlight bash"><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><span class="line"><span class="comment"># do something in current dir</span></span><br><span class="line">(<span class="built_in">cd</span> /some/other/dir &amp;&amp; other-command)</span><br><span class="line"><span class="comment"># continue in original dir</span></span><br></pre></td></tr></table></figure><ul><li><p>在 Bash 中，变量有许多的扩展方式。<code>$&#123;name:?error message&#125;</code> 用于检查变量是否存在。此外，当 Bash 脚本只需要一个参数时，可以使用这样的代码 <code>input_file=$&#123;1:?usage: $0 input_file&#125;</code>。在变量为空时使用默认值：<code>$&#123;name:-default&#125;</code>。如果你要在之前的例子中再加一个（可选的）参数，可以使用类似这样的代码 <code>output_file=$&#123;2:-logfile&#125;</code>，如果省略了 $2，它的值就为空，于是 <code>output_file</code> 就会被设为 <code>logfile</code>。数学表达式：<code>i=$(( (i + 1) % 5 ))</code>。序列：<code>&#123;1..10&#125;</code>。截断字符串：<code>$&#123;var%suffix&#125;</code> 和 <code>$&#123;var#prefix&#125;</code>。例如，假设 <code>var=foo.pdf</code>，那么 <code>echo $&#123;var%.pdf&#125;.txt</code> 将输出 <code>foo.txt</code>。</p></li><li><p>使用括号扩展（<code>&#123;</code>…<code>&#125;</code>）来减少输入相似文本，并自动化文本组合。这在某些情况下会很有用，例如 <code>mv foo.&#123;txt,pdf&#125; some-dir</code>（同时移动两个文件），<code>cp somefile&#123;,.bak&#125;</code>（会被扩展成 <code>cp somefile somefile.bak</code>）或者 <code>mkdir -p test-&#123;a,b,c&#125;/subtest-&#123;1,2,3&#125;</code>（会被扩展成所有可能的组合，并创建一个目录树）。</p></li><li><p>通过使用 <code>&lt;(some command)</code> 可以将输出视为文件。例如，对比本地文件 <code>/etc/hosts</code> 和一个远程文件：</p></li></ul><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">diff /etc/hosts &lt;(ssh somehost <span class="built_in">cat</span> /etc/hosts)</span><br></pre></td></tr></table></figure><ul><li>编写脚本时，你可能会想要把代码都放在大括号里。缺少右括号的话，代码就会因为语法错误而无法执行。如果你的脚本是要放在网上分享供他人使用的，这样的写法就体现出它的好处了，因为这样可以防止下载不完全代码被执行。</li></ul><figure class="highlight bash"><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><span class="line">&#123;</span><br><span class="line">      <span class="comment"># 在这里写代码</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>了解 Bash 中的“here documents”，例如 <code>cat &lt;&lt;EOF ...</code>。</p></li><li><p>在 Bash 中，同时重定向标准输出和标准错误：<code>some-command &gt;logfile 2&gt;&amp;1</code> 或者 <code>some-command &amp;&gt;logfile</code>。通常，为了保证命令不会在标准输入里残留一个未关闭的文件句柄捆绑在你当前所在的终端上，在命令后添加 <code>&lt;/dev/null</code> 是一个好习惯。</p></li><li><p>使用 <code>man ascii</code> 查看具有十六进制和十进制值的ASCII表。<code>man unicode</code>，<code>man utf-8</code>，以及 <code>man latin1</code> 有助于你去了解通用的编码信息。</p></li><li><p>使用 <code>screen</code> 或 <a href="https://tmux.github.io/"><code>tmux</code></a> 来使用多份屏幕，当你在使用 ssh 时（保存 session 信息）将尤为有用。而 <code>byobu</code> 可以为它们提供更多的信息和易用的管理工具。另一个轻量级的 session 持久化解决方案是 <a href="https://github.com/bogner/dtach"><code>dtach</code></a>。</p></li><li><p>ssh 中，了解如何使用 <code>-L</code> 或 <code>-D</code>（偶尔需要用 <code>-R</code>）开启隧道是非常有用的，比如当你需要从一台远程服务器上访问 web 页面。</p></li><li><p>对 ssh 设置做一些小优化可能是很有用的，例如这个 <code>~/.ssh/config</code> 文件包含了防止特定网络环境下连接断开、压缩数据、多通道等选项：</p></li></ul><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><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><span class="line">TCPKeepAlive=yes</span><br><span class="line">ServerAliveInterval=15</span><br><span class="line">ServerAliveCountMax=6</span><br><span class="line">Compression=yes</span><br><span class="line">ControlMaster auto</span><br><span class="line">ControlPath /tmp/%r@%h:%p</span><br><span class="line">ControlPersist yes</span><br></pre></td></tr></table></figure><ul><li><p>一些其他的关于 ssh 的选项是与安全相关的，应当小心翼翼的使用。例如你应当只能在可信任的网络中启用 <code>StrictHostKeyChecking=no</code>，<code>ForwardAgent=yes</code>。</p></li><li><p>考虑使用 <a href="https://mosh.mit.edu/"><code>mosh</code></a> 作为 ssh 的替代品，它使用 UDP 协议。它可以避免连接被中断并且对带宽需求更小，但它需要在服务端做相应的配置。</p></li><li><p>获取八进制形式的文件访问权限（修改系统设置时通常需要，但 <code>ls</code> 的功能不那么好用并且通常会搞砸），可以使用类似如下的代码：</p></li></ul><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">stat</span> -c <span class="string">&#x27;%A %a %n&#x27;</span> /etc/timezone</span><br></pre></td></tr></table></figure><ul><li><p>使用 <a href="https://github.com/mooz/percol"><code>percol</code></a> 或者 <a href="https://github.com/junegunn/fzf"><code>fzf</code></a> 可以交互式地从另一个命令输出中选取值。</p></li><li><p>使用 <code>fpp</code>（<a href="https://github.com/facebook/PathPicker">PathPicker</a>）可以与基于另一个命令(例如 <code>git</code>）输出的文件交互。</p></li><li><p>将 web 服务器上当前目录下所有的文件（以及子目录）暴露给你所处网络的所有用户，使用：<br><code>python -m SimpleHTTPServer 7777</code> （使用端口 7777 和 Python 2）或<code>python -m http.server 7777</code> （使用端口 7777 和 Python 3）。</p></li><li><p>以其他用户的身份执行命令，使用 <code>sudo</code>。默认以 root 用户的身份执行；使用 <code>-u</code> 来指定其他用户。使用 <code>-i</code> 来以该用户登录（需要输入_你自己的_密码）。</p></li><li><p>将 shell 切换为其他用户，使用 <code>su username</code> 或者 <code>sudo - username</code>。加入 <code>-</code> 会使得切换后的环境与使用该用户登录后的环境相同。省略用户名则默认为 root。切换到哪个用户，就需要输入_哪个用户的_密码。</p></li><li><p>了解命令行的 <a href="https://wiki.debian.org/CommonErrorMessages/ArgumentListTooLong">128K 限制</a>。使用通配符匹配大量文件名时，常会遇到“Argument list too long”的错误信息。（这种情况下换用 <code>find</code> 或 <code>xargs</code> 通常可以解决。）</p></li><li><p>当你需要一个基本的计算器时，可以使用 <code>python</code> 解释器（当然你要用 python 的时候也是这样）。例如：</p></li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; 2+3</span><br><span class="line">5</span><br></pre></td></tr></table></figure><h2 id="文件及数据处理"><a href="#文件及数据处理" class="headerlink" title="文件及数据处理"></a>文件及数据处理</h2><ul><li><p>在当前目录下通过文件名查找一个文件，使用类似于这样的命令：<code>find . -iname &#39;*something*&#39;</code>。在所有路径下通过文件名查找文件，使用 <code>locate something</code> （但注意到 <code>updatedb</code> 可能没有对最近新建的文件建立索引，所以你可能无法定位到这些未被索引的文件）。</p></li><li><p>使用 <a href="https://github.com/ggreer/the_silver_searcher"><code>ag</code></a> 在源代码或数据文件里检索（<code>grep -r</code> 同样可以做到，但相比之下 <code>ag</code> 更加先进）。</p></li><li><p>将 HTML 转为文本：<code>lynx -dump -stdin</code>。</p></li><li><p>Markdown，HTML，以及所有文档格式之间的转换，试试 <a href="http://pandoc.org/"><code>pandoc</code></a>。</p></li><li><p>当你要处理棘手的 XML 时候，<code>xmlstarlet</code> 算是上古时代流传下来的神器。</p></li><li><p>使用 <a href="http://stedolan.github.io/jq/"><code>jq</code></a> 处理 JSON。</p></li><li><p>使用 <a href="https://github.com/0k/shyaml"><code>shyaml</code></a> 处理 YAML。</p></li><li><p>要处理 Excel 或 CSV 文件的话，<a href="https://github.com/onyxfish/csvkit">csvkit</a> 提供了 <code>in2csv</code>，<code>csvcut</code>，<code>csvjoin</code>，<code>csvgrep</code> 等方便易用的工具。</p></li><li><p>当你要处理 Amazon S3 相关的工作的时候，<a href="https://github.com/s3tools/s3cmd"><code>s3cmd</code></a> 是一个很方便的工具而 <a href="https://github.com/bloomreach/s4cmd"><code>s4cmd</code></a> 的效率更高。Amazon 官方提供的 <a href="https://github.com/aws/aws-cli"><code>aws</code></a> 以及  <a href="https://github.com/donnemartin/saws"><code>saws</code></a> 是其他 AWS 相关工作的基础，值得学习。</p></li><li><p>了解如何使用 <code>sort</code> 和 <code>uniq</code>，包括 uniq 的 <code>-u</code> 参数和 <code>-d</code> 参数，具体内容在后文单行脚本节中。另外可以了解一下 <code>comm</code>。</p></li><li><p>了解如何使用 <code>cut</code>，<code>paste</code> 和 <code>join</code> 来更改文件。很多人都会使用 <code>cut</code>，但遗忘了 <code>join</code>。</p></li><li><p>了解如何运用 <code>wc</code> 去计算新行数（<code>-l</code>），字符数（<code>-m</code>），单词数（<code>-w</code>）以及字节数（<code>-c</code>）。</p></li><li><p>了解如何使用 <code>tee</code> 将标准输入复制到文件甚至标准输出，例如 <code>ls -al | tee file.txt</code>。</p></li><li><p>要进行一些复杂的计算，比如分组、逆序和一些其他的统计分析，可以考虑使用 <a href="https://www.gnu.org/software/datamash/"><code>datamash</code></a>。</p></li><li><p>注意到语言设置（中文或英文等）对许多命令行工具有一些微妙的影响，比如排序的顺序和性能。大多数 Linux 的安装过程会将 <code>LANG</code> 或其他有关的变量设置为符合本地的设置。要意识到当你改变语言设置时，排序的结果可能会改变。明白国际化可能会使 sort 或其他命令运行效率下降<em>许多倍</em>。某些情况下（例如集合运算）你可以放心的使用 <code>export LC_ALL=C</code> 来忽略掉国际化并按照字节来判断顺序。</p></li><li><p>你可以单独指定某一条命令的环境，只需在调用时把环境变量设定放在命令的前面，例如 <code>TZ=Pacific/Fiji date</code> 可以获取斐济的时间。</p></li><li><p>了解如何使用 <code>awk</code> 和 <code>sed</code> 来进行简单的数据处理。 参阅 <a href="#one-liners">One-liners</a> 获取示例。</p></li><li><p>替换一个或多个文件中出现的字符串：</p></li></ul><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">perl -pi.bak -e <span class="string">&#x27;s/old-string/new-string/g&#x27;</span> my-files-*.txt</span><br></pre></td></tr></table></figure><ul><li>使用 <a href="https://github.com/jlevy/repren"><code>repren</code></a> 来批量重命名文件，或是在多个文件中搜索替换内容。（有些时候 <code>rename</code> 命令也可以批量重命名，但要注意，它在不同 Linux 发行版中的功能并不完全一样。）</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 将文件、目录和内容全部重命名 foo -&gt; bar:</span></span><br><span class="line">repren --full --preserve-case --from foo --to bar .</span><br><span class="line"><span class="comment"># 还原所有备份文件 whatever.bak -&gt; whatever:</span></span><br><span class="line">repren --renames --from <span class="string">&#x27;(.*)\.bak&#x27;</span> --to <span class="string">&#x27;\1&#x27;</span> *.bak</span><br><span class="line"><span class="comment"># 用 rename 实现上述功能（若可用）:</span></span><br><span class="line">rename <span class="string">&#x27;s/\.bak$//&#x27;</span> *.bak</span><br></pre></td></tr></table></figure><ul><li>根据 man 页面的描述，<code>rsync</code> 是一个快速且非常灵活的文件复制工具。它闻名于设备之间的文件同步，但其实它在本地情况下也同样有用。在安全设置允许下，用 <code>rsync</code> 代替 <code>scp</code> 可以实现文件续传，而不用重新从头开始。它同时也是删除大量文件的<a href="https://web.archive.org/web/20130929001850/http://linuxnote.net/jianingy/en/linux/a-fast-way-to-remove-huge-number-of-files.html">最快方法</a>之一：</li></ul><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> empty &amp;&amp; rsync -r --delete empty/ some-dir &amp;&amp; <span class="built_in">rmdir</span> some-dir</span><br></pre></td></tr></table></figure><ul><li><p>若要在复制文件时获取当前进度，可使用 <code>pv</code>，<a href="https://github.com/dmerejkowsky/pycp"><code>pycp</code></a>，<a href="https://github.com/Xfennec/progress"><code>progress</code></a>，<code>rsync --progress</code>。若所执行的复制为block块拷贝，可以使用 <code>dd status=progress</code>。</p></li><li><p>使用 <code>shuf</code> 可以以行为单位来打乱文件的内容或从一个文件中随机选取多行。</p></li><li><p>了解 <code>sort</code> 的参数。显示数字时，使用 <code>-n</code> 或者 <code>-h</code> 来显示更易读的数（例如 <code>du -h</code> 的输出）。明白排序时关键字的工作原理（<code>-t</code> 和 <code>-k</code>）。例如，注意到你需要 <code>-k1，1</code> 来仅按第一个域来排序，而 <code>-k1</code> 意味着按整行排序。稳定排序（<code>sort -s</code>）在某些情况下很有用。例如，以第二个域为主关键字，第一个域为次关键字进行排序，你可以使用 <code>sort -k1，1 | sort -s -k2，2</code>。</p></li><li><p>如果你想在 Bash 命令行中写 tab 制表符，按下 <strong>ctrl-v</strong> <strong>[Tab]</strong> 或键入 <code>$&#39;\t&#39;</code> （后者可能更好，因为你可以复制粘贴它）。</p></li><li><p>标准的源代码对比及合并工具是 <code>diff</code> 和 <code>patch</code>。使用 <code>diffstat</code> 查看变更总览数据。注意到 <code>diff -r</code> 对整个文件夹有效。使用 <code>diff -r tree1 tree2 | diffstat</code> 查看变更的统计数据。<code>vimdiff</code> 用于比对并编辑文件。</p></li><li><p>对于二进制文件，使用 <code>hd</code>，<code>hexdump</code> 或者 <code>xxd</code> 使其以十六进制显示，使用 <code>bvi</code>，<code>hexedit</code> 或者 <code>biew</code> 来进行二进制编辑。</p></li><li><p>同样对于二进制文件，<code>strings</code>（包括 <code>grep</code> 等工具）可以帮助在二进制文件中查找特定比特。</p></li><li><p>制作二进制差分文件（Delta 压缩），使用 <code>xdelta3</code>。</p></li><li><p>使用 <code>iconv</code> 更改文本编码。需要更高级的功能，可以使用 <code>uconv</code>，它支持一些高级的 Unicode 功能。例如，这条命令移除了所有重音符号：</p></li></ul><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">uconv -f utf-8 -t utf-8 -x <span class="string">&#x27;::Any-Lower; ::Any-NFD; [:Nonspacing Mark:] &gt;; ::Any-NFC; &#x27;</span> &lt; input.txt &gt; output.txt</span><br></pre></td></tr></table></figure><ul><li><p>拆分文件可以使用 <code>split</code>（按大小拆分）和 <code>csplit</code>（按模式拆分）。</p></li><li><p>操作日期和时间表达式，可以用 <a href="http://www.fresse.org/dateutils/"><code>dateutils</code></a> 中的 <code>dateadd</code>、<code>datediff</code>、<code>strptime</code> 等工具。</p></li><li><p>使用 <code>zless</code>、<code>zmore</code>、<code>zcat</code> 和 <code>zgrep</code> 对压缩过的文件进行操作。</p></li><li><p>文件属性可以通过 <code>chattr</code> 进行设置，它比文件权限更加底层。例如，为了保护文件不被意外删除，可以使用不可修改标记：<code>sudo chattr +i /critical/directory/or/file</code></p></li><li><p>使用 <code>getfacl</code> 和 <code>setfacl</code> 以保存和恢复文件权限。例如：</p></li></ul><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><span class="line">getfacl -R /some/path &gt; permissions.txt</span><br><span class="line">setfacl --restore=permissions.txt</span><br></pre></td></tr></table></figure><ul><li>为了高效地创建空文件，请使用 <code>truncate</code>（创建<a href="https://zh.wikipedia.org/wiki/%E7%A8%80%E7%96%8F%E6%96%87%E4%BB%B6">稀疏文件</a>），<code>fallocate</code>（用于 ext4，xfs，btrf 和 ocfs2 文件系统），<code>xfs_mkfile</code>（适用于几乎所有的文件系统，包含在 xfsprogs 包中），<code>mkfile</code>（用于类 Unix 操作系统，比如 Solaris 和 Mac OS）。</li></ul><h2 id="系统调试"><a href="#系统调试" class="headerlink" title="系统调试"></a>系统调试</h2><ul><li><p><code>curl</code> 和 <code>curl -I</code> 可以被轻松地应用于 web 调试中，它们的好兄弟 <code>wget</code> 也是如此，或者也可以试试更潮的 <a href="https://github.com/jkbrzt/httpie"><code>httpie</code></a>。</p></li><li><p>获取 CPU 和硬盘的使用状态，通常使用使用 <code>top</code>（<code>htop</code> 更佳），<code>iostat</code> 和 <code>iotop</code>。而 <code>iostat -mxz 15</code> 可以让你获悉 CPU 和每个硬盘分区的基本信息和性能表现。</p></li><li><p>使用 <code>netstat</code> 和 <code>ss</code> 查看网络连接的细节。</p></li><li><p><code>dstat</code> 在你想要对系统的现状有一个粗略的认识时是非常有用的。然而若要对系统有一个深度的总体认识，使用 <a href="https://github.com/nicolargo/glances"><code>glances</code></a>，它会在一个终端窗口中向你提供一些系统级的数据。</p></li><li><p>若要了解内存状态，运行并理解 <code>free</code> 和 <code>vmstat</code> 的输出。值得留意的是“cached”的值，它指的是 Linux 内核用来作为文件缓存的内存大小，而与空闲内存无关。</p></li><li><p>Java 系统调试则是一件截然不同的事，一个可以用于 Oracle 的 JVM 或其他 JVM 上的调试的技巧是你可以运行 <code>kill -3 &lt;pid&gt;</code> 同时一个完整的栈轨迹和堆概述（包括 GC 的细节）会被保存到标准错误或是日志文件。JDK 中的 <code>jps</code>，<code>jstat</code>，<code>jstack</code>，<code>jmap</code> 很有用。<a href="https://github.com/aragozin/jvm-tools">SJK tools</a> 更高级。</p></li><li><p>使用 <a href="http://www.bitwizard.nl/mtr/"><code>mtr</code></a> 去跟踪路由，用于确定网络问题。</p></li><li><p>用 <a href="https://dev.yorhel.nl/ncdu"><code>ncdu</code></a> 来查看磁盘使用情况，它比寻常的命令，如 <code>du -sh *</code>，更节省时间。</p></li><li><p>查找正在使用带宽的套接字连接或进程，使用 <a href="http://www.ex-parrot.com/~pdw/iftop/"><code>iftop</code></a> 或 <a href="https://github.com/raboof/nethogs"><code>nethogs</code></a>。</p></li><li><p><code>ab</code> 工具（Apache 中自带）可以简单粗暴地检查 web 服务器的性能。对于更复杂的负载测试，使用 <code>siege</code>。</p></li><li><p><a href="https://wireshark.org/"><code>wireshark</code></a>，<a href="https://www.wireshark.org/docs/wsug_html_chunked/AppToolstshark.html"><code>tshark</code></a> 和 <a href="http://ngrep.sourceforge.net/"><code>ngrep</code></a> 可用于复杂的网络调试。</p></li><li><p>了解 <code>strace</code> 和 <code>ltrace</code>。这俩工具在你的程序运行失败、挂起甚至崩溃，而你却不知道为什么或你想对性能有个总体的认识的时候是非常有用的。注意 profile 参数（<code>-c</code>）和附加到一个运行的进程参数 （<code>-p</code>）。</p></li><li><p>了解使用 <code>ldd</code> 来检查共享库。但是<a href="http://www.catonmat.net/blog/ldd-arbitrary-code-execution/">永远不要在不信任的文件上运行</a>。</p></li><li><p>了解如何运用 <code>gdb</code> 连接到一个运行着的进程并获取它的堆栈轨迹。</p></li><li><p>学会使用 <code>/proc</code>。它在调试正在出现的问题的时候有时会效果惊人。比如：<code>/proc/cpuinfo</code>，<code>/proc/meminfo</code>，<code>/proc/cmdline</code>，<code>/proc/xxx/cwd</code>，<code>/proc/xxx/exe</code>，<code>/proc/xxx/fd/</code>，<code>/proc/xxx/smaps</code>（这里的 <code>xxx</code> 表示进程的 id 或 pid）。</p></li><li><p>当调试一些之前出现的问题的时候，<a href="http://sebastien.godard.pagesperso-orange.fr/"><code>sar</code></a> 非常有用。它展示了 cpu、内存以及网络等的历史数据。</p></li><li><p>关于更深层次的系统分析以及性能分析，看看 <code>stap</code>（<a href="https://sourceware.org/systemtap/wiki">SystemTap</a>），<a href="https://en.wikipedia.org/wiki/Perf_(Linux)"><code>perf</code></a>，以及<a href="https://github.com/draios/sysdig"><code>sysdig</code></a>。</p></li><li><p>查看你当前使用的系统，使用 <code>uname</code>，<code>uname -a</code>（Unix／kernel 信息）或者 <code>lsb_release -a</code>（Linux 发行版信息）。</p></li><li><p>无论什么东西工作得很欢乐（可能是硬件或驱动问题）时可以试试 <code>dmesg</code>。</p></li><li><p>如果你删除了一个文件，但通过 <code>du</code> 发现没有释放预期的磁盘空间，请检查文件是否被进程占用：<br><code>lsof | grep deleted | grep &quot;filename-of-my-big-file&quot;</code></p></li></ul><h2 id="单行脚本"><a href="#单行脚本" class="headerlink" title="单行脚本"></a>单行脚本</h2><p>一些命令组合的例子：</p><ul><li>当你需要对文本文件做集合交、并、差运算时，<code>sort</code> 和 <code>uniq</code> 会是你的好帮手。具体例子请参照代码后面的，此处假设 <code>a</code> 与 <code>b</code> 是两内容不同的文件。这种方式效率很高，并且在小文件和上 G 的文件上都能运用（注意尽管在 <code>/tmp</code> 在一个小的根分区上时你可能需要 <code>-T</code> 参数，但是实际上 <code>sort</code> 并不被内存大小约束），参阅前文中关于 <code>LC_ALL</code> 和 <code>sort</code> 的 <code>-u</code> 参数的部分。</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="built_in">sort</span> a b | <span class="built_in">uniq</span> &gt; c   <span class="comment"># c 是 a 并 b</span></span><br><span class="line"><span class="built_in">sort</span> a b | <span class="built_in">uniq</span> -d &gt; c   <span class="comment"># c 是 a 交 b</span></span><br><span class="line"><span class="built_in">sort</span> a b b | <span class="built_in">uniq</span> -u &gt; c   <span class="comment"># c 是 a - b</span></span><br></pre></td></tr></table></figure><ul><li><p>使用 <code>grep . *</code>（每行都会附上文件名）或者 <code>head -100 *</code>（每个文件有一个标题）来阅读检查目录下所有文件的内容。这在检查一个充满配置文件的目录（如 <code>/sys</code>、<code>/proc</code>、<code>/etc</code>）时特别好用。</p></li><li><p>计算文本文件第三列中所有数的和（可能比同等作用的 Python 代码快三倍且代码量少三倍）：</p></li></ul><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">&#x27;&#123; x += $3 &#125; END &#123; print x &#125;&#x27;</span> myfile</span><br></pre></td></tr></table></figure><ul><li>如果你想在文件树上查看大小&#x2F;日期，这可能看起来像递归版的 <code>ls -l</code> 但比 <code>ls -lR</code> 更易于理解：</li></ul><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">find . -<span class="built_in">type</span> f -<span class="built_in">ls</span></span><br></pre></td></tr></table></figure><ul><li>假设你有一个类似于 web 服务器日志文件的文本文件，并且一个确定的值只会出现在某些行上，假设一个 <code>acct_id</code> 参数在 URI 中。如果你想计算出每个 <code>acct_id</code> 值有多少次请求，使用如下代码：</li></ul><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">egrep -o <span class="string">&#x27;acct_id=[0-9]+&#x27;</span> access.log | <span class="built_in">cut</span> -d= -f2 | <span class="built_in">sort</span> | <span class="built_in">uniq</span> -c | <span class="built_in">sort</span> -rn</span><br></pre></td></tr></table></figure><ul><li><p>要持续监测文件改动，可以使用 <code>watch</code>，例如检查某个文件夹中文件的改变，可以用 <code>watch -d -n 2 &#39;ls -rtlh | tail&#39;</code>；或者在排查 WiFi 设置故障时要监测网络设置的更改，可以用 <code>watch -d -n 2 ifconfig</code>。</p></li><li><p>运行这个函数从这篇文档中随机获取一条技巧（解析 Markdown 文件并抽取项目）：</p></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="function"><span class="title">taocl</span></span>() &#123;</span><br><span class="line">  curl -s https://raw.githubusercontent.com/jlevy/the-art-of-command-line/master/README-zh.md|</span><br><span class="line">    pandoc -f markdown -t html |</span><br><span class="line">    iconv -f <span class="string">&#x27;utf-8&#x27;</span> -t <span class="string">&#x27;unicode&#x27;</span> |</span><br><span class="line">    xmlstarlet fo --html --dropdtd |</span><br><span class="line">    xmlstarlet sel -t -v <span class="string">&quot;(html/body/ul/li[count(p)&gt;0])[<span class="variable">$RANDOM</span> mod last()+1]&quot;</span> |</span><br><span class="line">    xmlstarlet unesc | <span class="built_in">fmt</span> -80</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="冷门但有用"><a href="#冷门但有用" class="headerlink" title="冷门但有用"></a>冷门但有用</h2><ul><li><p><code>expr</code>：计算表达式或正则匹配</p></li><li><p><code>m4</code>：简单的宏处理器</p></li><li><p><code>yes</code>：多次打印字符串</p></li><li><p><code>cal</code>：漂亮的日历</p></li><li><p><code>env</code>：执行一个命令（脚本文件中很有用）</p></li><li><p><code>printenv</code>：打印环境变量（调试时或在写脚本文件时很有用）</p></li><li><p><code>look</code>：查找以特定字符串开头的单词或行</p></li><li><p><code>cut</code>，<code>paste</code> 和 <code>join</code>：数据修改</p></li><li><p><code>fmt</code>：格式化文本段落</p></li><li><p><code>pr</code>：将文本格式化成页／列形式</p></li><li><p><code>fold</code>：包裹文本中的几行</p></li><li><p><code>column</code>：将文本格式化成多个对齐、定宽的列或表格</p></li><li><p><code>expand</code> 和 <code>unexpand</code>：制表符与空格之间转换</p></li><li><p><code>nl</code>：添加行号</p></li><li><p><code>seq</code>：打印数字</p></li><li><p><code>bc</code>：计算器</p></li><li><p><code>factor</code>：分解因数</p></li><li><p><a href="https://gnupg.org/"><code>gpg</code></a>：加密并签名文件</p></li><li><p><code>toe</code>：terminfo 入口列表</p></li><li><p><code>nc</code>：网络调试及数据传输</p></li><li><p><code>socat</code>：套接字代理，与 <code>netcat</code> 类似</p></li><li><p><a href="https://github.com/mattthias/slurm"><code>slurm</code></a>：网络流量可视化</p></li><li><p><code>dd</code>：文件或设备间传输数据</p></li><li><p><code>file</code>：确定文件类型</p></li><li><p><code>tree</code>：以树的形式显示路径和文件，类似于递归的 <code>ls</code></p></li><li><p><code>stat</code>：文件信息</p></li><li><p><code>time</code>：执行命令，并计算执行时间</p></li><li><p><code>timeout</code>：在指定时长范围内执行命令，并在规定时间结束后停止进程</p></li><li><p><code>lockfile</code>：使文件只能通过 <code>rm -f</code> 移除</p></li><li><p><code>logrotate</code>： 切换、压缩以及发送日志文件</p></li><li><p><code>watch</code>：重复运行同一个命令，展示结果并／或高亮有更改的部分</p></li><li><p><a href="https://github.com/joh/when-changed"><code>when-changed</code></a>：当检测到文件更改时执行指定命令。参阅 <code>inotifywait</code> 和 <code>entr</code>。</p></li><li><p><code>tac</code>：反向输出文件</p></li><li><p><code>shuf</code>：文件中随机选取几行</p></li><li><p><code>comm</code>：一行一行的比较排序过的文件</p></li><li><p><code>strings</code>：从二进制文件中抽取文本</p></li><li><p><code>tr</code>：转换字母</p></li><li><p><code>iconv</code> 或 <code>uconv</code>：文本编码转换</p></li><li><p><code>split</code> 和 <code>csplit</code>：分割文件</p></li><li><p><code>sponge</code>：在写入前读取所有输入，在读取文件后再向同一文件写入时比较有用，例如 <code>grep -v something some-file | sponge some-file</code></p></li><li><p><code>units</code>：将一种计量单位转换为另一种等效的计量单位（参阅 <code>/usr/share/units/definitions.units</code>）</p></li><li><p><code>apg</code>：随机生成密码</p></li><li><p><code>xz</code>：高比例的文件压缩</p></li><li><p><code>ldd</code>：动态库信息</p></li><li><p><code>nm</code>：提取 obj 文件中的符号</p></li><li><p><code>ab</code> 或 <a href="https://github.com/wg/wrk"><code>wrk</code></a>：web 服务器性能分析</p></li><li><p><code>strace</code>：调试系统调用</p></li><li><p><a href="http://www.bitwizard.nl/mtr/"><code>mtr</code></a>：更好的网络调试跟踪工具</p></li><li><p><code>cssh</code>：可视化的并发 shell</p></li><li><p><code>rsync</code>：通过 ssh 或本地文件系统同步文件和文件夹</p></li><li><p><a href="https://wireshark.org/"><code>wireshark</code></a> 和 <a href="https://www.wireshark.org/docs/wsug_html_chunked/AppToolstshark.html"><code>tshark</code></a>：抓包和网络调试工具</p></li><li><p><a href="http://ngrep.sourceforge.net/"><code>ngrep</code></a>：网络层的 grep</p></li><li><p><code>host</code> 和 <code>dig</code>：DNS 查找</p></li><li><p><code>lsof</code>：列出当前系统打开文件的工具以及查看端口信息</p></li><li><p><code>dstat</code>：系统状态查看</p></li><li><p><a href="https://github.com/nicolargo/glances"><code>glances</code></a>：高层次的多子系统总览</p></li><li><p><code>iostat</code>：硬盘使用状态</p></li><li><p><code>mpstat</code>： CPU 使用状态</p></li><li><p><code>vmstat</code>： 内存使用状态</p></li><li><p><code>htop</code>：top 的加强版</p></li><li><p><code>last</code>：登入记录</p></li><li><p><code>w</code>：查看处于登录状态的用户</p></li><li><p><code>id</code>：用户&#x2F;组 ID 信息</p></li><li><p><a href="http://sebastien.godard.pagesperso-orange.fr/"><code>sar</code></a>：系统历史数据</p></li><li><p><a href="http://www.ex-parrot.com/~pdw/iftop/"><code>iftop</code></a> 或 <a href="https://github.com/raboof/nethogs"><code>nethogs</code></a>：套接字及进程的网络利用情况</p></li><li><p><code>ss</code>：套接字数据</p></li><li><p><code>dmesg</code>：引导及系统错误信息</p></li><li><p><code>sysctl</code>： 在内核运行时动态地查看和修改内核的运行参数</p></li><li><p><code>hdparm</code>：SATA&#x2F;ATA 磁盘更改及性能分析</p></li><li><p><code>lsblk</code>：列出块设备信息：以树形展示你的磁盘以及磁盘分区信息</p></li><li><p><code>lshw</code>，<code>lscpu</code>，<code>lspci</code>，<code>lsusb</code> 和 <code>dmidecode</code>：查看硬件信息，包括 CPU、BIOS、RAID、显卡、USB设备等</p></li><li><p><code>lsmod</code> 和 <code>modinfo</code>：列出内核模块，并显示其细节</p></li><li><p><code>fortune</code>，<code>ddate</code> 和 <code>sl</code>：额，这主要取决于你是否认为蒸汽火车和莫名其妙的名人名言是否“有用”</p></li></ul><h2 id="仅限-OS-X-系统"><a href="#仅限-OS-X-系统" class="headerlink" title="仅限 OS X 系统"></a>仅限 OS X 系统</h2><p>以下是<em>仅限于</em> OS X 系统的技巧。</p><ul><li><p>用 <code>brew</code> （Homebrew）或者 <code>port</code> （MacPorts）进行包管理。这些可以用来在 OS X 系统上安装以上的大多数命令。</p></li><li><p>用 <code>pbcopy</code> 复制任何命令的输出到桌面应用，用 <code>pbpaste</code> 粘贴输入。</p></li><li><p>若要在 OS X 终端中将 Option 键视为 alt 键（例如在上面介绍的 <strong>alt-b</strong>、<strong>alt-f</strong> 等命令中用到），打开 偏好设置 -&gt; 描述文件 -&gt; 键盘 并勾选“使用 Option 键作为 Meta 键”。</p></li><li><p>用 <code>open</code> 或者 <code>open -a /Applications/Whatever.app</code> 使用桌面应用打开文件。</p></li><li><p>Spotlight：用 <code>mdfind</code> 搜索文件，用 <code>mdls</code> 列出元数据（例如照片的 EXIF 信息）。</p></li><li><p>注意 OS X 系统是基于 BSD UNIX 的，许多命令（例如 <code>ps</code>，<code>ls</code>，<code>tail</code>，<code>awk</code>，<code>sed</code>）都和 Linux 中有微妙的不同（ Linux 很大程度上受到了 System V-style Unix 和 GNU 工具影响）。你可以通过标题为 “BSD General Commands Manual” 的 man 页面发现这些不同。在有些情况下 GNU 版本的命令也可能被安装（例如 <code>gawk</code> 和 <code>gsed</code> 对应 GNU 中的 awk 和 sed ）。如果要写跨平台的 Bash 脚本，避免使用这些命令（例如，考虑 Python 或者 <code>perl</code> ）或者经过仔细的测试。</p></li><li><p>用 <code>sw_vers</code> 获取 OS X 的版本信息。</p></li></ul><h2 id="仅限-Windows-系统"><a href="#仅限-Windows-系统" class="headerlink" title="仅限 Windows 系统"></a>仅限 Windows 系统</h2><p>以下是<em>仅限于</em> Windows 系统的技巧。</p><h3 id="在-Winodws-下获取-Unix-工具"><a href="#在-Winodws-下获取-Unix-工具" class="headerlink" title="在 Winodws 下获取 Unix 工具"></a>在 Winodws 下获取 Unix 工具</h3><ul><li><p>可以安装 <a href="https://cygwin.com/">Cygwin</a> 允许你在 Microsoft Windows 中体验 Unix shell 的威力。这样的话，本文中介绍的大多数内容都将适用。</p></li><li><p>在 Windows 10 上，你可以使用 <a href="https://msdn.microsoft.com/commandline/wsl/about">Bash on Ubuntu on Windows</a>，它提供了一个熟悉的 Bash 环境，包含了不少 Unix 命令行工具。好处是它允许 Linux 上编写的程序在 Windows 上运行，而另一方面，Windows 上编写的程序却无法在 Bash 命令行中运行。</p></li><li><p>如果你在 Windows 上主要想用 GNU 开发者工具（例如 GCC），可以考虑 <a href="http://www.mingw.org/">MinGW</a> 以及它的 <a href="http://www.mingw.org/wiki/msys">MSYS</a> 包，这个包提供了例如 bash，gawk，make 和 grep 的工具。MSYS 并不包含所有可以与 Cygwin 媲美的特性。当制作 Unix 工具的原生 Windows 端口时 MinGW 将特别地有用。</p></li><li><p>另一个在 Windows 下实现接近 Unix 环境外观效果的选项是 <a href="https://github.com/dthree/cash">Cash</a>。注意在此环境下只有很少的 Unix 命令和命令行可用。</p></li></ul><h3 id="实用-Windows-命令行工具"><a href="#实用-Windows-命令行工具" class="headerlink" title="实用 Windows 命令行工具"></a>实用 Windows 命令行工具</h3><ul><li><p>可以使用 <code>wmic</code> 在命令行环境下给大部分 Windows 系统管理任务编写脚本以及执行这些任务。</p></li><li><p>Windows 实用的原生命令行网络工具包括 <code>ping</code>，<code>ipconfig</code>，<code>tracert</code>，和 <code>netstat</code>。</p></li><li><p>可以使用 <code>Rundll32</code> 命令来实现<a href="http://www.thewindowsclub.com/rundll32-shortcut-commands-windows">许多有用的 Windows 任务</a> 。</p></li></ul><h3 id="Cygwin-技巧"><a href="#Cygwin-技巧" class="headerlink" title="Cygwin 技巧"></a>Cygwin 技巧</h3><ul><li><p>通过 Cygwin 的包管理器来安装额外的 Unix 程序。</p></li><li><p>使用 <code>mintty</code> 作为你的命令行窗口。</p></li><li><p>要访问 Windows 剪贴板，可以通过 <code>/dev/clipboard</code>。</p></li><li><p>运行 <code>cygstart</code> 以通过默认程序打开一个文件。</p></li><li><p>要访问 Windows 注册表，可以使用 <code>regtool</code>。</p></li><li><p>注意 Windows 驱动器路径 <code>C:\</code> 在 Cygwin 中用 <code>/cygdrive/c</code> 代表，而 Cygwin 的 <code>/</code> 代表 Windows 中的 <code>C:\cygwin</code>。要转换 Cygwin 和 Windows 风格的路径可以用 <code>cygpath</code>。这在需要调用 Windows 程序的脚本里很有用。</p></li><li><p>学会使用 <code>wmic</code>，你就可以从命令行执行大多数 Windows 系统管理任务，并编成脚本。</p></li><li><p>要在 Windows 下获得 Unix 的界面和体验，另一个办法是使用 <a href="https://github.com/dthree/cash">Cash</a>。需要注意的是，这个环境支持的 Unix 命令和命令行参数非常少。</p></li><li><p>要在 Windows 上获取 GNU 开发者工具（比如 GCC）的另一个办法是使用 <a href="http://www.mingw.org/">MinGW</a> 以及它的 <a href="http://www.mingw.org/wiki/msys">MSYS</a> 软件包，该软件包提供了 bash、gawk、make、grep 等工具。然而 MSYS 提供的功能没有 Cygwin 完善。MinGW 在创建 Unix 工具的 Windows 原生移植方面非常有用。</p></li></ul><h2 id="更多资源"><a href="#更多资源" class="headerlink" title="更多资源"></a>更多资源</h2><ul><li><a href="https://github.com/alebcay/awesome-shell">awesome-shell</a>：一份精心组织的命令行工具及资源的列表。</li><li><a href="https://github.com/herrbischoff/awesome-osx-command-line">awesome-osx-command-line</a>：一份针对 OS X 命令行的更深入的指南。</li><li><a href="http://redsymbol.net/articles/unofficial-bash-strict-mode/">Strict mode</a>：为了编写更好的脚本文件。</li><li><a href="https://github.com/koalaman/shellcheck">shellcheck</a>：一个静态 shell 脚本分析工具，本质上是 bash／sh／zsh 的 lint。</li><li><a href="http://www.dwheeler.com/essays/filenames-in-shell.html">Filenames and Pathnames in Shell</a>：有关如何在 shell 脚本里正确处理文件名的细枝末节。</li><li><a href="http://datascienceatthecommandline.com/#tools">Data Science at the Command Line</a>：用于数据科学的一些命令和工具，摘自同名书籍。</li></ul><h2 id="免责声明"><a href="#免责声明" class="headerlink" title="免责声明"></a>免责声明</h2><p>除去特别小的工作，你编写的代码应当方便他人阅读。能力往往伴随着责任，你 <em>有能力</em> 在 Bash 中玩一些奇技淫巧并不意味着你应该去做！;)</p><h2 id="授权条款"><a href="#授权条款" class="headerlink" title="授权条款"></a>授权条款</h2><p>[<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="http://creativecommons.org/licenses/by-sa/4.0/" alt="Creative Commons License](https://i.creativecommons.org/l/by-sa/4.0/88x31.png)" title="">                </div>                <div class="image-caption">Creative Commons License](https://i.creativecommons.org/l/by-sa/4.0/88x31.png)</div>            </figure></p><p>本文使用授权协议 <a href="http://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本文转载自 &lt;a href=&quot;https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md&quot;&gt;the-art-of-command-line&lt;/a&gt; &lt;/p&gt;
&lt;</summary>
      
    
    
    
    <category term="转载" scheme="https://blog.jugg.xyz/categories/%E8%BD%AC%E8%BD%BD/"/>
    
    
    <category term="UNIX" scheme="https://blog.jugg.xyz/tags/UNIX/"/>
    
    <category term="Shell" scheme="https://blog.jugg.xyz/tags/Shell/"/>
    
  </entry>
  
  <entry>
    <title>Docker 基础知识</title>
    <link href="https://blog.jugg.xyz/2018/12/04/repost/Docker/"/>
    <id>https://blog.jugg.xyz/2018/12/04/repost/Docker/</id>
    <published>2018-12-04T04:40:20.000Z</published>
    <updated>2026-05-02T11:24:14.333Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文转载自 <a href="https://github.com/CyC2018/CS-Notes">CS-Note</a> 项目</p></blockquote><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/011f3ef6-d824-4d43-8b2c-36dab8eaaa72-1.png"/> </div><br><h1 id="一、解决的问题"><a href="#一、解决的问题" class="headerlink" title="一、解决的问题"></a>一、解决的问题</h1><p>由于不同的机器有不同的操作系统，以及不同的库和组件，在将一个应用部署到多台机器上需要进行大量的环境配置操作。</p><p>Docker 主要解决环境配置问题，它是一种虚拟化技术，对进程进行隔离，被隔离的进程独立于宿主操作系统和其它隔离的进程。使用 Docker 可以不修改应用程序代码，不需要开发人员学习特定环境下的技术，就能够将现有的应用程序部署在其他机器中。</p><h1 id="二、与虚拟机的比较"><a href="#二、与虚拟机的比较" class="headerlink" title="二、与虚拟机的比较"></a>二、与虚拟机的比较</h1><p>虚拟机也是一种虚拟化技术，它与 Docker 最大的区别在于它是通过模拟硬件，并在硬件上安装操作系统来实现。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/71f61bc3-582d-4c27-8bdd-dc7fb135bf8f.png"/> </div><br><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/7e873b60-44dc-4911-b080-defd5b8f0b49.png"/> </div><br><h2 id="启动速度"><a href="#启动速度" class="headerlink" title="启动速度"></a>启动速度</h2><p>启动虚拟机需要启动虚拟机的操作系统，再启动应用，这个过程非常慢；</p><p>而启动 Docker 相当于启动宿主操作系统上的一个进程。</p><h2 id="占用资源"><a href="#占用资源" class="headerlink" title="占用资源"></a>占用资源</h2><p>虚拟机是一个完整的操作系统，需要占用大量的磁盘、内存和 CPU，一台机器只能开启几十个的虚拟机。</p><p>而 Docker 只是一个进程，只需要将应用以及相关的组件打包，在运行时占用很少的资源，一台机器可以开启成千上万个 Docker。</p><h1 id="三、优势"><a href="#三、优势" class="headerlink" title="三、优势"></a>三、优势</h1><p>除了启动速度快以及占用资源少之外，Docker 具有以下优势：</p><h2 id="更容易迁移"><a href="#更容易迁移" class="headerlink" title="更容易迁移"></a>更容易迁移</h2><p>提供一致性的运行环境，可以在不同的机器上进行迁移，而不用担心环境变化导致无法运行。</p><h2 id="更容易维护"><a href="#更容易维护" class="headerlink" title="更容易维护"></a>更容易维护</h2><p>使用分层技术和镜像，使得应用可以更容易复用重复部分。复用程度越高，维护工作也越容易。</p><h2 id="更容易扩展"><a href="#更容易扩展" class="headerlink" title="更容易扩展"></a>更容易扩展</h2><p>可以使用基础镜像进一步扩展得到新的镜像，并且官方和开源社区提供了大量的镜像，通过扩展这些镜像可以非常容易得到我们想要的镜像。</p><h1 id="四、使用场景"><a href="#四、使用场景" class="headerlink" title="四、使用场景"></a>四、使用场景</h1><h2 id="持续集成"><a href="#持续集成" class="headerlink" title="持续集成"></a>持续集成</h2><p>持续集成指的是频繁地将代码集成到主干上，这样能够更快地发现错误。</p><p>Docker 具有轻量级以及隔离性的特点，在将代码集成到一个 Docker 中不会对其它 Docker 产生影响。</p><h2 id="提供可伸缩的云服务"><a href="#提供可伸缩的云服务" class="headerlink" title="提供可伸缩的云服务"></a>提供可伸缩的云服务</h2><p>根据应用的负载情况，可以很容易地增加或者减少 Docker。</p><h2 id="搭建微服务架构"><a href="#搭建微服务架构" class="headerlink" title="搭建微服务架构"></a>搭建微服务架构</h2><p>Docker 轻量级的特点使得它很适合用于部署、维护、组合微服务。</p><h1 id="五、镜像与容器"><a href="#五、镜像与容器" class="headerlink" title="五、镜像与容器"></a>五、镜像与容器</h1><p>镜像是一种静态的结构，可以看成面向对象里面的类，而容器是镜像的一个实例。</p><p>镜像包含着容器运行时所需要的代码以及其它组件，它是一种分层结构，每一层都是只读的（read-only layers）。构建镜像时，会一层一层构建，前一层是后一层的基础。镜像的这种分层存储结构很适合镜像的复用以及定制。</p><p>构建容器时，通过在镜像的基础上添加一个可写层（writable layer），用来保存着容器运行过程中的修改。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/docker-filesystems-busyboxrw.png"/> </div><br><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li><a href="https://blog.docker.com/2017/08/docker-101-introduction-docker-webinar-recap/">DOCKER 101: INTRODUCTION TO DOCKER WEBINAR RECAP</a></li><li><a href="http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html">Docker 入门教程</a></li><li><a href="http://www.bogotobogo.com/DevOps/Docker/Docker_Container_vs_Virtual_Machine.php">Docker container vs Virtual machine</a></li><li><a href="https://linoxide.com/linux-how-to/dockerfile-create-docker-container/">How to Create Docker Container using Dockerfile</a></li><li><a href="http://www.cnblogs.com/sammyliu/p/5877964.html">理解 Docker（2）：Docker 镜像</a></li><li><a href="https://yeasy.gitbooks.io/docker_practice/introduction/why.html">为什么要使用 Docker？</a></li><li><a href="https://www.docker.com/what-docker">What is Docker</a></li><li><a href="http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html">持续集成是什么？</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本文转载自 &lt;a href=&quot;https://github.com/CyC2018/CS-Notes&quot;&gt;CS-Note&lt;/a&gt; 项目&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div align=&quot;center&quot;&gt; &lt;img src=&quot;https://</summary>
      
    
    
    
    <category term="转载" scheme="https://blog.jugg.xyz/categories/%E8%BD%AC%E8%BD%BD/"/>
    
    
    <category term="Docker" scheme="https://blog.jugg.xyz/tags/Docker/"/>
    
  </entry>
  
  <entry>
    <title>MySQL基础知识</title>
    <link href="https://blog.jugg.xyz/2018/08/30/repost/MySQL/"/>
    <id>https://blog.jugg.xyz/2018/08/30/repost/MySQL/</id>
    <published>2018-08-30T10:30:02.000Z</published>
    <updated>2026-05-02T11:24:14.334Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文转载自 <a href="https://github.com/CyC2018/CS-Notes">CS-Note</a> 项目</p></blockquote><h1 id="一、索引"><a href="#一、索引" class="headerlink" title="一、索引"></a>一、索引</h1><h2 id="B-Tree-原理"><a href="#B-Tree-原理" class="headerlink" title="B+ Tree 原理"></a>B+ Tree 原理</h2><h3 id="1-数据结构"><a href="#1-数据结构" class="headerlink" title="1. 数据结构"></a>1. 数据结构</h3><p>B Tree 指的是 Balance Tree，也就是平衡树。平衡树是一颗查找树，并且所有叶子节点位于同一层。</p><p>B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现，它具有 B Tree 的平衡性，并且通过顺序访问指针来提高区间查询的性能。</p><p>在 B+ Tree 中，一个节点中的 key 从左到右非递减排列，如果某个指针的左右相邻 key 分别是 key<sub>i</sub> 和 key<sub>i+1</sub>，且不为 null，则该指针指向节点的所有 key 大于等于 key<sub>i</sub> 且小于等于 key<sub>i+1</sub>。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/061c88c1-572f-424f-b580-9cbce903a3fe.png"/> </div><br><h3 id="2-操作"><a href="#2-操作" class="headerlink" title="2. 操作"></a>2. 操作</h3><p>进行查找操作时，首先在根节点进行二分查找，找到一个 key 所在的指针，然后递归地在指针所指向的节点进行查找。直到查找到叶子节点，然后在叶子节点上进行二分查找，找出 key 所对应的 data。</p><p>插入删除操作会破坏平衡树的平衡性，因此在插入删除操作之后，需要对树进行一个分裂、合并、旋转等操作来维护平衡性。</p><h3 id="3-与红黑树的比较"><a href="#3-与红黑树的比较" class="headerlink" title="3. 与红黑树的比较"></a>3. 与红黑树的比较</h3><p>红黑树等平衡树也可以用来实现索引，但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构，主要有以下两个原因：</p><p>（一）更少的查找次数</p><p>平衡树查找操作的时间复杂度等于树高 h，而树高大致为 O(h)&#x3D;O(log<sub>d</sub>N)，其中 d 为每个节点的出度。</p><p>红黑树的出度为 2，而 B+ Tree 的出度一般都非常大，所以红黑树的树高 h 很明显比 B+ Tree 大非常多，查找的次数也就更多。</p><p>（二）利用磁盘预读特性</p><p>为了减少磁盘 I&#x2F;O，磁盘往往不是严格按需读取，而是每次都会预读。预读过程中，磁盘进行顺序读取，顺序读取不需要进行磁盘寻道，并且只需要很短的旋转时间，速度会非常快。</p><p>操作系统一般将内存和磁盘分割成固态大小的块，每一块称为一页，内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小，使得一次 I&#x2F;O 就能完全载入一个节点。并且可以利用预读特性，相邻的节点也能够被预先载入。</p><h2 id="MySQL-索引"><a href="#MySQL-索引" class="headerlink" title="MySQL 索引"></a>MySQL 索引</h2><p>索引是在存储引擎层实现的，而不是在服务器层实现的，所以不同存储引擎具有不同的索引类型和实现。</p><h3 id="1-B-Tree-索引"><a href="#1-B-Tree-索引" class="headerlink" title="1. B+Tree 索引"></a>1. B+Tree 索引</h3><p>是大多数 MySQL 存储引擎的默认索引类型。</p><p>因为不再需要进行全表扫描，只需要对树进行搜索即可，所以查找速度快很多。</p><p>除了用于查找，还可以用于排序和分组。</p><p>可以指定多个列作为索引列，多个索引列共同组成键。</p><p>适用于全键值、键值范围和键前缀查找，其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找，则无法使用索引。</p><p>InnoDB 的 B+Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录，这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方，所以一个表只能有一个聚簇索引。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/c28c6fbc-2bc1-47d9-9b2e-cf3d4034f877.jpg"/> </div><br><p>辅助索引的叶子节点的 data 域记录着主键的值，因此在使用辅助索引进行查找时，需要先查找到主键值，然后再到主索引中进行查找。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/7ab8ca28-2a41-4adf-9502-cc0a21e63b51.jpg"/> </div><br><h3 id="2-哈希索引"><a href="#2-哈希索引" class="headerlink" title="2. 哈希索引"></a>2. 哈希索引</h3><p>哈希索引能以 O(1) 时间进行查找，但是失去了有序性：</p><ul><li>无法用于排序与分组；</li><li>只支持精确查找，无法用于部分查找和范围查找。</li></ul><p>InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”，当某个索引值被使用的非常频繁时，会在 B+Tree 索引之上再创建一个哈希索引，这样就让 B+Tree 索引具有哈希索引的一些优点，比如快速的哈希查找。</p><h3 id="3-全文索引"><a href="#3-全文索引" class="headerlink" title="3. 全文索引"></a>3. 全文索引</h3><p>MyISAM 存储引擎支持全文索引，用于查找文本中的关键词，而不是直接比较是否相等。</p><p>查找条件使用 MATCH AGAINST，而不是普通的 WHERE。</p><p>全文索引使用倒排索引实现，它记录着关键词到其所在文档的映射。</p><p>InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。</p><h3 id="4-空间数据索引"><a href="#4-空间数据索引" class="headerlink" title="4. 空间数据索引"></a>4. 空间数据索引</h3><p>MyISAM 存储引擎支持空间数据索引（R-Tree），可以用于地理数据存储。空间数据索引会从所有维度来索引数据，可以有效地使用任意维度来进行组合查询。</p><p>必须使用 GIS 相关的函数来维护数据。</p><h2 id="索引优化"><a href="#索引优化" class="headerlink" title="索引优化"></a>索引优化</h2><h3 id="1-独立的列"><a href="#1-独立的列" class="headerlink" title="1. 独立的列"></a>1. 独立的列</h3><p>在进行查询时，索引列不能是表达式的一部分，也不能是函数的参数，否则无法使用索引。</p><p>例如下面的查询不能使用 actor_id 列的索引：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> actor_id <span class="keyword">FROM</span> sakila.actor <span class="keyword">WHERE</span> actor_id <span class="operator">+</span> <span class="number">1</span> <span class="operator">=</span> <span class="number">5</span>;</span><br></pre></td></tr></table></figure><h3 id="2-多列索引"><a href="#2-多列索引" class="headerlink" title="2. 多列索引"></a>2. 多列索引</h3><p>在需要使用多个列作为条件进行查询时，使用多列索引比使用多个单列索引性能更好。例如下面的语句中，最好把 actor_id 和 film_id 设置为多列索引。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> film_id, actor_ id <span class="keyword">FROM</span> sakila.film_actor</span><br><span class="line"><span class="keyword">WHERE</span> actor_id <span class="operator">=</span> <span class="number">1</span> <span class="keyword">AND</span> film_id <span class="operator">=</span> <span class="number">1</span>;</span><br></pre></td></tr></table></figure><h3 id="3-索引列的顺序"><a href="#3-索引列的顺序" class="headerlink" title="3. 索引列的顺序"></a>3. 索引列的顺序</h3><p>让选择性最强的索引列放在前面。</p><p>索引的选择性是指：不重复的索引值和记录总数的比值。最大值为 1，此时每个记录都有唯一的索引与其对应。选择性越高，查询效率也越高。</p><p>例如下面显示的结果中 customer_id 的选择性比 staff_id 更高，因此最好把 customer_id 列放在多列索引的前面。</p><figure class="highlight sql"><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><span class="line"><span class="keyword">SELECT</span> <span class="built_in">COUNT</span>(<span class="keyword">DISTINCT</span> staff_id)<span class="operator">/</span><span class="built_in">COUNT</span>(<span class="operator">*</span>) <span class="keyword">AS</span> staff_id_selectivity,</span><br><span class="line"><span class="built_in">COUNT</span>(<span class="keyword">DISTINCT</span> customer_id)<span class="operator">/</span><span class="built_in">COUNT</span>(<span class="operator">*</span>) <span class="keyword">AS</span> customer_id_selectivity,</span><br><span class="line"><span class="built_in">COUNT</span>(<span class="operator">*</span>)</span><br><span class="line"><span class="keyword">FROM</span> payment;</span><br></pre></td></tr></table></figure><figure class="highlight html"><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><span class="line">   staff_id_selectivity: 0.0001</span><br><span class="line">customer_id_selectivity: 0.0373</span><br><span class="line">               COUNT(*): 16049</span><br></pre></td></tr></table></figure><h3 id="4-前缀索引"><a href="#4-前缀索引" class="headerlink" title="4. 前缀索引"></a>4. 前缀索引</h3><p>对于 BLOB、TEXT 和 VARCHAR 类型的列，必须使用前缀索引，只索引开始的部分字符。</p><p>对于前缀长度的选取需要根据索引选择性来确定。</p><h3 id="5-覆盖索引"><a href="#5-覆盖索引" class="headerlink" title="5. 覆盖索引"></a>5. 覆盖索引</h3><p>索引包含所有需要查询的字段的值。</p><p>具有以下优点：</p><ul><li>索引通常远小于数据行的大小，只读取索引能大大减少数据访问量。</li><li>一些存储引擎（例如 MyISAM）在内存中只缓存索引，而数据依赖于操作系统来缓存。因此，只访问索引可以不使用系统调用（通常比较费时）。</li><li>对于 InnoDB 引擎，若辅助索引能够覆盖查询，则无需访问主索引。</li></ul><h2 id="索引的优点"><a href="#索引的优点" class="headerlink" title="索引的优点"></a>索引的优点</h2><ul><li><p>大大减少了服务器需要扫描的数据行数。</p></li><li><p>帮助服务器避免进行排序和分组，以及避免创建临时表（B+Tree 索引是有序的，可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建，因为不需要排序和分组，也就不需要创建临时表）。</p></li><li><p>将随机 I&#x2F;O 变为顺序 I&#x2F;O（B+Tree 索引是有序的，会将相邻的数据都存储在一起）。</p></li></ul><h2 id="索引的使用条件"><a href="#索引的使用条件" class="headerlink" title="索引的使用条件"></a>索引的使用条件</h2><ul><li><p>对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效；</p></li><li><p>对于中到大型的表，索引就非常有效；</p></li><li><p>但是对于特大型的表，建立和维护索引的代价将会随之增长。这种情况下，需要用到一种技术可以直接区分出需要查询的一组数据，而不是一条记录一条记录地匹配，例如可以使用分区技术。</p></li></ul><h1 id="二、查询性能优化"><a href="#二、查询性能优化" class="headerlink" title="二、查询性能优化"></a>二、查询性能优化</h1><h2 id="使用-Explain-进行分析"><a href="#使用-Explain-进行分析" class="headerlink" title="使用 Explain 进行分析"></a>使用 Explain 进行分析</h2><p>Explain 用来分析 SELECT 查询语句，开发人员可以通过分析 Explain 结果来优化查询语句。</p><p>比较重要的字段有：</p><ul><li>select_type : 查询类型，有简单查询、联合查询、子查询等</li><li>key : 使用的索引</li><li>rows : 扫描的行数</li></ul><h2 id="优化数据访问"><a href="#优化数据访问" class="headerlink" title="优化数据访问"></a>优化数据访问</h2><h3 id="1-减少请求的数据量"><a href="#1-减少请求的数据量" class="headerlink" title="1. 减少请求的数据量"></a>1. 减少请求的数据量</h3><ul><li>只返回必要的列：最好不要使用 SELECT * 语句。</li><li>只返回必要的行：使用 LIMIT 语句来限制返回的数据。</li><li>缓存重复查询的数据：使用缓存可以避免在数据库中进行查询，特别在要查询的数据经常被重复查询时，缓存带来的查询性能提升将会是非常明显的。</li></ul><h3 id="2-减少服务器端扫描的行数"><a href="#2-减少服务器端扫描的行数" class="headerlink" title="2. 减少服务器端扫描的行数"></a>2. 减少服务器端扫描的行数</h3><p>最有效的方式是使用索引来覆盖查询。</p><h2 id="重构查询方式"><a href="#重构查询方式" class="headerlink" title="重构查询方式"></a>重构查询方式</h2><h3 id="1-切分大查询"><a href="#1-切分大查询" class="headerlink" title="1. 切分大查询"></a>1. 切分大查询</h3><p>一个大查询如果一次性执行的话，可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">DELEFT <span class="keyword">FROM</span> messages <span class="keyword">WHERE</span> <span class="keyword">create</span> <span class="operator">&lt;</span> DATE_SUB(NOW(), <span class="type">INTERVAL</span> <span class="number">3</span> <span class="keyword">MONTH</span>);</span><br></pre></td></tr></table></figure><figure class="highlight sql"><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><span class="line">rows_affected <span class="operator">=</span> <span class="number">0</span></span><br><span class="line">do &#123;</span><br><span class="line">    rows_affected <span class="operator">=</span> do_query(</span><br><span class="line">    &quot;DELETE FROM messages WHERE create  &lt; DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000&quot;)</span><br><span class="line">&#125; while rows_affected <span class="operator">&gt;</span> <span class="number">0</span></span><br></pre></td></tr></table></figure><h3 id="2-分解大连接查询"><a href="#2-分解大连接查询" class="headerlink" title="2. 分解大连接查询"></a>2. 分解大连接查询</h3><p>将一个大连接查询分解成对每一个表进行一次单表查询，然后在应用程序中进行关联，这样做的好处有：</p><ul><li>让缓存更高效。对于连接查询，如果其中一个表发生变化，那么整个查询缓存就无法使用。而分解后的多个查询，即使其中一个表发生变化，对其它表的查询缓存依然可以使用。</li><li>分解成多个单表查询，这些单表查询的缓存结果更可能被其它查询使用到，从而减少冗余记录的查询。</li><li>减少锁竞争；</li><li>在应用层进行连接，可以更容易对数据库进行拆分，从而更容易做到高性能和可伸缩。</li><li>查询本身效率也可能会有所提升。例如下面的例子中，使用 IN() 代替连接查询，可以让 MySQL 按照 ID 顺序进行查询，这可能比随机的连接要更高效。</li></ul><figure class="highlight sql"><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><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> tab</span><br><span class="line"><span class="keyword">JOIN</span> tag_post <span class="keyword">ON</span> tag_post.tag_id<span class="operator">=</span>tag.id</span><br><span class="line"><span class="keyword">JOIN</span> post <span class="keyword">ON</span> tag_post.post_id<span class="operator">=</span>post.id</span><br><span class="line"><span class="keyword">WHERE</span> tag.tag<span class="operator">=</span><span class="string">&#x27;mysql&#x27;</span>;</span><br></pre></td></tr></table></figure><figure class="highlight sql"><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><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> tag <span class="keyword">WHERE</span> tag<span class="operator">=</span><span class="string">&#x27;mysql&#x27;</span>;</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> tag_post <span class="keyword">WHERE</span> tag_id<span class="operator">=</span><span class="number">1234</span>;</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> post <span class="keyword">WHERE</span> post.id <span class="keyword">IN</span> (<span class="number">123</span>,<span class="number">456</span>,<span class="number">567</span>,<span class="number">9098</span>,<span class="number">8904</span>);</span><br></pre></td></tr></table></figure><h1 id="三、存储引擎"><a href="#三、存储引擎" class="headerlink" title="三、存储引擎"></a>三、存储引擎</h1><h2 id="InnoDB"><a href="#InnoDB" class="headerlink" title="InnoDB"></a>InnoDB</h2><p>是 MySQL 默认的事务型存储引擎，只有在需要它不支持的特性时，才考虑使用其它存储引擎。</p><p>实现了四个标准的隔离级别，默认级别是可重复读（REPEATABLE READ）。在可重复读隔离级别下，通过多版本并发控制（MVCC）+ 间隙锁（Next-Key Locking）防止幻影读。</p><p>主索引是聚簇索引，在索引中保存了数据，从而避免直接读取磁盘，因此对查询性能有很大的提升。</p><p>内部做了很多优化，包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。</p><p>支持真正的在线热备份。其它存储引擎不支持在线热备份，要获取一致性视图需要停止对所有表的写入，而在读写混合场景中，停止写入可能也意味着停止读取。</p><h2 id="MyISAM"><a href="#MyISAM" class="headerlink" title="MyISAM"></a>MyISAM</h2><p>设计简单，数据以紧密格式存储。对于只读数据，或者表比较小、可以容忍修复操作，则依然可以使用它。</p><p>提供了大量的特性，包括压缩表、空间数据索引等。</p><p>不支持事务。</p><p>不支持行级锁，只能对整张表加锁，读取时会对需要读到的所有表加共享锁，写入时则对表加排它锁。但在表有读取操作的同时，也可以往表中插入新的记录，这被称为并发插入（CONCURRENT INSERT）。</p><p>可以手工或者自动执行检查和修复操作，但是和事务恢复以及崩溃恢复不同，可能导致一些数据丢失，而且修复操作是非常慢的。</p><p>如果指定了 DELAY_KEY_WRITE 选项，在每次修改执行完成时，不会立即将修改的索引数据写入磁盘，而是会写到内存中的键缓冲区，只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能，但是在数据库或者主机崩溃时会造成索引损坏，需要执行修复操作。</p><h2 id="比较"><a href="#比较" class="headerlink" title="比较"></a>比较</h2><ul><li><p>事务：InnoDB 是事务型的，可以使用 Commit 和 Rollback 语句。</p></li><li><p>并发：MyISAM 只支持表级锁，而 InnoDB 还支持行级锁。</p></li><li><p>外键：InnoDB 支持外键。</p></li><li><p>备份：InnoDB 支持在线热备份。</p></li><li><p>崩溃恢复：MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多，而且恢复的速度也更慢。</p></li><li><p>其它特性：MyISAM 支持压缩表和空间数据索引。</p></li></ul><h1 id="四、数据类型"><a href="#四、数据类型" class="headerlink" title="四、数据类型"></a>四、数据类型</h1><h2 id="整型"><a href="#整型" class="headerlink" title="整型"></a>整型</h2><p>TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间，一般情况下越小的列越好。</p><p>INT(11) 中的数字只是规定了交互工具显示字符的个数，对于存储和计算来说是没有意义的。</p><h2 id="浮点数"><a href="#浮点数" class="headerlink" title="浮点数"></a>浮点数</h2><p>FLOAT 和 DOUBLE 为浮点类型，DECIMAL 为高精度小数类型。CPU 原生支持浮点运算，但是不支持 DECIMAl 类型的计算，因此 DECIMAL 的计算比浮点类型需要更高的代价。</p><p>FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽，例如 DECIMAL(18, 9) 表示总共 18 位，取 9 位存储小数部分，剩下 9 位存储整数部分。</p><h2 id="字符串"><a href="#字符串" class="headerlink" title="字符串"></a>字符串</h2><p>主要有 CHAR 和 VARCHAR 两种类型，一种是定长的，一种是变长的。</p><p>VARCHAR 这种变长类型能够节省空间，因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长，当超出一个页所能容纳的大小时，就要执行额外的操作。MyISAM 会将行拆成不同的片段存储，而 InnoDB 则需要分裂页来使行放进页内。</p><p>VARCHAR 会保留字符串末尾的空格，而 CHAR 会删除。</p><h2 id="时间和日期"><a href="#时间和日期" class="headerlink" title="时间和日期"></a>时间和日期</h2><p>MySQL 提供了两种相似的日期时间类型：DATETIME 和 TIMESTAMP。</p><h3 id="1-DATETIME"><a href="#1-DATETIME" class="headerlink" title="1. DATETIME"></a>1. DATETIME</h3><p>能够保存从 1001 年到 9999 年的日期和时间，精度为秒，使用 8 字节的存储空间。</p><p>它与时区无关。</p><p>默认情况下，MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值，例如“2008-01-16 22:37:08”，这是 ANSI 标准定义的日期和时间表示方法。</p><h3 id="2-TIMESTAMP"><a href="#2-TIMESTAMP" class="headerlink" title="2. TIMESTAMP"></a>2. TIMESTAMP</h3><p>和 UNIX 时间戳相同，保存从 1970 年 1 月 1 日午夜（格林威治时间）以来的秒数，使用 4 个字节，只能表示从 1970 年到 2038 年。</p><p>它和时区有关，也就是说一个时间戳在不同的时区所代表的具体时间是不同的。</p><p>MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期，并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。</p><p>默认情况下，如果插入时没有指定 TIMESTAMP 列的值，会将这个值设置为当前时间。</p><p>应该尽量使用 TIMESTAMP，因为它比 DATETIME 空间效率更高。</p><h1 id="五、切分"><a href="#五、切分" class="headerlink" title="五、切分"></a>五、切分</h1><h2 id="水平切分"><a href="#水平切分" class="headerlink" title="水平切分"></a>水平切分</h2><p>水平切分又称为 Sharding，它是将同一个表中的记录拆分到多个结构相同的表中。</p><p>当一个表的数据不断增多时，Sharding 是必然的选择，它可以将数据分布到集群的不同节点上，从而缓存单个数据库的压力。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg"/> </div><br><h2 id="垂直切分"><a href="#垂直切分" class="headerlink" title="垂直切分"></a>垂直切分</h2><p>垂直切分是将一张表按列切分成多个表，通常是按照列的关系密集程度进行切分，也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。</p><p>在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中，例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/e130e5b8-b19a-4f1e-b860-223040525cf6.jpg"/> </div><br><h2 id="Sharding-策略"><a href="#Sharding-策略" class="headerlink" title="Sharding 策略"></a>Sharding 策略</h2><ul><li>哈希取模：hash(key) % N；</li><li>范围：可以是 ID 范围也可以是时间范围；</li><li>映射表：使用单独的一个数据库来存储映射关系。</li></ul><h2 id="Sharding-存在的问题及解决方案"><a href="#Sharding-存在的问题及解决方案" class="headerlink" title="Sharding 存在的问题及解决方案"></a>Sharding 存在的问题及解决方案</h2><h3 id="1-事务问题"><a href="#1-事务问题" class="headerlink" title="1. 事务问题"></a>1. 事务问题</h3><p>使用分布式事务来解决，比如 XA 接口。</p><h3 id="2-JOIN"><a href="#2-JOIN" class="headerlink" title="2. JOIN"></a>2. JOIN</h3><p>可以将原来的 JOIN 分解成多个单表 JOIN 查询，然后在用户程序中进行 JOIN。</p><h3 id="3-ID-唯一性"><a href="#3-ID-唯一性" class="headerlink" title="3. ID 唯一性"></a>3. ID 唯一性</h3><ul><li>使用全局唯一 ID：GUID</li><li>为每个分片指定一个 ID 范围</li><li>分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)</li></ul><h1 id="六、复制"><a href="#六、复制" class="headerlink" title="六、复制"></a>六、复制</h1><h2 id="主从复制"><a href="#主从复制" class="headerlink" title="主从复制"></a>主从复制</h2><p>主要涉及三个线程：binlog 线程、I&#x2F;O 线程和 SQL 线程。</p><ul><li><strong>binlog 线程</strong> ：负责将主服务器上的数据更改写入二进制日志（Binary log）中。</li><li><strong>I&#x2F;O 线程</strong> ：负责从主服务器上读取二进制日志，并写入从服务器的重放日志（Replay log）中。</li><li><strong>SQL 线程</strong> ：负责读取重放日志并重放其中的 SQL 语句。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/master-slave.png"/> </div><br><h2 id="读写分离"><a href="#读写分离" class="headerlink" title="读写分离"></a>读写分离</h2><p>主服务器处理写操作以及实时性要求比较高的读操作，而从服务器处理读操作。</p><p>读写分离能提高性能的原因在于：</p><ul><li>主从服务器负责各自的读和写，极大程度缓解了锁的争用；</li><li>从服务器可以使用 MyISAM，提升查询性能以及节约系统开销；</li><li>增加冗余，提高可用性。</li></ul><p>读写分离常用代理方式来实现，代理服务器接收应用层传来的读写请求，然后决定转发到哪个服务器。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/master-slave-proxy.png"/> </div><br><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li>BaronScbwartz, PeterZaitsev, VadimTkacbenko, 等. 高性能 MySQL[M]. 电子工业出版社, 2013.</li><li>姜承尧. MySQL 技术内幕: InnoDB 存储引擎 [M]. 机械工业出版社, 2011.</li><li><a href="https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html">20+ 条 MySQL 性能优化的最佳经验</a></li><li><a href="http://blog.720ui.com/2017/mysql_core_09_multi_db_table2/" title="服务端指南 数据存储篇 | MySQL（09） 分库与分表带来的分布式困境与应对之策">服务端指南 数据存储篇 | MySQL（09） 分库与分表带来的分布式困境与应对之策</a></li><li><a href="https://stackoverflow.com/questions/788829/how-to-create-unique-row-id-in-sharded-databases">How to create unique row ID in sharded databases?</a></li><li><a href="http://geekswithblogs.net/shaunxu/archive/2012/01/07/sql-azure-federation-ndash-introduction.aspx" title="Title of this entry.">SQL Azure Federation – Introduction</a></li><li><a href="http://blog.codinglabs.org/articles/theory-of-mysql-index.html">MySQL 索引背后的数据结构及算法原理</a></li><li><a href="https://segmentfault.com/a/1190000008131735">MySQL 性能优化神器 Explain 使用分析</a></li><li><a href="https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6">How Sharding Works</a></li><li><a href="https://tech.meituan.com/dianping_order_db_sharding.html">大众点评订单系统分库分表实践</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本文转载自 &lt;a href=&quot;https://github.com/CyC2018/CS-Notes&quot;&gt;CS-Note&lt;/a&gt; 项目&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&quot;一、索引&quot;&gt;&lt;a href=&quot;#一、索引&quot; class=&quot;he</summary>
      
    
    
    
    <category term="转载" scheme="https://blog.jugg.xyz/categories/%E8%BD%AC%E8%BD%BD/"/>
    
    
    <category term="MySQL" scheme="https://blog.jugg.xyz/tags/MySQL/"/>
    
  </entry>
  
  <entry>
    <title>HTTP 基础知识</title>
    <link href="https://blog.jugg.xyz/2018/06/05/repost/HTTP/"/>
    <id>https://blog.jugg.xyz/2018/06/05/repost/HTTP/</id>
    <published>2018-06-05T10:30:02.000Z</published>
    <updated>2026-05-02T11:24:14.333Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文转载自 <a href="https://github.com/CyC2018/CS-Notes">CS-Note</a> 项目</p></blockquote><h1 id="一-、基础概念"><a href="#一-、基础概念" class="headerlink" title="一 、基础概念"></a>一 、基础概念</h1><h2 id="URL"><a href="#URL" class="headerlink" title="URL"></a>URL</h2><p>URI 包含 URL 和 URN，目前 WEB 只有 URL 比较流行，所以见到的基本都是 URL。</p><ul><li>URI（Uniform Resource Identifier，统一资源标识符）</li><li>URL（Uniform Resource Locator，统一资源定位符）</li><li>URN（Uniform Resource Name，统一资源名称）</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/urlnuri.jpg" width="600"/> </div><br><h2 id="请求和响应报文"><a href="#请求和响应报文" class="headerlink" title="请求和响应报文"></a>请求和响应报文</h2><h3 id="1-请求报文"><a href="#1-请求报文" class="headerlink" title="1. 请求报文"></a>1. 请求报文</h3><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/HTTP_RequestMessageExample.png" width=""/> </div><br><h3 id="2-响应报文"><a href="#2-响应报文" class="headerlink" title="2. 响应报文"></a>2. 响应报文</h3><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/HTTP_ResponseMessageExample.png" width=""/> </div><br><h1 id="二、HTTP-方法"><a href="#二、HTTP-方法" class="headerlink" title="二、HTTP 方法"></a>二、HTTP 方法</h1><p>客户端发送的  <strong>请求报文</strong>  第一行为请求行，包含了方法字段。</p><h2 id="GET"><a href="#GET" class="headerlink" title="GET"></a>GET</h2><blockquote><p>获取资源</p></blockquote><p>当前网络请求中，绝大部分使用的是 GET 方法。</p><h2 id="HEAD"><a href="#HEAD" class="headerlink" title="HEAD"></a>HEAD</h2><blockquote><p>获取报文首部</p></blockquote><p>和 GET 方法一样，但是不返回报文实体主体部分。</p><p>主要用于确认 URL 的有效性以及资源更新的日期时间等。</p><h2 id="POST"><a href="#POST" class="headerlink" title="POST"></a>POST</h2><blockquote><p>传输实体主体</p></blockquote><p>POST 主要用来传输数据，而 GET 主要用来获取资源。</p><p>更多 POST 与 GET 的比较请见第八章。</p><h2 id="PUT"><a href="#PUT" class="headerlink" title="PUT"></a>PUT</h2><blockquote><p>上传文件</p></blockquote><p>由于自身不带验证机制，任何人都可以上传文件，因此存在安全性问题，一般不使用该方法。</p><figure class="highlight html"><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><span class="line">PUT /new.html HTTP/1.1</span><br><span class="line">Host: example.com</span><br><span class="line">Content-type: text/html</span><br><span class="line">Content-length: 16</span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">p</span>&gt;</span>New File<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="PATCH"><a href="#PATCH" class="headerlink" title="PATCH"></a>PATCH</h2><blockquote><p>对资源进行部分修改</p></blockquote><p>PUT 也可以用于修改资源，但是只能完全替代原始资源，PATCH 允许部分修改。</p><figure class="highlight html"><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><span class="line">PATCH /file.txt HTTP/1.1</span><br><span class="line">Host: www.example.com</span><br><span class="line">Content-Type: application/example</span><br><span class="line">If-Match: &quot;e0023aa4e&quot;</span><br><span class="line">Content-Length: 100</span><br><span class="line"></span><br><span class="line">[description of changes]</span><br></pre></td></tr></table></figure><h2 id="DELETE"><a href="#DELETE" class="headerlink" title="DELETE"></a>DELETE</h2><blockquote><p>删除文件</p></blockquote><p>与 PUT 功能相反，并且同样不带验证机制。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">DELETE /file.html HTTP/1.1</span><br></pre></td></tr></table></figure><h2 id="OPTIONS"><a href="#OPTIONS" class="headerlink" title="OPTIONS"></a>OPTIONS</h2><blockquote><p>查询支持的方法</p></blockquote><p>查询指定的 URL 能够支持的方法。</p><p>会返回 Allow: GET, POST, HEAD, OPTIONS 这样的内容。</p><h2 id="CONNECT"><a href="#CONNECT" class="headerlink" title="CONNECT"></a>CONNECT</h2><blockquote><p>要求在与代理服务器通信时建立隧道</p></blockquote><p>使用 SSL（Secure Sockets Layer，安全套接层）和 TLS（Transport Layer Security，传输层安全）协议把通信内容加密后经网络隧道传输。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CONNECT www.example.com:443 HTTP/1.1</span><br></pre></td></tr></table></figure><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/dc00f70e-c5c8-4d20-baf1-2d70014a97e3.jpg" width=""/> </div><br><h2 id="TRACE"><a href="#TRACE" class="headerlink" title="TRACE"></a>TRACE</h2><blockquote><p>追踪路径</p></blockquote><p>服务器会将通信路径返回给客户端。</p><p>发送请求时，在 Max-Forwards 首部字段中填入数值，每经过一个服务器就会减 1，当数值为 0 时就停止传输。</p><p>通常不会使用 TRACE，并且它容易受到 XST 攻击（Cross-Site Tracing，跨站追踪）。</p><h1 id="三、HTTP-状态码"><a href="#三、HTTP-状态码" class="headerlink" title="三、HTTP 状态码"></a>三、HTTP 状态码</h1><p>服务器返回的  <strong>响应报文</strong>  中第一行为状态行，包含了状态码以及原因短语，用来告知客户端请求的结果。</p><table><thead><tr><th align="center">状态码</th><th align="center">类别</th><th align="center">原因短语</th></tr></thead><tbody><tr><td align="center">1XX</td><td align="center">Informational（信息性状态码）</td><td align="center">接收的请求正在处理</td></tr><tr><td align="center">2XX</td><td align="center">Success（成功状态码）</td><td align="center">请求正常处理完毕</td></tr><tr><td align="center">3XX</td><td align="center">Redirection（重定向状态码）</td><td align="center">需要进行附加操作以完成请求</td></tr><tr><td align="center">4XX</td><td align="center">Client Error（客户端错误状态码）</td><td align="center">服务器无法处理请求</td></tr><tr><td align="center">5XX</td><td align="center">Server Error（服务器错误状态码）</td><td align="center">服务器处理请求出错</td></tr></tbody></table><h2 id="1XX-信息"><a href="#1XX-信息" class="headerlink" title="1XX 信息"></a>1XX 信息</h2><ul><li><strong>100 Continue</strong> ：表明到目前为止都很正常，客户端可以继续发送请求或者忽略这个响应。</li></ul><h2 id="2XX-成功"><a href="#2XX-成功" class="headerlink" title="2XX 成功"></a>2XX 成功</h2><ul><li><p><strong>200 OK</strong> </p></li><li><p><strong>204 No Content</strong> ：请求已经成功处理，但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息，而不需要返回数据时使用。</p></li><li><p><strong>206 Partial Content</strong> ：表示客户端进行了范围请求，响应报文包含由 Content-Range 指定范围的实体内容。</p></li></ul><h2 id="3XX-重定向"><a href="#3XX-重定向" class="headerlink" title="3XX 重定向"></a>3XX 重定向</h2><ul><li><p><strong>301 Moved Permanently</strong> ：永久性重定向</p></li><li><p><strong>302 Found</strong> ：临时性重定向</p></li><li><p><strong>303 See Other</strong> ：和 302 有着相同的功能，但是 303 明确要求客户端应该采用 GET 方法获取资源。</p></li><li><p>注：虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法，但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。</p></li><li><p><strong>304 Not Modified</strong> ：如果请求报文首部包含一些条件，例如：If-Match，If-Modified-Since，If-None-Match，If-Range，If-Unmodified-Since，如果不满足条件，则服务器会返回 304 状态码。</p></li><li><p><strong>307 Temporary Redirect</strong> ：临时重定向，与 302 的含义类似，但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。</p></li></ul><h2 id="4XX-客户端错误"><a href="#4XX-客户端错误" class="headerlink" title="4XX 客户端错误"></a>4XX 客户端错误</h2><ul><li><p><strong>400 Bad Request</strong> ：请求报文中存在语法错误。</p></li><li><p><strong>401 Unauthorized</strong> ：该状态码表示发送的请求需要有认证信息（BASIC 认证、DIGEST 认证）。如果之前已进行过一次请求，则表示用户认证失败。</p></li><li><p><strong>403 Forbidden</strong> ：请求被拒绝。</p></li><li><p><strong>404 Not Found</strong></p></li></ul><h2 id="5XX-服务器错误"><a href="#5XX-服务器错误" class="headerlink" title="5XX 服务器错误"></a>5XX 服务器错误</h2><ul><li><p><strong>500 Internal Server Error</strong> ：服务器正在执行请求时发生错误。</p></li><li><p><strong>503 Service Unavailable</strong> ：服务器暂时处于超负载或正在进行停机维护，现在无法处理请求。</p></li></ul><h1 id="四、HTTP-首部"><a href="#四、HTTP-首部" class="headerlink" title="四、HTTP 首部"></a>四、HTTP 首部</h1><p>有 4 种类型的首部字段：通用首部字段、请求首部字段、响应首部字段和实体首部字段。</p><p>各种首部字段及其含义如下（不需要全记，仅供查阅）：</p><h2 id="通用首部字段"><a href="#通用首部字段" class="headerlink" title="通用首部字段"></a>通用首部字段</h2><table><thead><tr><th align="center">首部字段名</th><th align="center">说明</th></tr></thead><tbody><tr><td align="center">Cache-Control</td><td align="center">控制缓存的行为</td></tr><tr><td align="center">Connection</td><td align="center">控制不再转发给代理的首部字段、管理持久连接</td></tr><tr><td align="center">Date</td><td align="center">创建报文的日期时间</td></tr><tr><td align="center">Pragma</td><td align="center">报文指令</td></tr><tr><td align="center">Trailer</td><td align="center">报文末端的首部一览</td></tr><tr><td align="center">Transfer-Encoding</td><td align="center">指定报文主体的传输编码方式</td></tr><tr><td align="center">Upgrade</td><td align="center">升级为其他协议</td></tr><tr><td align="center">Via</td><td align="center">代理服务器的相关信息</td></tr><tr><td align="center">Warning</td><td align="center">错误通知</td></tr></tbody></table><h2 id="请求首部字段"><a href="#请求首部字段" class="headerlink" title="请求首部字段"></a>请求首部字段</h2><table><thead><tr><th align="center">首部字段名</th><th align="center">说明</th></tr></thead><tbody><tr><td align="center">Accept</td><td align="center">用户代理可处理的媒体类型</td></tr><tr><td align="center">Accept-Charset</td><td align="center">优先的字符集</td></tr><tr><td align="center">Accept-Encoding</td><td align="center">优先的内容编码</td></tr><tr><td align="center">Accept-Language</td><td align="center">优先的语言（自然语言）</td></tr><tr><td align="center">Authorization</td><td align="center">Web 认证信息</td></tr><tr><td align="center">Expect</td><td align="center">期待服务器的特定行为</td></tr><tr><td align="center">From</td><td align="center">用户的电子邮箱地址</td></tr><tr><td align="center">Host</td><td align="center">请求资源所在服务器</td></tr><tr><td align="center">If-Match</td><td align="center">比较实体标记（ETag）</td></tr><tr><td align="center">If-Modified-Since</td><td align="center">比较资源的更新时间</td></tr><tr><td align="center">If-None-Match</td><td align="center">比较实体标记（与 If-Match 相反）</td></tr><tr><td align="center">If-Range</td><td align="center">资源未更新时发送实体 Byte 的范围请求</td></tr><tr><td align="center">If-Unmodified-Since</td><td align="center">比较资源的更新时间（与 If-Modified-Since 相反）</td></tr><tr><td align="center">Max-Forwards</td><td align="center">最大传输逐跳数</td></tr><tr><td align="center">Proxy-Authorization</td><td align="center">代理服务器要求客户端的认证信息</td></tr><tr><td align="center">Range</td><td align="center">实体的字节范围请求</td></tr><tr><td align="center">Referer</td><td align="center">对请求中 URI 的原始获取方</td></tr><tr><td align="center">TE</td><td align="center">传输编码的优先级</td></tr><tr><td align="center">User-Agent</td><td align="center">HTTP 客户端程序的信息</td></tr></tbody></table><h2 id="响应首部字段"><a href="#响应首部字段" class="headerlink" title="响应首部字段"></a>响应首部字段</h2><table><thead><tr><th align="center">首部字段名</th><th align="center">说明</th></tr></thead><tbody><tr><td align="center">Accept-Ranges</td><td align="center">是否接受字节范围请求</td></tr><tr><td align="center">Age</td><td align="center">推算资源创建经过时间</td></tr><tr><td align="center">ETag</td><td align="center">资源的匹配信息</td></tr><tr><td align="center">Location</td><td align="center">令客户端重定向至指定 URI</td></tr><tr><td align="center">Proxy-Authenticate</td><td align="center">代理服务器对客户端的认证信息</td></tr><tr><td align="center">Retry-After</td><td align="center">对再次发起请求的时机要求</td></tr><tr><td align="center">Server</td><td align="center">HTTP 服务器的安装信息</td></tr><tr><td align="center">Vary</td><td align="center">代理服务器缓存的管理信息</td></tr><tr><td align="center">WWW-Authenticate</td><td align="center">服务器对客户端的认证信息</td></tr></tbody></table><h2 id="实体首部字段"><a href="#实体首部字段" class="headerlink" title="实体首部字段"></a>实体首部字段</h2><table><thead><tr><th align="center">首部字段名</th><th align="center">说明</th></tr></thead><tbody><tr><td align="center">Allow</td><td align="center">资源可支持的 HTTP 方法</td></tr><tr><td align="center">Content-Encoding</td><td align="center">实体主体适用的编码方式</td></tr><tr><td align="center">Content-Language</td><td align="center">实体主体的自然语言</td></tr><tr><td align="center">Content-Length</td><td align="center">实体主体的大小</td></tr><tr><td align="center">Content-Location</td><td align="center">替代对应资源的 URI</td></tr><tr><td align="center">Content-MD5</td><td align="center">实体主体的报文摘要</td></tr><tr><td align="center">Content-Range</td><td align="center">实体主体的位置范围</td></tr><tr><td align="center">Content-Type</td><td align="center">实体主体的媒体类型</td></tr><tr><td align="center">Expires</td><td align="center">实体主体过期的日期时间</td></tr><tr><td align="center">Last-Modified</td><td align="center">资源的最后修改日期时间</td></tr></tbody></table><h1 id="五、具体应用"><a href="#五、具体应用" class="headerlink" title="五、具体应用"></a>五、具体应用</h1><h2 id="Cookie"><a href="#Cookie" class="headerlink" title="Cookie"></a>Cookie</h2><p>HTTP 协议是无状态的，主要是为了让 HTTP 协议尽可能简单，使得它能够处理大量事务。HTTP&#x2F;1.1 引入 Cookie 来保存状态信息。</p><p>Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据，它会在浏览器之后向同一服务器再次发起请求时被携带上，用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据，因此会带来额外的性能开销（尤其是在移动环境下）。</p><p>Cookie 曾一度用于客户端数据的存储，因为当时并没有其它合适的存储办法而作为唯一的存储手段，但现在随着现代浏览器开始支持各种各样的存储方式，Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地，如使用 Web storage API （本地存储和会话存储）或 IndexedDB。</p><h3 id="1-用途"><a href="#1-用途" class="headerlink" title="1. 用途"></a>1. 用途</h3><ul><li>会话状态管理（如用户登录状态、购物车、游戏分数或其它需要记录的信息）</li><li>个性化设置（如用户自定义设置、主题等）</li><li>浏览器行为跟踪（如跟踪分析用户行为等）</li></ul><h3 id="2-创建过程"><a href="#2-创建过程" class="headerlink" title="2. 创建过程"></a>2. 创建过程</h3><p>服务器发送的响应报文包含 Set-Cookie 首部字段，客户端得到响应报文后把 Cookie 内容保存到浏览器中。</p><figure class="highlight html"><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><span class="line">HTTP/1.0 200 OK</span><br><span class="line">Content-type: text/html</span><br><span class="line">Set-Cookie: yummy_cookie=choco</span><br><span class="line">Set-Cookie: tasty_cookie=strawberry</span><br><span class="line"></span><br><span class="line">[page content]</span><br></pre></td></tr></table></figure><p>客户端之后对同一个服务器发送请求时，会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。</p><figure class="highlight html"><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><span class="line">GET /sample_page.html HTTP/1.1</span><br><span class="line">Host: www.example.org</span><br><span class="line">Cookie: yummy_cookie=choco; tasty_cookie=strawberry</span><br></pre></td></tr></table></figure><h3 id="3-分类"><a href="#3-分类" class="headerlink" title="3. 分类"></a>3. 分类</h3><ul><li>会话期 Cookie：浏览器关闭之后它会被自动删除，也就是说它仅在会话期内有效。</li><li>持久性 Cookie：指定一个特定的过期时间（Expires）或有效期（max-age）之后就成为了持久性的 Cookie。</li></ul><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;</span><br></pre></td></tr></table></figure><h3 id="4-JavaScript-获取-Cookie"><a href="#4-JavaScript-获取-Cookie" class="headerlink" title="4. JavaScript 获取 Cookie"></a>4. JavaScript 获取 Cookie</h3><p>通过 <code>Document.cookie</code> 属性可创建新的 Cookie，也可通过该属性访问非 HttpOnly 标记的 Cookie。</p><figure class="highlight html"><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><span class="line">document.cookie = &quot;yummy_cookie=choco&quot;;</span><br><span class="line">document.cookie = &quot;tasty_cookie=strawberry&quot;;</span><br><span class="line">console.log(document.cookie);</span><br></pre></td></tr></table></figure><h3 id="5-Secure-和-HttpOnly"><a href="#5-Secure-和-HttpOnly" class="headerlink" title="5. Secure 和 HttpOnly"></a>5. Secure 和 HttpOnly</h3><p>标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记，敏感信息也不应该通过 Cookie 传输，因为 Cookie 有其固有的不安全性，Secure 标记也无法提供确实的安全保障。</p><p>标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。跨站脚本攻击 (XSS) 常常使用 JavaScript 的 <code>Document.cookie</code> API 窃取用户的 Cookie 信息，因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly</span><br></pre></td></tr></table></figure><h3 id="6-作用域"><a href="#6-作用域" class="headerlink" title="6. 作用域"></a>6. 作用域</h3><p>Domain 标识指定了哪些主机可以接受 Cookie。如果不指定，默认为当前文档的主机（不包含子域名）。如果指定了 Domain，则一般包含子域名。例如，如果设置 Domain&#x3D;mozilla.org，则 Cookie 也包含在子域名中（如 developer.mozilla.org）。</p><p>Path 标识指定了主机下的哪些路径可以接受 Cookie（该 URL 路径必须存在于请求 URL 中）。以字符 %x2F (“&#x2F;“) 作为路径分隔符，子路径也会被匹配。例如，设置 Path&#x3D;&#x2F;docs，则以下地址都会匹配：</p><ul><li>&#x2F;docs</li><li>&#x2F;docs&#x2F;Web&#x2F;</li><li>&#x2F;docs&#x2F;Web&#x2F;HTTP</li></ul><h3 id="7-Session"><a href="#7-Session" class="headerlink" title="7. Session"></a>7. Session</h3><p>除了可以将用户信息通过 Cookie 存储在用户浏览器中，也可以利用 Session 存储在服务器端，存储在服务器端的信息更加安全。</p><p>Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中，效率会更高。</p><p>使用 Session 维护用户登录状态的过程如下：</p><ul><li>用户进行登录时，用户提交包含用户名和密码的表单，放入 HTTP 请求报文中；</li><li>服务器验证该用户名和密码；</li><li>如果正确则把用户信息存储到 Redis 中，它在 Redis 中的 Key 称为 Session ID；</li><li>服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID，客户端收到响应报文之后将该 Cookie 值存入浏览器中；</li><li>客户端之后对同一个服务器进行请求时会包含该 Cookie 值，服务器收到之后提取出 Session ID，从 Redis 中取出用户信息，继续之前的业务操作。</li></ul><p>应该注意 Session ID 的安全性问题，不能让它被恶意攻击者轻易获取，那么就不能产生一个容易被猜到的 Session ID 值。此外，还需要经常重新生成 Session ID。在对安全性要求极高的场景下，例如转账等操作，除了使用 Session 管理用户状态之外，还需要对用户进行重新验证，比如重新输入密码，或者使用短信验证码等方式。</p><h3 id="8-浏览器禁用-Cookie"><a href="#8-浏览器禁用-Cookie" class="headerlink" title="8. 浏览器禁用 Cookie"></a>8. 浏览器禁用 Cookie</h3><p>此时无法使用 Cookie 来保存用户信息，只能使用 Session。除此之外，不能再将 Session ID 存放到 Cookie 中，而是使用 URL 重写技术，将 Session ID 作为 URL 的参数进行传递。</p><h3 id="9-Cookie-与-Session-选择"><a href="#9-Cookie-与-Session-选择" class="headerlink" title="9. Cookie 与 Session 选择"></a>9. Cookie 与 Session 选择</h3><ul><li>Cookie 只能存储 ASCII 码字符串，而 Session 则可以存取任何类型的数据，因此在考虑数据复杂性时首选 Session；</li><li>Cookie 存储在浏览器中，容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中，可以将 Cookie 值进行加密，然后在服务器进行解密；</li><li>对于大型网站，如果用户所有的信息都存储在 Session 中，那么开销是非常大的，因此不建议将所有的用户信息都存储到 Session 中。</li></ul><h2 id="缓存"><a href="#缓存" class="headerlink" title="缓存"></a>缓存</h2><h3 id="1-优点"><a href="#1-优点" class="headerlink" title="1. 优点"></a>1. 优点</h3><ul><li>缓解服务器压力；</li><li>降低客户端获取资源的延迟：缓存通常位于内存中，读取缓存的速度更快。并且缓存在地理位置上也有可能比源服务器来得近，例如浏览器缓存。</li></ul><h3 id="2-实现方法"><a href="#2-实现方法" class="headerlink" title="2. 实现方法"></a>2. 实现方法</h3><ul><li>让代理服务器进行缓存；</li><li>让客户端浏览器进行缓存。</li></ul><h3 id="3-Cache-Control"><a href="#3-Cache-Control" class="headerlink" title="3. Cache-Control"></a>3. Cache-Control</h3><p>HTTP&#x2F;1.1 通过 Cache-Control 首部字段来控制缓存。</p><p><strong>（一）禁止进行缓存</strong> </p><p>no-store 指令规定不能对请求或响应的任何一部分进行缓存。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Cache-Control: no-store</span><br></pre></td></tr></table></figure><p><strong>（二）强制确认缓存</strong> </p><p>no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性，只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Cache-Control: no-cache</span><br></pre></td></tr></table></figure><p><strong>（三）私有缓存和公共缓存</strong> </p><p>private 指令规定了将资源作为私有缓存，只能被单独用户所使用，一般存储在用户浏览器中。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Cache-Control: private</span><br></pre></td></tr></table></figure><p>public 指令规定了将资源作为公共缓存，可以被多个用户所使用，一般存储在代理服务器中。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Cache-Control: public</span><br></pre></td></tr></table></figure><p><strong>（四）缓存过期机制</strong> </p><p>max-age 指令出现在请求报文中，并且缓存资源的缓存时间小于该指令指定的时间，那么就能接受该缓存。</p><p>max-age 指令出现在响应报文中，表示缓存资源在缓存服务器中保存的时间。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Cache-Control: max-age=31536000</span><br></pre></td></tr></table></figure><p>Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。</p><ul><li>在 HTTP&#x2F;1.1 中，会优先处理 max-age 指令；</li><li>在 HTTP&#x2F;1.0 中，max-age 指令会被忽略掉。</li></ul><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Expires: Wed, 04 Jul 2012 08:26:05 GMT</span><br></pre></td></tr></table></figure><h3 id="4-缓存验证"><a href="#4-缓存验证" class="headerlink" title="4. 缓存验证"></a>4. 缓存验证</h3><p>需要先了解 ETag 首部字段的含义，它是资源的唯一标识。URL 不能唯一表示资源，例如 <code>http://www.google.com/</code> 有中文和英文两个资源，只有 ETag 才能对这两个资源进行唯一标识。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ETag: &quot;82e22293907ce725faf67773957acd12&quot;</span><br></pre></td></tr></table></figure><p>可以将缓存资源的 ETag 值放入 If-None-Match 首部，服务器收到该请求后，判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致，如果一致则表示缓存资源有效，返回 304 Not Modified。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">If-None-Match: &quot;82e22293907ce725faf67773957acd12&quot;</span><br></pre></td></tr></table></figure><p>Last-Modified 首部字段也可以用于缓存验证，它包含在源服务器发送的响应报文中，指示源服务器对资源的最后修改时间。但是它是一种弱校验器，因为只能精确到一秒，所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息，客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回，状态码为 200 OK。如果请求的资源从那时起未经修改，那么返回一个不带有消息主体的 304 Not Modified 响应。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT</span><br></pre></td></tr></table></figure><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT</span><br></pre></td></tr></table></figure><h2 id="连接管理"><a href="#连接管理" class="headerlink" title="连接管理"></a>连接管理</h2><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/HTTP1_x_Connections.png" width="800"/> </div><br><h3 id="1-短连接与长连接"><a href="#1-短连接与长连接" class="headerlink" title="1. 短连接与长连接"></a>1. 短连接与长连接</h3><p>当浏览器访问一个包含多张图片的 HTML 页面时，除了请求访问 HTML 页面资源，还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接，那么开销会很大。</p><p>长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。</p><ul><li>从 HTTP&#x2F;1.1 开始默认是长连接的，如果要断开连接，需要由客户端或者服务器端提出断开，使用 <code>Connection : close</code>；</li><li>在 HTTP&#x2F;1.1 之前默认是短连接的，如果需要使用长连接，则使用 <code>Connection : Keep-Alive</code>。</li></ul><h3 id="2-流水线"><a href="#2-流水线" class="headerlink" title="2. 流水线"></a>2. 流水线</h3><p>默认情况下，HTTP 请求是按顺序发出的，下一个请求只有在当前请求收到响应之后才会被发出。由于会受到网络延迟和带宽的限制，在下一个请求被发送到服务器之前，可能需要等待很长时间。</p><p>流水线是在同一条长连接上发出连续的请求，而不用等待响应返回，这样可以避免连接延迟。</p><h2 id="内容协商"><a href="#内容协商" class="headerlink" title="内容协商"></a>内容协商</h2><p>通过内容协商返回最合适的内容，例如根据浏览器的默认语言选择返回中文界面还是英文界面。</p><h3 id="1-类型"><a href="#1-类型" class="headerlink" title="1. 类型"></a>1. 类型</h3><p><strong>（一）服务端驱动型</strong> </p><p>客户端设置特定的 HTTP 首部字段，例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Languag，服务器根据这些字段返回特定的资源。</p><p>它存在以下问题：</p><ul><li>服务器很难知道客户端浏览器的全部信息；</li><li>客户端提供的信息相当冗长（HTTP&#x2F;2 协议的首部压缩机制缓解了这个问题），并且存在隐私风险（HTTP 指纹识别技术）；</li><li>给定的资源需要返回不同的展现形式，共享缓存的效率会降低，而服务器端的实现会越来越复杂。</li></ul><p><strong>（二）代理驱动型</strong> </p><p>服务器返回 300 Multiple Choices 或者 406 Not Acceptable，客户端从中选出最合适的那个资源。</p><h3 id="2-Vary"><a href="#2-Vary" class="headerlink" title="2. Vary"></a>2. Vary</h3><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Vary: Accept-Language</span><br></pre></td></tr></table></figure><p>在使用内容协商的情况下，只有当缓存服务器中的缓存满足内容协商条件时，才能使用该缓存，否则应该向源服务器请求该资源。</p><p>例如，一个客户端发送了一个包含 Accept-Language 首部字段的请求之后，源服务器返回的响应包含 <code>Vary: Accept-Language</code> 内容，缓存服务器对这个响应进行缓存之后，在客户端下一次访问同一个 URL 资源，并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。</p><h2 id="内容编码"><a href="#内容编码" class="headerlink" title="内容编码"></a>内容编码</h2><p>内容编码将实体主体进行压缩，从而减少传输的数据量。</p><p>常用的内容编码有：gzip、compress、deflate、identity。</p><p>浏览器发送 Accept-Encoding 首部，其中包含有它所支持的压缩算法，以及各自的优先级。服务器则从中选择一种，使用该算法对响应的消息主体进行压缩，并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的，在响应的 Vary 首部至少要包含 Content-Encoding。</p><h2 id="范围请求"><a href="#范围请求" class="headerlink" title="范围请求"></a>范围请求</h2><p>如果网络出现中断，服务器只发送了一部分数据，范围请求可以使得客户端只请求服务器未发送的那部分数据，从而避免服务器重新发送所有数据。</p><h3 id="1-Range"><a href="#1-Range" class="headerlink" title="1. Range"></a>1. Range</h3><p>在请求报文中添加 Range 首部字段指定请求的范围。</p><figure class="highlight html"><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><span class="line">GET /z4d4kWk.jpg HTTP/1.1</span><br><span class="line">Host: i.imgur.com</span><br><span class="line">Range: bytes=0-1023</span><br></pre></td></tr></table></figure><p>请求成功的话服务器返回的响应包含 206 Partial Content 状态码。</p><figure class="highlight html"><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><span class="line">HTTP/1.1 206 Partial Content</span><br><span class="line">Content-Range: bytes 0-1023/146515</span><br><span class="line">Content-Length: 1024</span><br><span class="line">...</span><br><span class="line">(binary content)</span><br></pre></td></tr></table></figure><h3 id="2-Accept-Ranges"><a href="#2-Accept-Ranges" class="headerlink" title="2. Accept-Ranges"></a>2. Accept-Ranges</h3><p>响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求，可以处理使用 bytes，否则使用 none。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Accept-Ranges: bytes</span><br></pre></td></tr></table></figure><h3 id="3-响应状态码"><a href="#3-响应状态码" class="headerlink" title="3. 响应状态码"></a>3. 响应状态码</h3><ul><li>在请求成功的情况下，服务器会返回 206 Partial Content 状态码。</li><li>在请求的范围越界的情况下，服务器会返回 416 Requested Range Not Satisfiable 状态码。</li><li>在不支持范围请求的情况下，服务器会返回 200 OK 状态码。</li></ul><h2 id="分块传输编码"><a href="#分块传输编码" class="headerlink" title="分块传输编码"></a>分块传输编码</h2><p>Chunked Transfer Coding，可以把数据分割成多块，让浏览器逐步显示页面。</p><h2 id="多部分对象集合"><a href="#多部分对象集合" class="headerlink" title="多部分对象集合"></a>多部分对象集合</h2><p>一份报文主体内可含有多种类型的实体同时发送，每个部分之间用 boundary 字段定义的分隔符进行分隔，每个部分都可以有首部字段。</p><p>例如，上传多个表单时可以使用如下方式：</p><figure class="highlight html"><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><span class="line">Content-Type: multipart/form-data; boundary=AaB03x</span><br><span class="line"></span><br><span class="line">--AaB03x</span><br><span class="line">Content-Disposition: form-data; name=&quot;submit-name&quot;</span><br><span class="line"></span><br><span class="line">Larry</span><br><span class="line">--AaB03x</span><br><span class="line">Content-Disposition: form-data; name=&quot;files&quot;; filename=&quot;file1.txt&quot;</span><br><span class="line">Content-Type: text/plain</span><br><span class="line"></span><br><span class="line">... contents of file1.txt ...</span><br><span class="line">--AaB03x--</span><br></pre></td></tr></table></figure><h2 id="虚拟主机"><a href="#虚拟主机" class="headerlink" title="虚拟主机"></a>虚拟主机</h2><p>HTTP&#x2F;1.1 使用虚拟主机技术，使得一台服务器拥有多个域名，并且在逻辑上可以看成多个服务器。</p><h2 id="通信数据转发"><a href="#通信数据转发" class="headerlink" title="通信数据转发"></a>通信数据转发</h2><h3 id="1-代理"><a href="#1-代理" class="headerlink" title="1. 代理"></a>1. 代理</h3><p>代理服务器接受客户端的请求，并且转发给其它服务器。</p><p>使用代理的主要目的是：</p><ul><li>缓存</li><li>负载均衡</li><li>网络访问控制</li><li>访问日志记录</li></ul><p>代理服务器分为正向代理和反向代理两种：</p><ul><li>用户察觉得到正向代理的存在。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/a314bb79-5b18-4e63-a976-3448bffa6f1b.png" width=""/> </div><br><ul><li>而反向代理一般位于内部网络中，用户察觉不到。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/2d09a847-b854-439c-9198-b29c65810944.png" width=""/> </div><br><h3 id="2-网关"><a href="#2-网关" class="headerlink" title="2. 网关"></a>2. 网关</h3><p>与代理服务器不同的是，网关服务器会将 HTTP 转化为其它协议进行通信，从而请求其它非 HTTP 服务器的服务。</p><h3 id="3-隧道"><a href="#3-隧道" class="headerlink" title="3. 隧道"></a>3. 隧道</h3><p>使用 SSL 等加密手段，在客户端和服务器之间建立一条安全的通信线路。</p><h1 id="六、HTTPs"><a href="#六、HTTPs" class="headerlink" title="六、HTTPs"></a>六、HTTPs</h1><p>HTTP 有以下安全性问题：</p><ul><li>使用明文进行通信，内容可能会被窃听；</li><li>不验证通信方的身份，通信方的身份有可能遭遇伪装；</li><li>无法证明报文的完整性，报文有可能遭篡改。</li></ul><p>HTTPs 并不是新协议，而是让 HTTP 先和 SSL（Secure Sockets Layer）通信，再由 SSL 和 TCP 通信，也就是说 HTTPs 使用了隧道进行通信。</p><p>通过使用 SSL，HTTPs 具有了加密（防窃听）、认证（防伪装）和完整性保护（防篡改）。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/ssl-offloading.jpg" width="700"/> </div><br><h2 id="加密"><a href="#加密" class="headerlink" title="加密"></a>加密</h2><h3 id="1-对称密钥加密"><a href="#1-对称密钥加密" class="headerlink" title="1. 对称密钥加密"></a>1. 对称密钥加密</h3><p>对称密钥加密（Symmetric-Key Encryption），加密和解密使用同一密钥。</p><ul><li>优点：运算速度快；</li><li>缺点：无法安全地将密钥传输给通信方。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/7fffa4b8-b36d-471f-ad0c-a88ee763bb76.png" width="600"/> </div><br><h3 id="2-非对称密钥加密"><a href="#2-非对称密钥加密" class="headerlink" title="2.非对称密钥加密"></a>2.非对称密钥加密</h3><p>非对称密钥加密，又称公开密钥加密（Public-Key Encryption），加密和解密使用不同的密钥。</p><p>公开密钥所有人都可以获得，通信发送方获得接收方的公开密钥之后，就可以使用公开密钥进行加密，接收方收到通信内容后使用私有密钥解密。</p><p>非对称密钥除了用来加密，还可以用来进行签名。因为私有密钥无法被其他人获取，因此通信发送方使用其私有密钥进行签名，通信接收方使用发送方的公开密钥对签名进行解密，就能判断这个签名是否正确。</p><ul><li>优点：可以更安全地将公开密钥传输给通信发送方；</li><li>缺点：运算速度慢。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/39ccb299-ee99-4dd1-b8b4-2f9ec9495cb4.png" width="600"/> </div><br><h3 id="3-HTTPs-采用的加密方式"><a href="#3-HTTPs-采用的加密方式" class="headerlink" title="3. HTTPs 采用的加密方式"></a>3. HTTPs 采用的加密方式</h3><p>HTTPs 采用混合的加密机制，使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性，之后使用对称密钥加密进行通信来保证通信过程的效率。（下图中的 Session Key 就是对称密钥）</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/How-HTTPS-Works.png" width="600"/> </div><br><h2 id="认证"><a href="#认证" class="headerlink" title="认证"></a>认证</h2><p>通过使用  <strong>证书</strong>  来对通信方进行认证。</p><p>数字证书认证机构（CA，Certificate Authority）是客户端与服务器双方都可信赖的第三方机构。</p><p>服务器的运营人员向 CA 提出公开密钥的申请，CA 在判明提出申请者的身份之后，会对已申请的公开密钥做数字签名，然后分配这个已签名的公开密钥，并将该公开密钥放入公开密钥证书后绑定在一起。</p><p>进行 HTTPs 通信时，服务器会把证书发送给客户端。客户端取得其中的公开密钥之后，先使用数字签名进行验证，如果验证通过，就可以开始通信了。</p><p>通信开始时，客户端需要使用服务器的公开密钥将自己的私有密钥传输给服务器，之后再进行对称密钥加密。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/2017-06-11-ca.png" width=""/> </div><br><h2 id="完整性保护"><a href="#完整性保护" class="headerlink" title="完整性保护"></a>完整性保护</h2><p>SSL 提供报文摘要功能来进行完整性保护。</p><p>HTTP 也提供了 MD5 报文摘要功能，但不是安全的。例如报文内容被篡改之后，同时重新计算 MD5 的值，通信接收方是无法意识到发生了篡改。</p><p>HTTPs 的报文摘要功能之所以安全，是因为它结合了加密和认证这两个操作。试想一下，加密之后的报文，遭到篡改之后，也很难重新计算报文摘要，因为无法轻易获取明文。</p><h2 id="HTTPs-的缺点"><a href="#HTTPs-的缺点" class="headerlink" title="HTTPs 的缺点"></a>HTTPs 的缺点</h2><ul><li>因为需要进行加密解密等过程，因此速度会更慢；</li><li>需要支付证书授权的高额费用。</li></ul><h2 id="配置-HTTPs"><a href="#配置-HTTPs" class="headerlink" title="配置 HTTPs"></a>配置 HTTPs</h2><p><a href="https://aotu.io/notes/2016/08/16/nginx-https/index.html">Nginx 配置 HTTPS 服务器</a></p><h1 id="七、HTTP-2-0"><a href="#七、HTTP-2-0" class="headerlink" title="七、HTTP&#x2F;2.0"></a>七、HTTP&#x2F;2.0</h1><h2 id="HTTP-1-x-缺陷"><a href="#HTTP-1-x-缺陷" class="headerlink" title="HTTP&#x2F;1.x 缺陷"></a>HTTP&#x2F;1.x 缺陷</h2><p> HTTP&#x2F;1.x 实现简单是以牺牲性能为代价的：</p><ul><li>客户端需要使用多个连接才能实现并发和缩短延迟；</li><li>不会压缩请求和响应首部，从而导致不必要的网络流量；</li><li>不支持有效的资源优先级，致使底层 TCP 连接的利用率低下。</li></ul><h2 id="二进制分帧层"><a href="#二进制分帧层" class="headerlink" title="二进制分帧层"></a>二进制分帧层</h2><p>HTTP&#x2F;2.0 将报文分成 HEADERS 帧和 DATA 帧，它们都是二进制格式的。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/86e6a91d-a285-447a-9345-c5484b8d0c47.png" width="400"/> </div><br><p>在通信过程中，只会有一个 TCP 连接存在，它承载了任意数量的双向数据流（Stream）。</p><ul><li>一个数据流都有一个唯一标识符和可选的优先级信息，用于承载双向信息。</li><li>消息（Message）是与逻辑请求或响应消息对应的完整的一系列帧。</li><li>帧（Fram）是最小的通信单位，来自不同数据流的帧可以交错发送，然后再根据每个帧头的数据流标识符重新组装。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/af198da1-2480-4043-b07f-a3b91a88b815.png" width="600"/> </div><br><h2 id="服务端推送"><a href="#服务端推送" class="headerlink" title="服务端推送"></a>服务端推送</h2><p>HTTP&#x2F;2.0 在客户端请求一个资源时，会把相关的资源一起发送给客户端，客户端就不需要再次发起请求了。例如客户端请求 page.html 页面，服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/e3f1657c-80fc-4dfa-9643-bf51abd201c6.png" width="800"/> </div><br><h2 id="首部压缩"><a href="#首部压缩" class="headerlink" title="首部压缩"></a>首部压缩</h2><p>HTTP&#x2F;1.1 的首部带有大量信息，而且每次都要重复发送。</p><p>HTTP&#x2F;2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表，从而避免了重复传输。</p><p>不仅如此，HTTP&#x2F;2.0 也使用 Huffman 编码对首部字段进行压缩。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/_u4E0B_u8F7D.png" width="600"/> </div><br><h1 id="八、GET-和-POST-比较"><a href="#八、GET-和-POST-比较" class="headerlink" title="八、GET 和 POST 比较"></a>八、GET 和 POST 比较</h1><h2 id="作用"><a href="#作用" class="headerlink" title="作用"></a>作用</h2><p>GET 用于获取资源，而 POST 用于传输实体主体。</p><h2 id="参数"><a href="#参数" class="headerlink" title="参数"></a>参数</h2><p>GET 和 POST 的请求都能使用额外的参数，但是 GET 的参数是以查询字符串出现在 URL 中，而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高，因为照样可以通过一些抓包工具（Fiddler）查看。</p><p>因为 URL 只支持 ASCII 码，因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 <code>中文</code> 会转换为 <code>%E4%B8%AD%E6%96%87</code>，而空格会转换为 <code>%20</code>。POST 参考支持标准字符集。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET /test/demo_form.asp?name1=value1&amp;name2=value2 HTTP/1.1</span><br></pre></td></tr></table></figure><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><span class="line">POST /test/demo_form.asp HTTP/1.1</span><br><span class="line">Host: w3schools.com</span><br><span class="line">name1=value1&amp;name2=value2</span><br></pre></td></tr></table></figure><h2 id="安全"><a href="#安全" class="headerlink" title="安全"></a>安全</h2><p>安全的 HTTP 方法不会改变服务器状态，也就是说它只是可读的。</p><p>GET 方法是安全的，而 POST 却不是，因为 POST 的目的是传送实体主体内容，这个内容可能是用户上传的表单数据，上传成功之后，服务器可能把这个数据存储到数据库中，因此状态也就发生了改变。</p><p>安全的方法除了 GET 之外还有：HEAD、OPTIONS。</p><p>不安全的方法除了 POST 之外还有 PUT、DELETE。</p><h2 id="幂等性"><a href="#幂等性" class="headerlink" title="幂等性"></a>幂等性</h2><p>幂等的 HTTP 方法，同样的请求被执行一次与连续执行多次的效果是一样的，服务器的状态也是一样的。换句话说就是，幂等方法不应该具有副作用（统计用途除外）。</p><p>所有的安全方法也都是幂等的。</p><p>在正确实现的条件下，GET，HEAD，PUT 和 DELETE 等方法都是幂等的，而 POST 方法不是。</p><p>GET &#x2F;pageX HTTP&#x2F;1.1 是幂等的，连续调用多次，客户端接收到的结果都是一样的：</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><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GET /pageX HTTP/1.1</span><br><span class="line">GET /pageX HTTP/1.1</span><br><span class="line">GET /pageX HTTP/1.1</span><br><span class="line">GET /pageX HTTP/1.1</span><br></pre></td></tr></table></figure><p>POST &#x2F;add_row HTTP&#x2F;1.1 不是幂等的，如果调用多次，就会增加多行记录：</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><span class="line">POST /add_row HTTP/1.1   -&gt; Adds a 1nd row</span><br><span class="line">POST /add_row HTTP/1.1   -&gt; Adds a 2nd row</span><br><span class="line">POST /add_row HTTP/1.1   -&gt; Adds a 3rd row</span><br></pre></td></tr></table></figure><p>DELETE &#x2F;idX&#x2F;delete HTTP&#x2F;1.1 是幂等的，即便不同的请求接收到的状态码不一样：</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><span class="line">DELETE /idX/delete HTTP/1.1   -&gt; Returns 200 if idX exists</span><br><span class="line">DELETE /idX/delete HTTP/1.1   -&gt; Returns 404 as it just got deleted</span><br><span class="line">DELETE /idX/delete HTTP/1.1   -&gt; Returns 404</span><br></pre></td></tr></table></figure><h2 id="可缓存"><a href="#可缓存" class="headerlink" title="可缓存"></a>可缓存</h2><p>如果要对响应进行缓存，需要满足以下条件：</p><ul><li>请求报文的 HTTP 方法本身是可缓存的，包括 GET 和 HEAD，但是 PUT 和 DELETE 不可缓存，POST 在多数情况下不可缓存的。</li><li>响应报文的状态码是可缓存的，包括：200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。</li><li>响应报文的 Cache-Control 首部字段没有指定不进行缓存。</li></ul><h2 id="XMLHttpRequest"><a href="#XMLHttpRequest" class="headerlink" title="XMLHttpRequest"></a>XMLHttpRequest</h2><p>为了阐述 POST 和 GET 的另一个区别，需要先了解 XMLHttpRequest：</p><blockquote><p>XMLHttpRequest 是一个 API，它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式，并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。</p></blockquote><ul><li>在使用 XMLHttpRequest 的 POST 方法时，浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做，例如火狐就不会。</li><li>而 GET 方法 Header 和 Data 会一起发送。</li></ul><h1 id="九、HTTP-1-0-与-HTTP-1-1-的区别"><a href="#九、HTTP-1-0-与-HTTP-1-1-的区别" class="headerlink" title="九、HTTP&#x2F;1.0 与 HTTP&#x2F;1.1 的区别"></a>九、HTTP&#x2F;1.0 与 HTTP&#x2F;1.1 的区别</h1><blockquote><p>详细内容请见上文</p></blockquote><ul><li><p>HTTP&#x2F;1.1 默认是长连接</p></li><li><p>HTTP&#x2F;1.1 支持管线化处理</p></li><li><p>HTTP&#x2F;1.1 支持同时打开多个 TCP 连接</p></li><li><p>HTTP&#x2F;1.1 支持虚拟主机</p></li><li><p>HTTP&#x2F;1.1 新增状态码 100</p></li><li><p>HTTP&#x2F;1.1 支持分块传输编码</p></li><li><p>HTTP&#x2F;1.1 新增缓存处理指令 max-age</p></li></ul><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li>上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014.</li><li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP">MDN : HTTP</a></li><li><a href="https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn">HTTP&#x2F;2 简介</a></li><li><a href="http://php.net/manual/zh/function.htmlspecialchars.php">htmlspecialchars</a></li><li><a href="http://java2db.com/java-io/how-to-get-and-the-difference-between-file-uri-and-url-in-java">Difference between file URI and URL in java</a></li><li><a href="https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement">How to Fix SQL Injection Using Java PreparedStatement &amp; CallableStatement</a></li><li><a href="https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html">浅谈 HTTP 中 Get 与 Post 的区别</a></li><li><a href="https://www.webdancers.com/are-http-and-www-necesary/">Are http:&#x2F;&#x2F; and www really necessary?</a></li><li><a href="https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html">HTTP (HyperText Transfer Protocol)</a></li><li><a href="https://www.igvita.com/2011/12/01/web-vpn-secure-proxies-with-spdy-chrome/">Web-VPN: Secure Proxies with SPDY &amp; Chrome</a></li><li><a href="http://en.wikipedia.org/wiki/File:HTTP_persistent_connection.svg">File:HTTP persistent connection.svg</a></li><li><a href="https://en.wikipedia.org/wiki/Proxy_server">Proxy server</a></li><li><a href="https://www.x-cart.com/blog/what-is-https-and-ssl.html">What Is This HTTPS&#x2F;SSL Thing And Why Should You Care?</a></li><li><a href="https://securebox.comodo.com/ssl-sniffing/ssl-offloading/">What is SSL Offloading?</a></li><li><a href="https://docs.oracle.com/cd/E19424-01/820-4811/6ng8i26bn/index.html">Sun Directory Server Enterprise Edition 7.0 Reference - Key Encryption</a></li><li><a href="https://www.codeproject.com/Articles/326574/An-Introduction-to-Mutual-SSL-Authentication">An Introduction to Mutual SSL Authentication</a></li><li><a href="https://danielmiessler.com/study/url-uri/">The Difference Between URLs and URIs</a></li><li><a href="https://juejin.im/entry/5766c29d6be3ff006a31b84e#comment">Cookie 与 Session 的区别</a></li><li><a href="https://www.zhihu.com/question/19786827">COOKIE 和 SESSION 有什么区别</a></li><li><a href="https://harttle.land/2015/08/10/cookie-session.html">Cookie&#x2F;Session 的机制与安全</a></li><li><a href="https://shijianan.com/2017/06/11/https/">HTTPS 证书原理</a></li><li><a href="https://stackoverflow.com/questions/176264/what-is-the-difference-between-a-uri-a-url-and-a-urn">What is the difference between a URI, a URL and a URN?</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest">XMLHttpRequest</a></li><li><a href="https://blog.josephscott.org/2009/08/27/xmlhttprequest-xhr-uses-multiple-packets-for-http-post/">XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?</a></li><li><a href="https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences">Symmetric vs. Asymmetric Encryption – What are differences?</a></li><li><a href="https://www.kancloud.cn/digest/web-performance-http2">Web 性能优化与 HTTP&#x2F;2</a></li><li><a href="https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn">HTTP&#x2F;2 简介</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本文转载自 &lt;a href=&quot;https://github.com/CyC2018/CS-Notes&quot;&gt;CS-Note&lt;/a&gt; 项目&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&quot;一-、基础概念&quot;&gt;&lt;a href=&quot;#一-、基础概念&quot; cla</summary>
      
    
    
    
    <category term="转载" scheme="https://blog.jugg.xyz/categories/%E8%BD%AC%E8%BD%BD/"/>
    
    
    <category term="HTTP" scheme="https://blog.jugg.xyz/tags/HTTP/"/>
    
  </entry>
  
  <entry>
    <title>计算机网络基础知识</title>
    <link href="https://blog.jugg.xyz/2018/04/17/repost/Computer-Network/"/>
    <id>https://blog.jugg.xyz/2018/04/17/repost/Computer-Network/</id>
    <published>2018-04-17T14:29:20.000Z</published>
    <updated>2026-05-02T11:24:14.333Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文转载自 <a href="https://github.com/CyC2018/CS-Notes">CS-Note</a> 项目</p></blockquote><h1 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h1><h2 id="网络的网络"><a href="#网络的网络" class="headerlink" title="网络的网络"></a>网络的网络</h2><p>网络把主机连接起来，而互联网是把多种不同的网络连接起来，因此互联网是网络的网络。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/network-of-networks.gif" width="400"/> </div><br><h2 id="ISP"><a href="#ISP" class="headerlink" title="ISP"></a>ISP</h2><p>互联网服务提供商 ISP 可以从互联网管理机构获得许多 IP 地址，同时拥有通信线路以及路由器等联网设备，个人或机构向 ISP 缴纳一定的费用就可以接入互联网。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/46cec213-3048-4a80-aded-fdd577542801.jpg" width="500"/> </div><br><p>目前的互联网是一种多层次 ISP 结构，ISP 根据覆盖面积的大小分为第一层 ISP、区域 ISP 和接入 ISP。互联网交换点 IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/168e893c-e4a0-4ba4-b81f-9d993483abd0.jpg" width="500"/> </div><br><h2 id="主机之间的通信方式"><a href="#主机之间的通信方式" class="headerlink" title="主机之间的通信方式"></a>主机之间的通信方式</h2><ul><li><p>客户-服务器（C&#x2F;S）：客户是服务的请求方，服务器是服务的提供方。</p></li><li><p>对等（P2P）：不区分客户和服务器。</p></li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/2ad244f5-939c-49fa-9385-69bc688677ab.jpg" width=""/> </div><br><h2 id="电路交换与分组交换"><a href="#电路交换与分组交换" class="headerlink" title="电路交换与分组交换"></a>电路交换与分组交换</h2><h3 id="1-电路交换"><a href="#1-电路交换" class="headerlink" title="1. 电路交换"></a>1. 电路交换</h3><p>电路交换用于电话通信系统，两个用户要通信之前需要建立一条专用的物理链路，并且在整个通信过程中始终占用该链路。由于通信的过程中不可能一直在使用传输线路，因此电路交换对线路的利用率很低，往往不到 10%。</p><h3 id="2-分组交换"><a href="#2-分组交换" class="headerlink" title="2. 分组交换"></a>2. 分组交换</h3><p>每个分组都有首部和尾部，包含了源地址和目的地址等控制信息，在同一个传输线路上同时传输多个分组互相不会影响，因此在同一条传输线路上允许同时传输多个分组，也就是说分组交换不需要占用传输线路。</p><p>在一个邮局通信系统中，邮局收到一份邮件之后，先存储下来，然后把相同目的地的邮件一起转发到下一个目的地，这个过程就是存储转发过程，分组交换也使用了存储转发过程。</p><h2 id="时延"><a href="#时延" class="headerlink" title="时延"></a>时延</h2><p>总时延 &#x3D; 传输时延 + 传播时延 + 处理时延 + 排队时延</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/3939369b-3a4a-48a0-b9eb-3efae26dd400.png" width="800"/> </div><br><h3 id="1-传输时延"><a href="#1-传输时延" class="headerlink" title="1. 传输时延"></a>1. 传输时延</h3><p>主机或路由器传输数据帧所需要的时间。</p><div align="center"><img src="https://latex.codecogs.com/gif.latex?delay=\frac{l(bit)}{v(bit/s)}"/></div> <br><p>其中 l 表示数据帧的长度，v 表示传输速率。</p><h3 id="2-传播时延"><a href="#2-传播时延" class="headerlink" title="2. 传播时延"></a>2. 传播时延</h3><p>电磁波在信道中传播所需要花费的时间，电磁波传播的速度接近光速。</p><div align="center"><img src="https://latex.codecogs.com/gif.latex?delay=\frac{l(m)}{v(m/s)}"/></div> <br><p>其中 l 表示信道长度，v 表示电磁波在信道上的传播速度。</p><h3 id="3-处理时延"><a href="#3-处理时延" class="headerlink" title="3. 处理时延"></a>3. 处理时延</h3><p>主机或路由器收到分组时进行处理所需要的时间，例如分析首部、从分组中提取数据、进行差错检验或查找适当的路由等。</p><h3 id="4-排队时延"><a href="#4-排队时延" class="headerlink" title="4. 排队时延"></a>4. 排队时延</h3><p>分组在路由器的输入队列和输出队列中排队等待的时间，取决于网络当前的通信量。</p><h2 id="计算机网络体系结构"><a href="#计算机网络体系结构" class="headerlink" title="计算机网络体系结构*"></a>计算机网络体系结构*</h2><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/426df589-6f97-4622-b74d-4a81fcb1da8e.png" width="600"/> </div><br><h3 id="1-五层协议"><a href="#1-五层协议" class="headerlink" title="1. 五层协议"></a>1. 五层协议</h3><ul><li><p><strong>应用层</strong> ：为特定应用程序提供数据传输服务，例如 HTTP、DNS 等。数据单位为报文。</p></li><li><p><strong>运输层</strong> ：提供的是进程间的通用数据传输服务。由于应用层协议很多，定义通用的运输层协议就可以支持不断增多的应用层协议。运输层包括两种协议：传输控制协议 TCP，提供面向连接、可靠的数据传输服务，数据单位为报文段；用户数据报协议 UDP，提供无连接、尽最大努力的数据传输服务，数据单位为用户数据报。TCP 主要提供完整性服务，UDP 主要提供及时性服务。</p></li><li><p><strong>网络层</strong> ：为主机间提供数据传输服务，而运输层协议是为主机中的进程提供服务。网络层把运输层传递下来的报文段或者用户数据报封装成分组。</p></li><li><p><strong>数据链路层</strong> ：网络层针对的还是主机之间的数据传输服务，而主机之间可以有很多链路，链路层协议就是为同一链路的主机提供服务。数据链路层把网络层传下来的分组封装成帧。</p></li><li><p><strong>物理层</strong> ：考虑的是怎样在传输媒体上传输数据比特流，而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异，使数据链路层感觉不到这些差异。</p></li></ul><h3 id="2-OSI"><a href="#2-OSI" class="headerlink" title="2. OSI"></a>2. OSI</h3><p>其中表示层和会话层用途如下：</p><ul><li><p><strong>表示层</strong> ：数据压缩、加密以及数据描述，这使得应用程序不必关心在各台主机中数据内部格式不同的问题。</p></li><li><p><strong>会话层</strong> ：建立及管理会话。</p></li></ul><p>五层协议没有表示层和会话层，而是将这些功能留给应用程序开发者处理。</p><h3 id="3-TCP-IP"><a href="#3-TCP-IP" class="headerlink" title="3. TCP&#x2F;IP"></a>3. TCP&#x2F;IP</h3><p>它只有四层，相当于五层协议中数据链路层和物理层合并为网络接口层。</p><p>TCP&#x2F;IP 体系结构不严格遵循 OSI 分层概念，应用层可能会直接使用 IP 层或者网络接口层。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/45e0e0bf-386d-4280-a341-a0b9496c7674.png" width="400"/> </div><br><p>TCP&#x2F;IP 协议族是一种沙漏形状，中间小两边大，IP 协议在其中占用举足轻重的地位。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/d4eef1e2-5703-4ca4-82ab-8dda93d6b81f.png" width="500"/> </div><br><h3 id="4-数据在各层之间的传递过程"><a href="#4-数据在各层之间的传递过程" class="headerlink" title="4. 数据在各层之间的传递过程"></a>4. 数据在各层之间的传递过程</h3><p>在向下的过程中，需要添加下层协议所需要的首部或者尾部，而在向上的过程中不断拆开首部和尾部。</p><p>路由器只有下面三层协议，因为路由器位于网络核心中，不需要为进程或者应用程序提供服务，因此也就不需要运输层和应用层。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/ac106e7e-489a-4082-abd9-dabebe48394c.jpg" width="800"/> </div><br><h1 id="二、物理层"><a href="#二、物理层" class="headerlink" title="二、物理层"></a>二、物理层</h1><h2 id="通信方式"><a href="#通信方式" class="headerlink" title="通信方式"></a>通信方式</h2><p>根据信息在传输线上的传送方向，分为以下三种通信方式：</p><ul><li>单工通信：单向传输</li><li>半双工通信：双向交替传输</li><li>全双工通信：双向同时传输</li></ul><h2 id="带通调制"><a href="#带通调制" class="headerlink" title="带通调制"></a>带通调制</h2><p>模拟信号是连续的信号，数字信号是离散的信号。带通调制把数字信号转换为模拟信号。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/f0a31c04-6e26-408c-8395-88f4e2ae928b.jpg"/> </div><br><h1 id="三、数据链路层"><a href="#三、数据链路层" class="headerlink" title="三、数据链路层"></a>三、数据链路层</h1><h2 id="基本问题"><a href="#基本问题" class="headerlink" title="基本问题"></a>基本问题</h2><h3 id="1-封装成帧"><a href="#1-封装成帧" class="headerlink" title="1. 封装成帧"></a>1. 封装成帧</h3><p>将网络层传下来的分组添加首部和尾部，用于标记帧的开始和结束。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/ea5f3efe-d5e6-499b-b278-9e898af61257.jpg" width="500"/> </div><br><h3 id="2-透明传输"><a href="#2-透明传输" class="headerlink" title="2. 透明传输"></a>2. 透明传输</h3><p>透明表示一个实际存在的事物看起来好像不存在一样。</p><p>帧使用首部和尾部进行定界，如果帧的数据部分含有和首部尾部相同的内容，那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符。如果数据部分出现转义字符，那么就在转义字符前面再加个转义字符。在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符，用户察觉不到转义字符的存在。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/c5022dd3-be22-4250-b9f6-38ae984a04d7.jpg" width="600"/> </div><br><h3 id="3-差错检测"><a href="#3-差错检测" class="headerlink" title="3. 差错检测"></a>3. 差错检测</h3><p>目前数据链路层广泛使用了循环冗余检验（CRC）来检查比特差错。</p><h2 id="信道分类"><a href="#信道分类" class="headerlink" title="信道分类"></a>信道分类</h2><h3 id="1-广播信道"><a href="#1-广播信道" class="headerlink" title="1. 广播信道"></a>1. 广播信道</h3><p>一对多通信，一个节点发送的数据能够被广播信道上所有的节点接收到。</p><p>所有的节点都在同一个广播信道上发送数据，因此需要有专门的控制方法进行协调，避免发生冲突（冲突也叫碰撞）。</p><p>主要有两种控制方法进行协调，一个是使用信道复用技术，一是使用 CSMA&#x2F;CD 协议。</p><h3 id="2-点对点信道"><a href="#2-点对点信道" class="headerlink" title="2. 点对点信道"></a>2. 点对点信道</h3><p>一对一通信。</p><p>因为不会发生碰撞，因此也比较简单，使用 PPP 协议进行控制。</p><h2 id="信道复用技术"><a href="#信道复用技术" class="headerlink" title="信道复用技术"></a>信道复用技术</h2><h3 id="1-频分复用"><a href="#1-频分复用" class="headerlink" title="1. 频分复用"></a>1. 频分复用</h3><p>频分复用的所有主机在相同的时间占用不同的频率带宽资源。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/c4c14368-519c-4a0e-8331-0a553715e3e7.jpg"/> </div><br><h3 id="2-时分复用"><a href="#2-时分复用" class="headerlink" title="2. 时分复用"></a>2. 时分复用</h3><p>时分复用的所有主机在不同的时间占用相同的频率带宽资源。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/fa2273c3-1b5f-48ce-8e8b-441a4116c1c4.jpg"/> </div><br><p>使用频分复用和时分复用进行通信，在通信的过程中主机会一直占用一部分信道资源。但是由于计算机数据的突发性质，通信过程没必要一直占用信道资源而不让出给其它用户使用，因此这两种方式对信道的利用率都不高。</p><h3 id="3-统计时分复用"><a href="#3-统计时分复用" class="headerlink" title="3. 统计时分复用"></a>3. 统计时分复用</h3><p>是对时分复用的一种改进，不固定每个用户在时分复用帧中的位置，只要有数据就集中起来组成统计时分复用帧然后发送。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/5999e5de-7c16-4b52-b3aa-6dc7b58c7894.png" width="700"/> </div><br><h3 id="4-波分复用"><a href="#4-波分复用" class="headerlink" title="4. 波分复用"></a>4. 波分复用</h3><p>光的频分复用。由于光的频率很高，因此习惯上用波长而不是频率来表示所使用的光载波。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/21041ec2-babb-483f-bf47-8b8148eec162.png" width="700"/> </div><br><h3 id="5-码分复用"><a href="#5-码分复用" class="headerlink" title="5. 码分复用"></a>5. 码分复用</h3><p>为每个用户分配 m bit 的码片，并且所有的码片正交，对于任意两个码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 和 <img src="https://latex.codecogs.com/gif.latex?\vec{T}"/> 有</p><div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{T}=0"/></div> <br><p>为了讨论方便，取 m&#x3D;8，设码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片，发送比特 0 时就发送该码片的反码 11100100。</p><p>在计算时将 00011011 记作 (-1 -1 -1 +1 +1 -1 +1 +1)，可以得到</p><div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{S}=1"/></div> <br><div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{S'}=-1"/></div> <br><p>其中 <img src="https://latex.codecogs.com/gif.latex?\vec{S'}"/> 为 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 的反码。</p><p>利用上面的式子我们知道，当接收端使用码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 对接收到的数据进行内积运算时，结果为 0 的是其它用户发送的数据，结果为 1 的是用户发送的比特 1，结果为 -1 的是用户发送的比特 0。</p><p>码分复用需要发送的数据量为原先的 m 倍。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/92ad9bae-7d02-43ba-8115-a9d6f530ca28.png" width="600"/> </div><br><h2 id="CSMA-CD-协议"><a href="#CSMA-CD-协议" class="headerlink" title="CSMA&#x2F;CD 协议*"></a>CSMA&#x2F;CD 协议*</h2><p>CSMA&#x2F;CD 表示载波监听多点接入 &#x2F; 碰撞检测。</p><ul><li><strong>多点接入</strong> ：说明这是总线型网络，许多主机以多点的方式连接到总线上。</li><li><strong>载波监听</strong> ：每个主机都必须不停地监听信道。在发送前，如果监听到信道正在使用，就必须等待。</li><li><strong>碰撞检测</strong> ：在发送中，如果监听到信道已有其它主机正在发送数据，就表示发生了碰撞。虽然每个主机在发送数据之前都已经监听到信道为空闲，但是由于电磁波的传播时延的存在，还是有可能会发生碰撞。</li></ul><p>记端到端的传播时延为 τ，最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞，称 2τ 为  <strong>争用期</strong> 。只有经过争用期之后还没有检测到碰撞，才能肯定这次发送不会发生碰撞。</p><p>当发生碰撞时，站点要停止发送，等待一段时间再发送。这个时间采用  <strong>截断二进制指数退避算法</strong>  来确定。从离散的整数集合 {0, 1, .., (2<sup>k</sup>-1)} 中随机取出一个数，记作 r，然后取 r 倍的争用期作为重传等待时间。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/5aa82b89-f266-44da-887d-18f31f01d8ef.png" width="600"/> </div><br><h2 id="PPP-协议"><a href="#PPP-协议" class="headerlink" title="PPP 协议"></a>PPP 协议</h2><p>互联网用户通常需要连接到某个 ISP 之后才能接入到互联网，PPP 协议是用户计算机和 ISP 进行通信时所使用的数据链路层协议。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/ddcf2327-8d84-425d-8535-121a94bcb88d.jpg" width="600"/> </div><br><p>PPP 的帧格式：</p><ul><li>F 字段为帧的定界符</li><li>A 和 C 字段暂时没有意义</li><li>FCS 字段是使用 CRC 的检验序列</li><li>信息部分的长度不超过 1500</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/69f16984-a66f-4288-82e4-79b4aa43e835.jpg" width="500"/> </div><br><h2 id="MAC-地址"><a href="#MAC-地址" class="headerlink" title="MAC 地址"></a>MAC 地址</h2><p>MAC 地址是链路层地址，长度为 6 字节（48 位），用于唯一标识网络适配器（网卡）。</p><p>一台主机拥有多少个适配器就有多少个 MAC 地址。例如笔记本电脑普遍存在无线网络适配器和有线网络适配器，因此就有两个 MAC 地址。</p><h2 id="局域网"><a href="#局域网" class="headerlink" title="局域网"></a>局域网</h2><p>局域网是一种典型的广播信道，主要特点是网络为一个单位所拥有，且地理范围和站点数目均有限。</p><p>主要有以太网、令牌环网、FDDI 和 ATM 等局域网技术，目前以太网占领着有线局域网市场。</p><p>可以按照网络拓扑结构对局域网进行分类：</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/a6026bb4-3daf-439f-b1ec-a5a24e19d2fb.jpg" width="600"/> </div><br><h2 id="以太网"><a href="#以太网" class="headerlink" title="以太网*"></a>以太网*</h2><p>以太网是一种星型拓扑结构局域网。</p><p>早期使用集线器进行连接，集线器是一种物理层设备，作用于比特而不是帧，当一个比特到达接口时，集线器重新生成这个比特，并将其能量强度放大，从而扩大网络的传输距离，之后再将这个比特发送到其它所有接口。如果集线器同时收到同时从两个不同接口的帧，那么就发生了碰撞。</p><p>目前以太网使用交换机替代了集线器，交换机是一种链路层设备，它不会发生碰撞，能根据 MAC 地址进行存储转发。</p><p>以太网帧格式：</p><ul><li><strong>类型</strong> ：标记上层使用的协议；</li><li><strong>数据</strong> ：长度在 46-1500 之间，如果太小则需要填充；</li><li><strong>FCS</strong> ：帧检验序列，使用的是 CRC 检验方法；</li><li><strong>前同步码</strong> ：只是为了计算 FCS 临时加入的，计算结束之后会丢弃。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/50d38e84-238f-4081-8876-14ef6d7938b5.jpg" width="600"/> </div><br><h2 id="交换机"><a href="#交换机" class="headerlink" title="交换机*"></a>交换机*</h2><p>交换机具有自学习能力，学习的是交换表的内容，交换表中存储着 MAC 地址到接口的映射。</p><p>正是由于这种自学习能力，因此交换机是一种即插即用设备，不需要网络管理员手动配置交换表内容。</p><p>下图中，交换机有 4 个接口，主机 A 向主机 B 发送数据帧时，交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B，先查交换表，此时没有主机 B 的表项，那么主机 A 就发送广播帧，主机 C 和主机 D 会丢弃该帧。主机 B 收下之后，查找交换表得到主机 A 映射的接口为 1，就发送数据帧到接口 1，同时交换机添加主机 B 到接口 3 的映射。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/c9cfcd20-c901-435f-9a07-3e46830c359f.jpg" width="800"/> </div><br><h2 id="虚拟局域网"><a href="#虚拟局域网" class="headerlink" title="虚拟局域网"></a>虚拟局域网</h2><p>虚拟局域网可以建立与物理位置无关的逻辑组，只有在同一个虚拟局域网中的成员才会收到链路层广播信息。</p><p>例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网，A1 发送的广播会被 A2、A3、A4 收到，而其它站点收不到。</p><p>使用 VLAN 干线连接来建立虚拟局域网，每台交换机上的一个特殊接口被设置为干线接口，以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q，它在标准以太网帧上加进了 4 字节首部 VLAN 标签，用于表示该帧属于哪一个虚拟局域网。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/a74b70ac-323a-4b31-b4d5-90569b8a944b.png" width="500"/> </div><br><h1 id="四、网络层"><a href="#四、网络层" class="headerlink" title="四、网络层*"></a>四、网络层*</h1><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>因为网络层是整个互联网的核心，因此应当让网络层尽可能简单。网络层向上只提供简单灵活的、无连接的、尽最大努力交互的数据报服务。</p><p>使用 IP 协议，可以把异构的物理网络连接起来，使得在网络层看起来好像是一个统一的网络。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/7b038838-c75b-4538-ae84-6299386704e5.jpg" width="500"/> </div><br><p>与 IP 协议配套使用的还有三个协议：</p><ul><li>地址解析协议 ARP（Address Resolution Protocol）</li><li>网际控制报文协议 ICMP（Internet Control Message Protocol）</li><li>网际组管理协议 IGMP（Internet Group Management Protocol）</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/0a9f4125-b6ab-4e94-a807-fd7070ae726a.png" width="350"/> </div><br><h2 id="IP-数据报格式"><a href="#IP-数据报格式" class="headerlink" title="IP 数据报格式"></a>IP 数据报格式</h2><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/85c05fb1-5546-4c50-9221-21f231cdc8c5.jpg" width="700"/> </div><br><ul><li><p><strong>版本</strong>  : 有 4（IPv4）和 6（IPv6）两个值；</p></li><li><p><strong>首部长度</strong>  : 占 4 位，因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度，也就是 4 字节。因为首部固定长度为 20 字节，因此该值最小为 5。如果可选字段的长度不是 4 字节的整数倍，就用尾部的填充部分来填充。</p></li><li><p><strong>区分服务</strong>  : 用来获得更好的服务，一般情况下不使用。</p></li><li><p><strong>总长度</strong>  : 包括首部长度和数据部分长度。</p></li><li><p><strong>生存时间</strong>  ：TTL，它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位，当 TTL 为 0 时就丢弃数据报。</p></li><li><p><strong>协议</strong> ：指出携带的数据应该上交给哪个协议进行处理，例如 ICMP、TCP、UDP 等。</p></li><li><p><strong>首部检验和</strong> ：因为数据报每经过一个路由器，都要重新计算检验和，因此检验和不包含数据部分可以减少计算的工作量。</p></li><li><p><strong>标识</strong>  : 在数据报长度过长从而发生分片的情况下，相同数据报的不同分片具有相同的标识符。</p></li><li><p><strong>片偏移</strong>  : 和标识符一起，用于发生分片的情况。片偏移的单位为 8 字节。</p></li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/23ba890e-e11c-45e2-a20c-64d217f83430.png" width="700"/> </div><br><h2 id="IP-地址编址方式"><a href="#IP-地址编址方式" class="headerlink" title="IP 地址编址方式"></a>IP 地址编址方式</h2><p>IP 地址的编址方式经历了三个历史阶段：</p><ul><li>分类</li><li>子网划分</li><li>无分类</li></ul><h3 id="1-分类"><a href="#1-分类" class="headerlink" title="1. 分类"></a>1. 分类</h3><p>由两部分组成，网络号和主机号，其中不同分类具有不同的网络号长度，并且是固定的。</p><p>IP 地址 ::&#x3D; {&lt; 网络号 &gt;, &lt; 主机号 &gt;}</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/cbf50eb8-22b4-4528-a2e7-d187143d57f7.png" width="500"/> </div><br><h3 id="2-子网划分"><a href="#2-子网划分" class="headerlink" title="2. 子网划分"></a>2. 子网划分</h3><p>通过在主机号字段中拿一部分作为子网号，把两级 IP 地址划分为三级 IP 地址。</p><p>IP 地址 ::&#x3D; {&lt; 网络号 &gt;, &lt; 子网号 &gt;, &lt; 主机号 &gt;}</p><p>要使用子网，必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0，如果 B 类地址的子网占两个比特，那么子网掩码为 11111111 11111111 11000000 00000000，也就是 255.255.192.0。</p><p>注意，外部网络看不到子网的存在。</p><h3 id="3-无分类"><a href="#3-无分类" class="headerlink" title="3. 无分类"></a>3. 无分类</h3><p>无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念，使用网络前缀和主机号来对 IP 地址进行编码，网络前缀的长度可以根据需要变化。</p><p>IP 地址 ::&#x3D; {&lt; 网络前缀号 &gt;, &lt; 主机号 &gt;}</p><p>CIDR 的记法上采用在 IP 地址后面加上网络前缀长度的方法，例如 128.14.35.7&#x2F;20 表示前 20 位为网络前缀。</p><p>CIDR 的地址掩码可以继续称为子网掩码，子网掩码首 1 长度为网络前缀的长度。</p><p>一个 CIDR 地址块中有很多地址，一个 CIDR 表示的网络就可以表示原来的很多个网络，并且在路由表中只需要一个路由就可以代替原来的多个路由，减少了路由表项的数量。把这种通过使用网络前缀来减少路由表项的方式称为路由聚合，也称为  <strong>构成超网</strong> 。</p><p>在路由表中的项目由“网络前缀”和“下一跳地址”组成，在查找时可能会得到不止一个匹配结果，应当采用最长前缀匹配来确定应该匹配哪一个。</p><h2 id="地址解析协议-ARP"><a href="#地址解析协议-ARP" class="headerlink" title="地址解析协议 ARP"></a>地址解析协议 ARP</h2><p>网络层实现主机之间的通信，而链路层实现具体每段链路之间的通信。因此在通信过程中，IP 数据报的源地址和目的地址始终不变，而 MAC 地址随着链路的改变而改变。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/66192382-558b-4b05-a35d-ac4a2b1a9811.jpg" width="700"/> </div><br><p>ARP 实现由 IP 地址得到 MAC 地址。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/b9d79a5a-e7af-499b-b989-f10483e71b8b.jpg" width="500"/> </div><br><p>每个主机都有一个 ARP 高速缓存，里面有本局域网上的各主机和路由器的 IP 地址到 MAC 地址的映射表。</p><p>如果主机 A 知道主机 B 的 IP 地址，但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射，此时主机 A 通过广播的方式发送 ARP 请求分组，主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址，随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/8006a450-6c2f-498c-a928-c927f758b1d0.png" width="700"/> </div><br><h2 id="网际控制报文协议-ICMP"><a href="#网际控制报文协议-ICMP" class="headerlink" title="网际控制报文协议 ICMP"></a>网际控制报文协议 ICMP</h2><p>ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中，但是不属于高层协议。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/e3124763-f75e-46c3-ba82-341e6c98d862.jpg" width="500"/> </div><br><p>ICMP 报文分为差错报告报文和询问报文。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/aa29cc88-7256-4399-8c7f-3cf4a6489559.png" width="600"/> </div><br><h3 id="1-Ping"><a href="#1-Ping" class="headerlink" title="1. Ping"></a>1. Ping</h3><p>Ping 是 ICMP 的一个重要应用，主要用来测试两台主机之间的连通性。</p><p>Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文，目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。</p><h3 id="2-Traceroute"><a href="#2-Traceroute" class="headerlink" title="2. Traceroute"></a>2. Traceroute</h3><p>Traceroute 是 ICMP 的另一个应用，用来跟踪一个分组从源点到终点的路径。</p><p>Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报，并由目的主机发送终点不可达差错报告报文。</p><ul><li>源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1，当 P1 到达路径上的第一个路由器 R1 时，R1 收下它并把 TTL 减 1，此时 TTL 等于 0，R1 就把 P1 丢弃，并向源主机发送一个 ICMP 时间超过差错报告报文；</li><li>源主机接着发送第二个数据报 P2，并把 TTL 设置为 2。P2 先到达 R1，R1 收下后把 TTL 减 1 再转发给 R2，R2 收下后也把 TTL 减 1，由于此时 TTL 等于 0，R2 就丢弃 P2，并向源主机发送一个 ICMP 时间超过差错报文。</li><li>不断执行这样的步骤，直到最后一个数据报刚刚到达目的主机，主机不转发数据报，也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP，因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。</li><li>之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。</li></ul><h2 id="虚拟专用网-VPN"><a href="#虚拟专用网-VPN" class="headerlink" title="虚拟专用网 VPN"></a>虚拟专用网 VPN</h2><p>由于 IP 地址的紧缺，一个机构能申请到的 IP 地址数往往远小于本机构所拥有的主机数。并且一个机构并不需要把所有的主机接入到外部的互联网中，机构内的计算机可以使用仅在本机构有效的 IP 地址（专用地址）。</p><p>有三个专用地址块：</p><ul><li>10.0.0.0 ~ 10.255.255.255</li><li>172.16.0.0 ~ 172.31.255.255</li><li>192.168.0.0 ~ 192.168.255.255</li></ul><p>VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信；虚拟指“好像是”，而实际上并不是，它有经过公用的互联网。</p><p>下图中，场所 A 和 B 的通信经过互联网，如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信，IP 数据报的源地址是 10.1.0.1，目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1，R1 对内部数据进行加密，然后重新加上数据报的首部，源地址是路由器 R1 的全球地址 125.1.2.3，目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密，恢复原来的数据报，此时目的地址为 10.2.0.3，就交付给 Y。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/1556770b-8c01-4681-af10-46f1df69202c.jpg" width="800"/> </div><br><h2 id="网络地址转换-NAT"><a href="#网络地址转换-NAT" class="headerlink" title="网络地址转换 NAT"></a>网络地址转换 NAT</h2><p>专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时，可以使用 NAT 来将本地 IP 转换为全球 IP。</p><p>在以前，NAT 将本地 IP 和全球 IP 一一对应，这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址，现在常用的 NAT 转换表把运输层的端口号也用上了，使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/2719067e-b299-4639-9065-bed6729dbf0b.png" width=""/> </div><br><h2 id="路由器的结构"><a href="#路由器的结构" class="headerlink" title="路由器的结构"></a>路由器的结构</h2><p>路由器从功能上可以划分为：路由选择和分组转发。</p><p>分组转发结构由三个部分组成：交换结构、一组输入端口和一组输出端口。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/c3369072-c740-43b0-b276-202bd1d3960d.jpg" width="600"/> </div><br><h2 id="路由器分组转发流程"><a href="#路由器分组转发流程" class="headerlink" title="路由器分组转发流程"></a>路由器分组转发流程</h2><ul><li>从数据报的首部提取目的主机的 IP 地址 D，得到目的网络地址 N。</li><li>若 N 就是与此路由器直接相连的某个网络地址，则进行直接交付；</li><li>若路由表中有目的地址为 D 的特定主机路由，则把数据报传送给表中所指明的下一跳路由器；</li><li>若路由表中有到达网络 N 的路由，则把数据报传送给路由表中所指明的下一跳路由器；</li><li>若路由表中有一个默认路由，则把数据报传送给路由表中所指明的默认路由器；</li><li>报告转发分组出错。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/1ab49e39-012b-4383-8284-26570987e3c4.jpg" width="800"/> </div><br><h2 id="路由选择协议"><a href="#路由选择协议" class="headerlink" title="路由选择协议"></a>路由选择协议</h2><p>路由选择协议都是自适应的，能随着网络通信量和拓扑结构的变化而自适应地进行调整。</p><p>互联网可以划分为许多较小的自治系统 AS，一个 AS 可以使用一种和别的 AS 不同的路由选择协议。</p><p>可以把路由选择协议划分为两大类：</p><ul><li>自治系统内部的路由选择：RIP 和 OSPF</li><li>自治系统间的路由选择：BGP</li></ul><h3 id="1-内部网关协议-RIP"><a href="#1-内部网关协议-RIP" class="headerlink" title="1. 内部网关协议 RIP"></a>1. 内部网关协议 RIP</h3><p>RIP 是一种基于距离向量的路由选择协议。距离是指跳数，直接相连的路由器跳数为 1。跳数最多为 15，超过 15 表示不可达。</p><p>RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表，经过若干次交换之后，所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。</p><p>距离向量算法：</p><ul><li>对地址为 X 的相邻路由器发来的 RIP 报文，先修改报文中的所有项目，把下一跳字段中的地址改为 X，并把所有的距离字段加 1；</li><li>对修改后的 RIP 报文中的每一个项目，进行以下步骤：</li><li>若原来的路由表中没有目的网络 N，则把该项目添加到路由表中；</li><li>否则：若下一跳路由器地址是 X，则把收到的项目替换原来路由表中的项目；否则：若收到的项目中的距离 d 小于路由表中的距离，则进行更新（例如原始路由表项为 Net2, 5, P，新表项为 Net2, 4, X，则更新）；否则什么也不做。</li><li>若 3 分钟还没有收到相邻路由器的更新路由表，则把该相邻路由器标为不可达，即把距离置为 16。</li></ul><p>RIP 协议实现简单，开销小。但是 RIP 能使用的最大距离为 15，限制了网络的规模。并且当网络出现故障时，要经过比较长的时间才能将此消息传送到所有路由器。</p><h3 id="2-内部网关协议-OSPF"><a href="#2-内部网关协议-OSPF" class="headerlink" title="2. 内部网关协议 OSPF"></a>2. 内部网关协议 OSPF</h3><p>开放最短路径优先 OSPF，是为了克服 RIP 的缺点而开发出来的。</p><p>开放表示 OSPF 不受某一家厂商控制，而是公开发表的；最短路径优先表示使用了 Dijkstra 提出的最短路径算法 SPF。</p><p>OSPF 具有以下特点：</p><ul><li>向本自治系统中的所有路由器发送信息，这种方法是洪泛法。</li><li>发送的信息就是与相邻路由器的链路状态，链路状态包括与哪些路由器相连以及链路的度量，度量用费用、距离、时延、带宽等来表示。</li><li>只有当链路状态发生变化时，路由器才会发送信息。</li></ul><p>所有路由器都具有全网的拓扑结构图，并且是一致的。相比于 RIP，OSPF 的更新过程收敛的很快。</p><h3 id="3-外部网关协议-BGP"><a href="#3-外部网关协议-BGP" class="headerlink" title="3. 外部网关协议 BGP"></a>3. 外部网关协议 BGP</h3><p>BGP（Border Gateway Protocol，边界网关协议）</p><p>AS 之间的路由选择很困难，主要是由于：</p><ul><li>互联网规模很大；</li><li>各个 AS 内部使用不同的路由选择协议，无法准确定义路径的度量；</li><li>AS 之间的路由选择必须考虑有关的策略，比如有些 AS 不愿意让其它 AS 经过。</li></ul><p>BGP 只能寻找一条比较好的路由，而不是最佳路由。</p><p>每个 AS 都必须配置 BGP 发言人，通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/9cd0ae20-4fb5-4017-a000-f7d3a0eb3529.png" width="600"/> </div><br><h1 id="五、运输层"><a href="#五、运输层" class="headerlink" title="五、运输层*"></a>五、运输层*</h1><p>网络层只把分组发送到目的主机，但是真正通信的并不是主机而是主机中的进程。运输层提供了进程间的逻辑通信，运输层向高层用户屏蔽了下面网络层的核心细节，使应用程序看起来像是在两个运输层实体之间有一条端到端的逻辑通信信道。</p><h2 id="UDP-和-TCP-的特点"><a href="#UDP-和-TCP-的特点" class="headerlink" title="UDP 和 TCP 的特点"></a>UDP 和 TCP 的特点</h2><ul><li><p>用户数据报协议 UDP（User Datagram Protocol）是无连接的，尽最大可能交付，没有拥塞控制，面向报文（对于应用程序传下来的报文不合并也不拆分，只是添加 UDP 首部），支持一对一、一对多、多对一和多对多的交互通信。</p></li><li><p>传输控制协议 TCP（Transmission Control Protocol）是面向连接的，提供可靠交付，有流量控制，拥塞控制，提供全双工通信，面向字节流（把应用层传下来的报文看成字节流，把字节流组织成大小不等的数据块），每一条 TCP 连接只能是点对点的（一对一）。</p></li></ul><h2 id="UDP-首部格式"><a href="#UDP-首部格式" class="headerlink" title="UDP 首部格式"></a>UDP 首部格式</h2><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/d4c3a4a1-0846-46ec-9cc3-eaddfca71254.jpg" width="600"/> </div><br><p>首部字段只有 8 个字节，包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。</p><h2 id="TCP-首部格式"><a href="#TCP-首部格式" class="headerlink" title="TCP 首部格式"></a>TCP 首部格式</h2><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/55dc4e84-573d-4c13-a765-52ed1dd251f9.png" width="700"/> </div><br><ul><li><p><strong>序号</strong>  ：用于对字节流进行编号，例如序号为 301，表示第一个字节的编号为 301，如果携带的数据长度为 100 字节，那么下一个报文段的序号应为 401。</p></li><li><p><strong>确认号</strong>  ：期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段，序号为 501，携带的数据长度为 200 字节，因此 B 期望下一个报文段的序号为 701，B 发送给 A 的确认报文段中确认号就为 701。</p></li><li><p><strong>数据偏移</strong>  ：指的是数据部分距离报文段起始处的偏移量，实际上指的是首部的长度。</p></li><li><p><strong>确认 ACK</strong>  ：当 ACK&#x3D;1 时确认号字段有效，否则无效。TCP 规定，在连接建立后所有传送的报文段都必须把 ACK 置 1。</p></li><li><p><strong>同步 SYN</strong>  ：在连接建立时用来同步序号。当 SYN&#x3D;1，ACK&#x3D;0 时表示这是一个连接请求报文段。若对方同意建立连接，则响应报文中 SYN&#x3D;1，ACK&#x3D;1。</p></li><li><p><strong>终止 FIN</strong>  ：用来释放一个连接，当 FIN&#x3D;1 时，表示此报文段的发送方的数据已发送完毕，并要求释放连接。</p></li><li><p><strong>窗口</strong>  ：窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制，是因为接收方的数据缓存空间是有限的。</p></li></ul><h2 id="TCP-的三次握手"><a href="#TCP-的三次握手" class="headerlink" title="TCP 的三次握手"></a>TCP 的三次握手</h2><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/e92d0ebc-7d46-413b-aec1-34a39602f787.png" width="600"/> </div><br><p>假设 A 为客户端，B 为服务器端。</p><ul><li><p>首先 B 处于 LISTEN（监听）状态，等待客户的连接请求。</p></li><li><p>A 向 B 发送连接请求报文，SYN&#x3D;1，ACK&#x3D;0，选择一个初始的序号 x。</p></li><li><p>B 收到连接请求报文，如果同意建立连接，则向 A 发送连接确认报文，SYN&#x3D;1，ACK&#x3D;1，确认号为 x+1，同时也选择一个初始的序号 y。</p></li><li><p>A 收到 B 的连接确认报文后，还要向 B 发出确认，确认号为 y+1，序号为 x+1。</p></li><li><p>B 收到 A 的确认后，连接建立。</p></li></ul><p><strong>三次握手的原因</strong> </p><p>第三次握手是为了防止失效的连接请求到达服务器，让服务器错误打开连接。</p><p>客户端发送的连接请求如果在网络中滞留，那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后，就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器，如果不进行三次握手，那么服务器就会打开两个连接。如果有第三次握手，客户端会忽略服务器之后发送的对滞留连接请求的连接确认，不进行第三次握手，因此就不会再次打开连接。</p><h2 id="TCP-的四次挥手"><a href="#TCP-的四次挥手" class="headerlink" title="TCP 的四次挥手"></a>TCP 的四次挥手</h2><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/f87afe72-c2df-4c12-ac03-9b8d581a8af8.jpg" width="600"/> </div><br><p>以下描述不讨论序号和确认号，因为序号和确认号的规则比较简单。并且不讨论 ACK，因为 ACK 在连接建立之后都为 1。</p><ul><li><p>A 发送连接释放报文，FIN&#x3D;1。</p></li><li><p>B 收到之后发出确认，此时 TCP 属于半关闭状态，B 能向 A 发送数据但是 A 不能向 B 发送数据。</p></li><li><p>当 B 不再需要连接时，发送连接释放报文，FIN&#x3D;1。</p></li><li><p>A 收到后发出确认，进入 TIME-WAIT 状态，等待 2 MSL（最大报文存活时间）后释放连接。</p></li><li><p>B 收到 A 的确认后释放连接。</p></li></ul><p><strong>四次挥手的原因</strong> </p><p>客户端发送了 FIN 连接释放报文之后，服务器收到了这个报文，就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据，传送完毕之后，服务器会发送 FIN 连接释放报文。</p><p><strong>TIME_WAIT</strong> </p><p>客户端接收到服务器端的 FIN 报文后进入此状态，此时并不是直接进入 CLOSED 状态，还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由：</p><ul><li><p>确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文，那么就会重新发送连接释放请求报文，A 等待一段时间就是为了处理这种情况的发生。</p></li><li><p>等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失，使得下一个新的连接不会出现旧的连接请求报文。</p></li></ul><h2 id="TCP-可靠传输"><a href="#TCP-可靠传输" class="headerlink" title="TCP 可靠传输"></a>TCP 可靠传输</h2><p>TCP 使用超时重传来实现可靠传输：如果一个已经发送的报文段在超时时间内没有收到确认，那么就重传这个报文段。</p><p>一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT，加权平均往返时间 RTTs 计算如下：</p><div align="center"><img src="https://latex.codecogs.com/gif.latex?RTTs=(1-a)*(RTTs)+a*RTT"/></div> <br><p>超时时间 RTO 应该略大于 RTTs，TCP 使用的超时时间计算如下：</p><div align="center"><img src="https://latex.codecogs.com/gif.latex?RTO=RTTs+4*RTT_d"/></div> <br><p>其中 RTT<sub>d</sub> 为偏差。</p><h2 id="TCP-滑动窗口"><a href="#TCP-滑动窗口" class="headerlink" title="TCP 滑动窗口"></a>TCP 滑动窗口</h2><p>窗口是缓存的一部分，用来暂时存放字节流。发送方和接收方各有一个窗口，接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小，发送方根据这个值和其它信息设置自己的窗口大小。</p><p>发送窗口内的字节都允许被发送，接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认，那么就将发送窗口向右滑动一定距离，直到左部第一个字节不是已发送并且已确认的状态；接收窗口的滑动类似，接收窗口左部字节已经发送确认并交付主机，就向右滑动接收窗口。</p><p>接收窗口只会对窗口内最后一个按序到达的字节进行确认，例如接收窗口已经收到的字节为 {31, 34, 35}，其中 {31} 按序到达，而 {34, 35} 就不是，因此只对字节 31 进行确认。发送方得到一个字节的确认之后，就知道这个字节之前的所有字节都已经被接收。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/a3253deb-8d21-40a1-aae4-7d178e4aa319.jpg" width="800"/> </div><br><h2 id="TCP-流量控制"><a href="#TCP-流量控制" class="headerlink" title="TCP 流量控制"></a>TCP 流量控制</h2><p>流量控制是为了控制发送方发送速率，保证接收方来得及接收。</p><p>接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小，从而影响发送方的发送速率。将窗口字段设置为 0，则发送方不能发送数据。</p><h2 id="TCP-拥塞控制"><a href="#TCP-拥塞控制" class="headerlink" title="TCP 拥塞控制"></a>TCP 拥塞控制</h2><p>如果网络出现拥塞，分组将会丢失，此时发送方会继续重传，从而导致网络拥塞程度更高。因此当出现拥塞时，应当控制发送方的速率。这一点和流量控制很像，但是出发点不同。流量控制是为了让接收方能来得及接收，而拥塞控制是为了降低整个网络的拥塞程度。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/51e2ed95-65b8-4ae9-8af3-65602d452a25.jpg" width="500"/> </div><br><p>TCP 主要通过四个算法来进行拥塞控制：慢开始、拥塞避免、快重传、快恢复。</p><p>发送方需要维护一个叫做拥塞窗口（cwnd）的状态变量，注意拥塞窗口与发送方窗口的区别：拥塞窗口只是一个状态变量，实际决定发送方能发送多少数据的是发送方窗口。</p><p>为了便于讨论，做如下假设：</p><ul><li>接收方有足够大的接收缓存，因此不会发生流量控制；</li><li>虽然 TCP 的窗口基于字节，但是这里设窗口的大小单位为报文段。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/910f613f-514f-4534-87dd-9b4699d59d31.png" width="800"/> </div><br><h3 id="1-慢开始与拥塞避免"><a href="#1-慢开始与拥塞避免" class="headerlink" title="1. 慢开始与拥塞避免"></a>1. 慢开始与拥塞避免</h3><p>发送的最初执行慢开始，令 cwnd &#x3D; 1，发送方只能发送 1 个报文段；当收到确认后，将 cwnd 加倍，因此之后发送方能够发送的报文段数量为：2、4、8 …</p><p>注意到慢开始每个轮次都将 cwnd 加倍，这样会让 cwnd 增长速度非常快，从而使得发送方发送的速度增长速度过快，网络拥塞的可能性也就更高。设置一个慢开始门限 ssthresh，当 cwnd &gt;&#x3D; ssthresh 时，进入拥塞避免，每个轮次只将 cwnd 加 1。</p><p>如果出现了超时，则令 ssthresh &#x3D; cwnd &#x2F; 2，然后重新执行慢开始。</p><h3 id="2-快重传与快恢复"><a href="#2-快重传与快恢复" class="headerlink" title="2. 快重传与快恢复"></a>2. 快重传与快恢复</h3><p>在接收方，要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M<sub>1</sub> 和 M<sub>2</sub>，此时收到 M<sub>4</sub>，应当发送对 M<sub>2</sub> 的确认。</p><p>在发送方，如果收到三个重复确认，那么可以知道下一个报文段丢失，此时执行快重传，立即重传下一个报文段。例如收到三个 M<sub>2</sub>，则 M<sub>3</sub> 丢失，立即重传 M<sub>3</sub>。</p><p>在这种情况下，只是丢失个别报文段，而不是网络拥塞。因此执行快恢复，令 ssthresh &#x3D; cwnd &#x2F; 2 ，cwnd &#x3D; ssthresh，注意到此时直接进入拥塞避免。</p><p>慢开始和快恢复的快慢指的是 cwnd 的设定值，而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1，而快恢复 cwnd 设定为 ssthresh。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/f61b5419-c94a-4df1-8d4d-aed9ae8cc6d5.png" width="600"/> </div><br><h1 id="六、应用层"><a href="#六、应用层" class="headerlink" title="六、应用层"></a>六、应用层</h1><h2 id="域名系统"><a href="#域名系统" class="headerlink" title="域名系统"></a>域名系统</h2><p>DNS 是一个分布式数据库，提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指，每个站点只保留它自己的那部分数据。</p><p>域名具有层次结构，从上到下依次为：根域名、顶级域名、二级域名。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/b54eeb16-0b0e-484c-be62-306f57c40d77.jpg"/> </div><br><p>DNS 可以使用 UDP 或者 TCP 进行传输，使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输，这就要求域名解析器和域名服务器都必须自己处理超时和重传来保证可靠性。在两种情况下会使用 TCP 进行传输：</p><ul><li>如果返回的响应超过的 512 字节（UDP 最大只支持 512 字节的数据）。</li><li>区域传送（区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据）。</li></ul><h2 id="文件传送协议"><a href="#文件传送协议" class="headerlink" title="文件传送协议"></a>文件传送协议</h2><p>FTP 使用 TCP 进行连接，它需要两个连接来传送一个文件：</p><ul><li>控制连接：服务器打开端口号 21 等待客户端的连接，客户端主动建立连接后，使用这个连接将客户端的命令传送给服务器，并传回服务器的应答。</li><li>数据连接：用来传送一个文件数据。</li></ul><p>根据数据连接是否是服务器端主动建立，FTP 有主动和被动两种模式：</p><ul><li>主动模式：服务器端主动建立数据连接，其中服务器端的端口号为 20，客户端的端口号随机，但是必须大于 1024，因为 0~1023 是熟知端口号。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/03f47940-3843-4b51-9e42-5dcaff44858b.jpg"/> </div><br><ul><li>被动模式：客户端主动建立数据连接，其中客户端的端口号由客户端自己指定，服务器端的端口号随机。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/be5c2c61-86d2-4dba-a289-b48ea23219de.jpg"/> </div><br><p>主动模式要求客户端开放端口号给服务器端，需要去配置客户端的防火墙。被动模式只需要服务器端开放端口号即可，无需客户端配置防火墙。但是被动模式会导致服务器端的安全性减弱，因为开放了过多的端口号。</p><h2 id="动态主机配置协议"><a href="#动态主机配置协议" class="headerlink" title="动态主机配置协议"></a>动态主机配置协议</h2><p>DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式，用户不再需要去手动配置 IP 地址等信息。</p><p>DHCP 配置的内容不仅是 IP 地址，还包括子网掩码、网关 IP 地址。</p><p>DHCP 工作过程如下：</p><ol><li>客户端发送 Discover 报文，该报文的目的地址为 255.255.255.255:67，源地址为 0.0.0.0:68，被放入 UDP 中，该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网，就需要使用中继代理。</li><li>DHCP 服务器收到 Discover 报文之后，发送 Offer 报文给客户端，该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息，因此客户端需要进行选择。</li><li>如果客户端选择了某个 DHCP 服务器提供的信息，那么就发送 Request 报文给该 DHCP 服务器。</li><li>DHCP 服务器发送 Ack 报文，表示客户端此时可以使用提供给它的信息。</li></ol><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/bf16c541-0717-473b-b75d-4115864f4fbf.jpg"/> </div><br><h2 id="远程登录协议"><a href="#远程登录协议" class="headerlink" title="远程登录协议"></a>远程登录协议</h2><p>TELNET 用于登录到远程主机上，并且远程主机上的输出也会返回。</p><p>TELNET 可以适应许多计算机和操作系统的差异，例如不同操作系统系统的换行符定义。</p><h2 id="电子邮件协议"><a href="#电子邮件协议" class="headerlink" title="电子邮件协议"></a>电子邮件协议</h2><p>一个电子邮件系统由三部分组成：用户代理、邮件服务器以及邮件协议。</p><p>邮件协议包含发送协议和读取协议，发送协议常用 SMTP，读取协议常用 POP3 和 IMAP。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/7b3efa99-d306-4982-8cfb-e7153c33aab4.png" width="700"/> </div><br><h3 id="1-SMTP"><a href="#1-SMTP" class="headerlink" title="1. SMTP"></a>1. SMTP</h3><p>SMTP 只能发送 ASCII 码，而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP，而是增加邮件主体的结构，定义了非 ASCII 码的编码规则。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/ed5522bb-3a60-481c-8654-43e7195a48fe.png" width=""/> </div><br><h3 id="2-POP3"><a href="#2-POP3" class="headerlink" title="2. POP3"></a>2. POP3</h3><p>POP3 的特点是只要用户从服务器上读取了邮件，就把该邮件删除。</p><h3 id="3-IMAP"><a href="#3-IMAP" class="headerlink" title="3. IMAP"></a>3. IMAP</h3><p>IMAP 协议中客户端和服务器上的邮件保持同步，如果不手动删除邮件，那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。</p><h2 id="常用端口"><a href="#常用端口" class="headerlink" title="常用端口"></a>常用端口</h2><table><thead><tr><th align="center">应用</th><th align="center">应用层协议</th><th align="center">端口号</th><th align="center">运输层协议</th><th align="center">备注</th></tr></thead><tbody><tr><td align="center">域名解析</td><td align="center">DNS</td><td align="center">53</td><td align="center">UDP&#x2F;TCP</td><td align="center">长度超过 512 字节时使用 TCP</td></tr><tr><td align="center">动态主机配置协议</td><td align="center">DHCP</td><td align="center">67&#x2F;68</td><td align="center">UDP</td><td align="center"></td></tr><tr><td align="center">简单网络管理协议</td><td align="center">SNMP</td><td align="center">161&#x2F;162</td><td align="center">UDP</td><td align="center"></td></tr><tr><td align="center">文件传送协议</td><td align="center">FTP</td><td align="center">20&#x2F;21</td><td align="center">TCP</td><td align="center">控制连接 21，数据连接 20</td></tr><tr><td align="center">远程终端协议</td><td align="center">TELNET</td><td align="center">23</td><td align="center">TCP</td><td align="center"></td></tr><tr><td align="center">超文本传送协议</td><td align="center">HTTP</td><td align="center">80</td><td align="center">TCP</td><td align="center"></td></tr><tr><td align="center">简单邮件传送协议</td><td align="center">SMTP</td><td align="center">25</td><td align="center">TCP</td><td align="center"></td></tr><tr><td align="center">邮件读取协议</td><td align="center">POP3</td><td align="center">110</td><td align="center">TCP</td><td align="center"></td></tr><tr><td align="center">网际报文存取协议</td><td align="center">IMAP</td><td align="center">143</td><td align="center">TCP</td><td align="center"></td></tr></tbody></table><h2 id="Web-页面请求过程"><a href="#Web-页面请求过程" class="headerlink" title="Web 页面请求过程"></a>Web 页面请求过程</h2><h3 id="1-DHCP-配置主机信息"><a href="#1-DHCP-配置主机信息" class="headerlink" title="1. DHCP 配置主机信息"></a>1. DHCP 配置主机信息</h3><ul><li><p>假设主机最开始没有 IP 地址以及其它信息，那么就需要先使用 DHCP 来获取。</p></li><li><p>主机生成一个 DHCP 请求报文，并将这个报文放入具有目的端口 67 和源端口 68 的 UDP 报文段中。</p></li><li><p>该报文段则被放入在一个具有广播 IP 目的地址(255.255.255.255) 和源 IP 地址（0.0.0.0）的 IP 数据报中。</p></li><li><p>该数据报则被放置在 MAC 帧中，该帧具有目的地址 FF:FF:FF:FF:FF:FF，将广播到与交换机连接的所有设备。</p></li><li><p>连接在交换机的 DHCP 服务器收到广播帧之后，不断地向上分解得到 IP 数据报、UDP 报文段、DHCP 请求报文，之后生成 DHCP ACK 报文，该报文包含以下信息：IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码。该报文被放入 UDP 报文段中，UDP 报文段有被放入 IP 数据报中，最后放入 MAC 帧中。</p></li><li><p>该帧的目的地址是请求主机的 MAC 地址，因为交换机具有自学习能力，之前主机发送了广播帧之后就记录了 MAC 地址到其转发接口的交换表项，因此现在交换机就可以直接知道应该向哪个接口发送该帧。</p></li><li><p>主机收到该帧后，不断分解得到 DHCP 报文。之后就配置它的 IP 地址、子网掩码和 DNS 服务器的 IP 地址，并在其 IP 转发表中安装默认网关。</p></li></ul><h3 id="2-ARP-解析-MAC-地址"><a href="#2-ARP-解析-MAC-地址" class="headerlink" title="2. ARP 解析 MAC 地址"></a>2. ARP 解析 MAC 地址</h3><ul><li><p>主机通过浏览器生成一个 TCP 套接字，套接字向 HTTP 服务器发送 HTTP 请求。为了生成该套接字，主机需要知道网站的域名对应的 IP 地址。</p></li><li><p>主机生成一个 DNS 查询报文，该报文具有 53 号端口，因为 DNS 服务器的端口号是 53。</p></li><li><p>该 DNS 查询报文被放入目的地址为 DNS 服务器 IP 地址的 IP 数据报中。</p></li><li><p>该 IP 数据报被放入一个以太网帧中，该帧将发送到网关路由器。</p></li><li><p>DHCP 过程只知道网关路由器的 IP 地址，为了获取网关路由器的 MAC 地址，需要使用 ARP 协议。</p></li><li><p>主机生成一个包含目的地址为网关路由器 IP 地址的 ARP 查询报文，将该 ARP 查询报文放入一个具有广播目的地址（FF:FF:FF:FF:FF:FF）的以太网帧中，并向交换机发送该以太网帧，交换机将该帧转发给所有的连接设备，包括网关路由器。</p></li><li><p>网关路由器接收到该帧后，不断向上分解得到 ARP 报文，发现其中的 IP 地址与其接口的 IP 地址匹配，因此就发送一个 ARP 回答报文，包含了它的 MAC 地址，发回给主机。</p></li></ul><h3 id="3-DNS-解析域名"><a href="#3-DNS-解析域名" class="headerlink" title="3. DNS 解析域名"></a>3. DNS 解析域名</h3><ul><li><p>知道了网关路由器的 MAC 地址之后，就可以继续 DNS 的解析过程了。</p></li><li><p>网关路由器接收到包含 DNS 查询报文的以太网帧后，抽取出 IP 数据报，并根据转发表决定该 IP 数据报应该转发的路由器。</p></li><li><p>因为路由器具有内部网关协议（RIP、OSPF）和外部网关协议（BGP）这两种路由选择协议，因此路由表中已经配置了网关路由器到达 DNS 服务器的路由表项。</p></li><li><p>到达 DNS 服务器之后，DNS 服务器抽取出 DNS 查询报文，并在 DNS 数据库中查找待解析的域名。</p></li><li><p>找到 DNS 记录之后，发送 DNS 回答报文，将该回答报文放入 UDP 报文段中，然后放入 IP 数据报中，通过路由器反向转发回网关路由器，并经过以太网交换机到达主机。</p></li></ul><h3 id="4-HTTP-请求页面"><a href="#4-HTTP-请求页面" class="headerlink" title="4. HTTP 请求页面"></a>4. HTTP 请求页面</h3><ul><li><p>有了 HTTP 服务器的 IP 地址之后，主机就能够生成 TCP 套接字，该套接字将用于向 Web 服务器发送 HTTP GET 报文。</p></li><li><p>在生成 TCP 套接字之前，必须先与 HTTP 服务器进行三次握手来建立连接。生成一个具有目的端口 80 的 TCP SYN 报文段，并向 HTTP 服务器发送该报文段。</p></li><li><p>HTTP 服务器收到该报文段之后，生成 TCP SYN ACK 报文段，发回给主机。</p></li><li><p>连接建立之后，浏览器生成 HTTP GET 报文，并交付给 HTTP 服务器。</p></li><li><p>HTTP 服务器从 TCP 套接字读取 HTTP GET 报文，生成一个 HTTP 响应报文，将 Web 页面内容放入报文主体中，发回给主机。</p></li><li><p>浏览器收到 HTTP 响应报文后，抽取出 Web 页面内容，之后进行渲染，显示 Web 页面。</p></li></ul><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li>计算机网络, 谢希仁</li><li>JamesF.Kurose, KeithW.Ross, 库罗斯, 等. 计算机网络: 自顶向下方法 [M]. 机械工业出版社, 2014.</li><li>W.RichardStevens. TCP&#x2F;IP 详解. 卷 1, 协议 [M]. 机械工业出版社, 2006.</li><li><a href="https://securitywing.com/active-vs-passive-ftp-mode/">Active vs Passive FTP Mode: Which One is More Secure?</a></li><li><a href="http://www.serv-u.com/kb/1138/active-and-passive-ftp-transfers-defined">Active and Passive FTP Transfers Defined - KB Article #1138</a></li><li><a href="https://zh.wikipedia.org/wiki/Traceroute">Traceroute</a></li><li><a href="https://zh.wikipedia.org/wiki/Ping">ping</a></li><li><a href="http://webcache.googleusercontent.com/search?q=cache:http://anandgiria.blogspot.com/2013/09/windows-dhcp-interview-questions-and.html">How DHCP works and DHCP Interview Questions and Answers</a></li><li><a href="https://www.quora.com/What-is-process-of-DORA-in-DHCP">What is process of DORA in DHCP?</a></li><li><a href="https://tecadmin.net/what-is-dhcp-server/">What is DHCP Server ?</a></li><li><a href="http://www.climatechangenews.com/2011/html/university-tokyo.html">Tackling emissions targets in Tokyo</a></li><li><a href="http://www.climatechangenews.com/2011/html/university-tokyo.html">What does my ISP know when I use Tor?</a></li><li><a href="http://www.linyibin.cn/2017/02/12/technology-ComputerNetworking-Internet/">Technology-Computer Networking[1]-Computer Networks and the Internet</a></li><li><a href="http://slidesplayer.com/slide/11616167/">P2P 网络概述.</a></li><li><a href="http://slideplayer.com/slide/5115386/">Circuit Switching (a) Circuit switching. (b) Packet switching.</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本文转载自 &lt;a href=&quot;https://github.com/CyC2018/CS-Notes&quot;&gt;CS-Note&lt;/a&gt; 项目&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&quot;一、概述&quot;&gt;&lt;a href=&quot;#一、概述&quot; class=&quot;he</summary>
      
    
    
    
    <category term="转载" scheme="https://blog.jugg.xyz/categories/%E8%BD%AC%E8%BD%BD/"/>
    
    
    <category term="数据通信" scheme="https://blog.jugg.xyz/tags/%E6%95%B0%E6%8D%AE%E9%80%9A%E4%BF%A1/"/>
    
    <category term="网络协议" scheme="https://blog.jugg.xyz/tags/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/"/>
    
  </entry>
  
  <entry>
    <title>计算机操作系统基础知识</title>
    <link href="https://blog.jugg.xyz/2018/04/06/repost/Operating-System/"/>
    <id>https://blog.jugg.xyz/2018/04/06/repost/Operating-System/</id>
    <published>2018-04-06T10:30:02.000Z</published>
    <updated>2026-05-02T11:24:14.334Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文转载自 <a href="https://github.com/CyC2018/CS-Notes">CS-Note</a> 项目</p></blockquote><h1 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h1><h2 id="基本特征"><a href="#基本特征" class="headerlink" title="基本特征"></a>基本特征</h2><h3 id="1-并发"><a href="#1-并发" class="headerlink" title="1. 并发"></a>1. 并发</h3><p>并发是指宏观上在一段时间内能同时运行多个程序，而并行则指同一时刻能运行多个指令。</p><p>并行需要硬件支持，如多流水线或者多处理器。</p><p>操作系统通过引入进程和线程，使得程序能够并发运行。</p><h3 id="2-共享"><a href="#2-共享" class="headerlink" title="2. 共享"></a>2. 共享</h3><p>共享是指系统中的资源可以被多个并发进程共同使用。</p><p>有两种共享方式：互斥共享和同时共享。</p><p>互斥共享的资源称为临界资源，例如打印机等，在同一时间只允许一个进程访问，需要用同步机制来实现对临界资源的访问。</p><h3 id="3-虚拟"><a href="#3-虚拟" class="headerlink" title="3. 虚拟"></a>3. 虚拟</h3><p>虚拟技术把一个物理实体转换为多个逻辑实体。</p><p>主要有两种虚拟技术：时分复用技术和空分复用技术。</p><p>多个进程能在同一个处理器上并发执行使用了时分复用技术，让每个进程轮流占有处理器，每次只执行一小个时间片并快速切换。</p><p>虚拟内存使用了空分复用技术，它将物理内存抽象为地址空间，每个进程都有各自的地址空间。地址空间和物理内存使用页进行交换，地址空间的页并不需要全部在物理内存中，当使用到一个没有在物理内存的页时，执行页面置换算法，将该页置换到内存中。</p><h3 id="4-异步"><a href="#4-异步" class="headerlink" title="4. 异步"></a>4. 异步</h3><p>异步指进程不是一次性执行完毕，而是走走停停，以不可知的速度向前推进。</p><h2 id="基本功能"><a href="#基本功能" class="headerlink" title="基本功能"></a>基本功能</h2><h3 id="1-进程管理"><a href="#1-进程管理" class="headerlink" title="1. 进程管理"></a>1. 进程管理</h3><p>进程控制、进程同步、进程通信、死锁处理、处理机调度等。</p><h3 id="2-内存管理"><a href="#2-内存管理" class="headerlink" title="2. 内存管理"></a>2. 内存管理</h3><p>内存分配、地址映射、内存保护与共享、虚拟内存等。</p><h3 id="3-文件管理"><a href="#3-文件管理" class="headerlink" title="3. 文件管理"></a>3. 文件管理</h3><p>文件存储空间的管理、目录管理、文件读写管理和保护等。</p><h3 id="4-设备管理"><a href="#4-设备管理" class="headerlink" title="4. 设备管理"></a>4. 设备管理</h3><p>完成用户的 I&#x2F;O 请求，方便用户使用各种设备，并提高设备的利用率。</p><p>主要包括缓冲管理、设备分配、设备处理、虛拟设备等。</p><h2 id="系统调用"><a href="#系统调用" class="headerlink" title="系统调用"></a>系统调用</h2><p>如果一个进程在用户态需要使用内核态的功能，就进行系统调用从而陷入内核，由操作系统代为完成。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/tGPV0.png" width="600"/> </div><br><p>Linux 的系统调用主要有以下这些：</p><table><thead><tr><th align="center">Task</th><th>Commands</th></tr></thead><tbody><tr><td align="center">进程控制</td><td>fork(); exit(); wait();</td></tr><tr><td align="center">进程通信</td><td>pipe(); shmget(); mmap();</td></tr><tr><td align="center">文件操作</td><td>open(); read(); write();</td></tr><tr><td align="center">设备操作</td><td>ioctl(); read(); write();</td></tr><tr><td align="center">信息维护</td><td>getpid(); alarm(); sleep();</td></tr><tr><td align="center">安全</td><td>chmod(); umask(); chown();</td></tr></tbody></table><h2 id="大内核和微内核"><a href="#大内核和微内核" class="headerlink" title="大内核和微内核"></a>大内核和微内核</h2><h3 id="1-大内核"><a href="#1-大内核" class="headerlink" title="1. 大内核"></a>1. 大内核</h3><p>大内核是将操作系统功能作为一个紧密结合的整体放到内核。</p><p>由于各模块共享信息，因此有很高的性能。</p><h3 id="2-微内核"><a href="#2-微内核" class="headerlink" title="2. 微内核"></a>2. 微内核</h3><p>由于操作系统不断复杂，因此将一部分操作系统功能移出内核，从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务，相互独立。</p><p>在微内核结构下，操作系统被划分成小的、定义良好的模块，只有微内核这一个模块运行在内核态，其余模块运行在用户态。</p><p>因为需要频繁地在用户态和核心态之间进行切换，所以会有一定的性能损失。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/2_14_microkernelArchitecture.jpg"/> </div><br><h2 id="中断分类"><a href="#中断分类" class="headerlink" title="中断分类"></a>中断分类</h2><h3 id="1-外中断"><a href="#1-外中断" class="headerlink" title="1. 外中断"></a>1. 外中断</h3><p>由 CPU 执行指令以外的事件引起，如 I&#x2F;O 完成中断，表示设备输入&#x2F;输出处理已经完成，处理器能够发送下一个输入&#x2F;输出请求。此外还有时钟中断、控制台中断等。</p><h3 id="2-异常"><a href="#2-异常" class="headerlink" title="2. 异常"></a>2. 异常</h3><p>由 CPU 执行指令的内部事件引起，如非法操作码、地址越界、算术溢出等。</p><h3 id="3-陷入"><a href="#3-陷入" class="headerlink" title="3. 陷入"></a>3. 陷入</h3><p>在用户程序中使用系统调用。</p><h1 id="二、进程管理"><a href="#二、进程管理" class="headerlink" title="二、进程管理"></a>二、进程管理</h1><h2 id="进程与线程"><a href="#进程与线程" class="headerlink" title="进程与线程"></a>进程与线程</h2><h3 id="1-进程"><a href="#1-进程" class="headerlink" title="1. 进程"></a>1. 进程</h3><p>进程是资源分配的基本单位。</p><p>进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态，所谓的创建进程和撤销进程，都是指对 PCB 的操作。</p><p>下图显示了 4 个程序创建了 4 个进程，这 4 个进程可以并发地执行。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/a6ac2b08-3861-4e85-baa8-382287bfee9f.png"/> </div><br><h3 id="2-线程"><a href="#2-线程" class="headerlink" title="2. 线程"></a>2. 线程</h3><p>线程是独立调度的基本单位。</p><p>一个进程中可以有多个线程，它们共享进程资源。</p><p>QQ 和浏览器是两个进程，浏览器进程里面有很多线程，例如 HTTP 请求线程、事件响应线程、渲染线程等等，线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时，浏览器还可以响应用户的其它事件。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/3cd630ea-017c-488d-ad1d-732b4efeddf5.png"/> </div><br><h3 id="3-区别"><a href="#3-区别" class="headerlink" title="3. 区别"></a>3. 区别</h3><p>（一）拥有资源</p><p>进程是资源分配的基本单位，但是线程不拥有资源，线程可以访问隶属进程的资源。</p><p>（二）调度</p><p>线程是独立调度的基本单位，在同一进程中，线程的切换不会引起进程切换，从一个进程内的线程切换到另一个进程中的线程时，会引起进程切换。</p><p>（三）系统开销</p><p>由于创建或撤销进程时，系统都要为之分配或回收资源，如内存空间、I&#x2F;O 设备等，所付出的开销远大于创建或撤销线程时的开销。类似地，在进行进程切换时，涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置，而线程切换时只需保存和设置少量寄存器内容，开销很小。</p><p>（四）通信方面</p><p>进程间通信 (IPC) 需要进程同步和互斥手段的辅助，以保证数据的一致性。而线程间可以通过直接读&#x2F;写同一进程中的数据段（如全局变量）来进行通信。</p><h2 id="进程状态的切换"><a href="#进程状态的切换" class="headerlink" title="进程状态的切换"></a>进程状态的切换</h2><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/ProcessState.png" width="500"/> </div><br><ul><li>就绪状态（ready）：等待被调度</li><li>运行状态（running）</li><li>阻塞状态（waiting）：等待资源</li></ul><p>应该注意以下内容：</p><ul><li>只有就绪态和运行态可以相互转换，其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间，转为运行状态；而运行状态的进程，在分配给它的 CPU 时间片用完之后就会转为就绪状态，等待下一次调度。</li><li>阻塞状态是缺少需要的资源从而由运行状态转换而来，但是该资源不包括 CPU 时间，缺少 CPU 时间会从运行态转换为就绪态。</li></ul><h2 id="进程调度算法"><a href="#进程调度算法" class="headerlink" title="进程调度算法"></a>进程调度算法</h2><p>不同环境的调度算法目标不同，因此需要针对不同环境来讨论调度算法。</p><h3 id="1-批处理系统"><a href="#1-批处理系统" class="headerlink" title="1. 批处理系统"></a>1. 批处理系统</h3><p>批处理系统没有太多的用户操作，在该系统中，调度算法目标是保证吞吐量和周转时间（从提交到终止的时间）。</p><p><strong>1.1 先来先服务 first-come first-serverd（FCFS）</strong> </p><p>按照请求的顺序进行调度。</p><p>有利于长作业，但不利于短作业，因为短作业必须一直等待前面的长作业执行完毕才能执行，而长作业又需要执行很长时间，造成了短作业等待时间过长。</p><p><strong>1.2 短作业优先 shortest job first（SJF）</strong> </p><p>按估计运行时间最短的顺序进行调度。</p><p>长作业有可能会饿死，处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来，那么长作业永远得不到调度。</p><p><strong>1.3 最短剩余时间优先 shortest remaining time next（SRTN）</strong> </p><p>按估计剩余时间最短的顺序进行调度。</p><h3 id="2-交互式系统"><a href="#2-交互式系统" class="headerlink" title="2. 交互式系统"></a>2. 交互式系统</h3><p>交互式系统有大量的用户交互操作，在该系统中调度算法的目标是快速地进行响应。</p><p><strong>2.1 时间片轮转</strong> </p><p>将所有就绪进程按 FCFS 的原则排成一个队列，每次调度时，把 CPU 时间分配给队首进程，该进程可以执行一个时间片。当时间片用完时，由计时器发出时钟中断，调度程序便停止该进程的执行，并将它送往就绪队列的末尾，同时继续把 CPU 时间分配给队首的进程。</p><p>时间片轮转算法的效率和时间片的大小有很大关系：</p><ul><li>因为进程切换都要保存进程的信息并且载入新进程的信息，如果时间片太小，会导致进程切换得太频繁，在进程切换上就会花过多时间。</li><li>而如果时间片过长，那么实时性就不能得到保证。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/8c662999-c16c-481c-9f40-1fdba5bc9167.png"/> </div><br><p><strong>2.2 优先级调度</strong> </p><p>为每个进程分配一个优先级，按优先级进行调度。</p><p>为了防止低优先级的进程永远等不到调度，可以随着时间的推移增加等待进程的优先级。</p><p><strong>2.3 多级反馈队列</strong> </p><p>如果一个进程需要执行 100 个时间片，如果采用时间片轮转调度算法，那么需要交换 100 次。</p><p>多级队列是为这种需要连续执行多个时间片的进程考虑，它设置了多个队列，每个队列时间片大小都不同，例如 1,2,4,8,..。进程在第一个队列没执行完，就会被移到下一个队列。这种方式下，之前的进程只需要交换 7 次。</p><p>每个队列优先权也不同，最上面的优先权最高。因此只有上一个队列没有进程在排队，才能调度当前队列上的进程。</p><p>可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/042cf928-3c8e-4815-ae9c-f2780202c68f.png"/> </div><br><h3 id="3-实时系统"><a href="#3-实时系统" class="headerlink" title="3. 实时系统"></a>3. 实时系统</h3><p>实时系统要求一个请求在一个确定时间内得到响应。</p><p>分为硬实时和软实时，前者必须满足绝对的截止时间，后者可以容忍一定的超时。</p><h2 id="进程同步"><a href="#进程同步" class="headerlink" title="进程同步"></a>进程同步</h2><h3 id="1-临界区"><a href="#1-临界区" class="headerlink" title="1. 临界区"></a>1. 临界区</h3><p>对临界资源进行访问的那段代码称为临界区。</p><p>为了互斥访问临界资源，每个进程在进入临界区之前，需要先进行检查。</p><figure class="highlight html"><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><span class="line">// entry section</span><br><span class="line">// critical section;</span><br><span class="line">// exit section</span><br></pre></td></tr></table></figure><h3 id="2-同步与互斥"><a href="#2-同步与互斥" class="headerlink" title="2. 同步与互斥"></a>2. 同步与互斥</h3><ul><li>同步：多个进程按一定顺序执行；</li><li>互斥：多个进程在同一时刻只有一个进程能进入临界区。</li></ul><h3 id="3-信号量"><a href="#3-信号量" class="headerlink" title="3. 信号量"></a>3. 信号量</h3><p>信号量（Semaphore）是一个整型变量，可以对其执行 down 和 up 操作，也就是常见的 P 和 V 操作。</p><ul><li><strong>down</strong>  : 如果信号量大于 0 ，执行 -1 操作；如果信号量等于 0，进程睡眠，等待信号量大于 0；</li><li><strong>up</strong> ：对信号量执行 +1 操作，唤醒睡眠的进程让其完成 down 操作。</li></ul><p>down 和 up 操作需要被设计成原语，不可分割，通常的做法是在执行这些操作的时候屏蔽中断。</p><p>如果信号量的取值只能为 0 或者 1，那么就成为了  <strong>互斥量（Mutex）</strong> ，0 表示临界区已经加锁，1 表示临界区解锁。</p><figure class="highlight c"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="type">int</span> semaphore;</span><br><span class="line">semaphore mutex = <span class="number">1</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">P1</span><span class="params">()</span> &#123;</span><br><span class="line">    down(&amp;mutex);</span><br><span class="line">    <span class="comment">// 临界区</span></span><br><span class="line">    up(&amp;mutex);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">P2</span><span class="params">()</span> &#123;</span><br><span class="line">    down(&amp;mutex);</span><br><span class="line">    <span class="comment">// 临界区</span></span><br><span class="line">    up(&amp;mutex);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><font size=3>  <strong>使用信号量实现生产者-消费者问题</strong>  </font> </br></p><p>问题描述：使用一个缓冲区来保存物品，只有缓冲区没有满，生产者才可以放入物品；只有缓冲区不为空，消费者才可以拿走物品。</p><p>因为缓冲区属于临界资源，因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。</p><p>为了同步生产者和消费者的行为，需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计，这里需要使用两个信号量：empty 记录空缓冲区的数量，full 记录满缓冲区的数量。其中，empty 信号量是在生产者进程中使用，当 empty 不为 0 时，生产者才可以放入物品；full 信号量是在消费者进程中使用，当 full 信号量不为 0 时，消费者才可以取走物品。</p><p>注意，不能先对缓冲区进行加锁，再测试信号量。也就是说，不能先执行 down(mutex) 再执行 down(empty)。如果这么做了，那么可能会出现这种情况：生产者对缓冲区加锁后，执行 down(empty) 操作，发现 empty &#x3D; 0，此时生产者睡眠。消费者不能进入临界区，因为生产者对缓冲区加锁了，消费者就无法执行 up(empty) 操作，empty 永远都为 0，导致生产者永远等待下，不会释放锁，消费者因此也会永远等待下去。</p><figure class="highlight c"><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><span class="line"><span class="meta">#<span class="keyword">define</span> N 100</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="type">int</span> semaphore;</span><br><span class="line">semaphore mutex = <span class="number">1</span>;</span><br><span class="line">semaphore empty = N;</span><br><span class="line">semaphore full = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">producer</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">while</span>(TRUE) &#123;</span><br><span class="line">        <span class="type">int</span> item = produce_item();</span><br><span class="line">        down(&amp;empty);</span><br><span class="line">        down(&amp;mutex);</span><br><span class="line">        insert_item(item);</span><br><span class="line">        up(&amp;mutex);</span><br><span class="line">        up(&amp;full);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">consumer</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">while</span>(TRUE) &#123;</span><br><span class="line">        down(&amp;full);</span><br><span class="line">        down(&amp;mutex);</span><br><span class="line">        <span class="type">int</span> item = remove_item();</span><br><span class="line">        up(&amp;mutex);</span><br><span class="line">        up(&amp;empty);</span><br><span class="line">        consume_item(item);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-管程"><a href="#4-管程" class="headerlink" title="4. 管程"></a>4. 管程</h3><p>使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制，而管程把控制的代码独立出来，不仅不容易出错，也使得客户端代码调用更容易。</p><p>c 语言不支持管程，下面的示例代码使用了类 Pascal 语言来描述管程。示例代码的管程提供了 insert() 和 remove() 方法，客户端代码通过调用这两个方法来解决生产者-消费者问题。</p><figure class="highlight pascal"><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><span class="line">monitor ProducerConsumer</span><br><span class="line">    integer i;</span><br><span class="line">    condition c;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">procedure</span> <span class="title">insert</span><span class="params">()</span>;</span></span><br><span class="line">    <span class="keyword">begin</span></span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    <span class="keyword">end</span>;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">procedure</span> <span class="title">remove</span><span class="params">()</span>;</span></span><br><span class="line">    <span class="keyword">begin</span></span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    <span class="keyword">end</span>;</span><br><span class="line"><span class="keyword">end</span> monitor;</span><br></pre></td></tr></table></figure><p>管程有一个重要特性：在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程，否者其它进程永远不能使用管程。</p><p>管程引入了  <strong>条件变量</strong>  以及相关的操作：<strong>wait()</strong> 和 <strong>signal()</strong> 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞，把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。</p><p><font size=3> <strong>使用管程实现生产者-消费者问题</strong> </font><br></p><figure class="highlight pascal"><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><span class="line"><span class="comment">// 管程</span></span><br><span class="line">monitor ProducerConsumer</span><br><span class="line">    condition full, empty;</span><br><span class="line">    integer count := <span class="number">0</span>;</span><br><span class="line">    condition c;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">procedure</span> <span class="title">insert</span><span class="params">(item: integer)</span>;</span></span><br><span class="line">    <span class="keyword">begin</span></span><br><span class="line">        <span class="keyword">if</span> count = N <span class="keyword">then</span> wait(full);</span><br><span class="line">        insert_item(item);</span><br><span class="line">        count := count + <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">if</span> count = <span class="number">1</span> <span class="keyword">then</span> signal(empty);</span><br><span class="line">    <span class="keyword">end</span>;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">remove</span>:</span> integer;</span><br><span class="line">    <span class="keyword">begin</span></span><br><span class="line">        <span class="keyword">if</span> count = <span class="number">0</span> <span class="keyword">then</span> wait(empty);</span><br><span class="line">        remove = remove_item;</span><br><span class="line">        count := count - <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">if</span> count = N -<span class="number">1</span> <span class="keyword">then</span> signal(full);</span><br><span class="line">    <span class="keyword">end</span>;</span><br><span class="line"><span class="keyword">end</span> monitor;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 生产者客户端</span></span><br><span class="line"><span class="function"><span class="keyword">procedure</span> <span class="title">producer</span></span></span><br><span class="line"><span class="function"><span class="title">begin</span></span></span><br><span class="line"><span class="function">    <span class="title">while</span> <span class="title">true</span> <span class="title">do</span></span></span><br><span class="line"><span class="function">    <span class="title">begin</span></span></span><br><span class="line"><span class="function">        <span class="title">item</span> = <span class="title">produce_item</span>;</span></span><br><span class="line">        ProducerConsumer.insert(item);</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 消费者客户端</span></span><br><span class="line"><span class="function"><span class="keyword">procedure</span> <span class="title">consumer</span></span></span><br><span class="line"><span class="function"><span class="title">begin</span></span></span><br><span class="line"><span class="function">    <span class="title">while</span> <span class="title">true</span> <span class="title">do</span></span></span><br><span class="line"><span class="function">    <span class="title">begin</span></span></span><br><span class="line"><span class="function">        <span class="title">item</span> = <span class="title">ProducerConsumer</span>.<span class="title">remove</span>;</span></span><br><span class="line">        consume_item(item);</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span>;</span><br></pre></td></tr></table></figure><h2 id="经典同步问题"><a href="#经典同步问题" class="headerlink" title="经典同步问题"></a>经典同步问题</h2><p>生产者和消费者问题前面已经讨论过了。</p><h3 id="1-读者-写者问题"><a href="#1-读者-写者问题" class="headerlink" title="1. 读者-写者问题"></a>1. 读者-写者问题</h3><p>允许多个进程同时对数据进行读操作，但是不允许读和写以及写和写操作同时发生。</p><p>一个整型变量 count 记录在对数据进行读操作的进程数量，一个互斥量 count_mutex 用于对 count 加锁，一个互斥量 data_mutex 用于对读写的数据加锁。</p><figure class="highlight c"><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><span class="line"><span class="keyword">typedef</span> <span class="type">int</span> semaphore;</span><br><span class="line">semaphore count_mutex = <span class="number">1</span>;</span><br><span class="line">semaphore data_mutex = <span class="number">1</span>;</span><br><span class="line"><span class="type">int</span> count = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">reader</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">while</span>(TRUE) &#123;</span><br><span class="line">        down(&amp;count_mutex);</span><br><span class="line">        count++;</span><br><span class="line">        <span class="keyword">if</span>(count == <span class="number">1</span>) down(&amp;data_mutex); <span class="comment">// 第一个读者需要对数据进行加锁，防止写进程访问</span></span><br><span class="line">        up(&amp;count_mutex);</span><br><span class="line">        read();</span><br><span class="line">        down(&amp;count_mutex);</span><br><span class="line">        count--;</span><br><span class="line">        <span class="keyword">if</span>(count == <span class="number">0</span>) up(&amp;data_mutex);</span><br><span class="line">        up(&amp;count_mutex);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">writer</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">while</span>(TRUE) &#123;</span><br><span class="line">        down(&amp;data_mutex);</span><br><span class="line">        write();</span><br><span class="line">        up(&amp;data_mutex);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-哲学家进餐问题"><a href="#2-哲学家进餐问题" class="headerlink" title="2. 哲学家进餐问题"></a>2. 哲学家进餐问题</h3><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/a9077f06-7584-4f2b-8c20-3a8e46928820.jpg"/> </div><br><p>五个哲学家围着一张圆桌，每个哲学家面前放着食物。哲学家的生活有两种交替活动：吃饭以及思考。当一个哲学家吃饭时，需要先拿起自己左右两边的两根筷子，并且一次只能拿起一根筷子。</p><p>下面是一种错误的解法，考虑到如果所有哲学家同时拿起左手边的筷子，那么就无法拿起右手边的筷子，造成死锁。</p><figure class="highlight c"><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><span class="line"><span class="meta">#<span class="keyword">define</span> N 5</span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">philosopher</span><span class="params">(<span class="type">int</span> i)</span> &#123;</span><br><span class="line">    <span class="keyword">while</span>(TRUE) &#123;</span><br><span class="line">        think();</span><br><span class="line">        take(i);       <span class="comment">// 拿起左边的筷子</span></span><br><span class="line">        take((i+<span class="number">1</span>)%N); <span class="comment">// 拿起右边的筷子</span></span><br><span class="line">        eat();</span><br><span class="line">        put(i);</span><br><span class="line">        put((i+<span class="number">1</span>)%N);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为了防止死锁的发生，可以设置两个条件：</p><ul><li>必须同时拿起左右两根筷子；</li><li>只有在两个邻居都没有进餐的情况下才允许进餐。</li></ul><figure class="highlight c"><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><span class="line"><span class="meta">#<span class="keyword">define</span> N 5</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> LEFT (i + N - 1) % N <span class="comment">// 左邻居</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> RIGHT (i + 1) % N    <span class="comment">// 右邻居</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> THINKING 0</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> HUNGRY   1</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> EATING   2</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="type">int</span> semaphore;</span><br><span class="line"><span class="type">int</span> state[N];                <span class="comment">// 跟踪每个哲学家的状态</span></span><br><span class="line">semaphore mutex = <span class="number">1</span>;         <span class="comment">// 临界区的互斥</span></span><br><span class="line">semaphore s[N];              <span class="comment">// 每个哲学家一个信号量</span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">philosopher</span><span class="params">(<span class="type">int</span> i)</span> &#123;</span><br><span class="line">    <span class="keyword">while</span>(TRUE) &#123;</span><br><span class="line">        think();</span><br><span class="line">        take_two(i);</span><br><span class="line">        eat();</span><br><span class="line">        put_tow(i);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">take_two</span><span class="params">(<span class="type">int</span> i)</span> &#123;</span><br><span class="line">    down(&amp;mutex);</span><br><span class="line">    state[i] = HUNGRY;</span><br><span class="line">    test(i);</span><br><span class="line">    up(&amp;mutex);</span><br><span class="line">    down(&amp;s[i]);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">put_tow</span><span class="params">(i)</span> &#123;</span><br><span class="line">    down(&amp;mutex);</span><br><span class="line">    state[i] = THINKING;</span><br><span class="line">    test(LEFT);</span><br><span class="line">    test(RIGHT);</span><br><span class="line">    up(&amp;mutex);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">test</span><span class="params">(i)</span> &#123;         <span class="comment">// 尝试拿起两把筷子</span></span><br><span class="line">    <span class="keyword">if</span>(state[i] == HUNGRY &amp;&amp; state[LEFT] != EATING &amp;&amp; state[RIGHT] !=EATING) &#123;</span><br><span class="line">        state[i] = EATING;</span><br><span class="line">        up(&amp;s[i]);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="进程通信"><a href="#进程通信" class="headerlink" title="进程通信"></a>进程通信</h2><p>进程同步与进程通信很容易混淆，它们的区别在于：</p><ul><li>进程同步：控制多个进程按一定顺序执行；</li><li>进程通信：进程间传输信息。</li></ul><p>进程通信是一种手段，而进程同步是一种目的。也可以说，为了能够达到进程同步的目的，需要让进程进行通信，传输一些进程同步所需要的信息。</p><h3 id="1-管道"><a href="#1-管道" class="headerlink" title="1. 管道"></a>1. 管道</h3><p>管道是通过调用 pipe 函数创建的，fd[0] 用于读，fd[1] 用于写。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"><span class="type">int</span> <span class="title function_">pipe</span><span class="params">(<span class="type">int</span> fd[<span class="number">2</span>])</span>;</span><br></pre></td></tr></table></figure><p>它具有以下限制：</p><ul><li>只支持半双工通信（单向交替传输）；</li><li>只能在父子进程中使用。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/53cd9ade-b0a6-4399-b4de-7f1fbd06cdfb.png"/> </div><br><h3 id="2-FIFO"><a href="#2-FIFO" class="headerlink" title="2. FIFO"></a>2. FIFO</h3><p>也称为命名管道，去除了管道只能在父子进程中使用的限制。</p><figure class="highlight c"><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><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/stat.h&gt;</span></span></span><br><span class="line"><span class="type">int</span> <span class="title function_">mkfifo</span><span class="params">(<span class="type">const</span> <span class="type">char</span> *path, <span class="type">mode_t</span> mode)</span>;</span><br><span class="line"><span class="type">int</span> <span class="title function_">mkfifoat</span><span class="params">(<span class="type">int</span> fd, <span class="type">const</span> <span class="type">char</span> *path, <span class="type">mode_t</span> mode)</span>;</span><br></pre></td></tr></table></figure><p>FIFO 常用于客户-服务器应用程序中，FIFO 用作汇聚点，在客户进程和服务器进程之间传递数据。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/2ac50b81-d92a-4401-b9ec-f2113ecc3076.png"/> </div><br><h3 id="3-消息队列"><a href="#3-消息队列" class="headerlink" title="3. 消息队列"></a>3. 消息队列</h3><p>相比于 FIFO，消息队列具有以下优点：</p><ul><li>消息队列可以独立于读写进程存在，从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难；</li><li>避免了 FIFO 的同步阻塞问题，不需要进程自己提供同步方法；</li><li>读进程可以根据消息类型有选择地接收消息，而不像 FIFO 那样只能默认地接收。</li></ul><h3 id="4-信号量"><a href="#4-信号量" class="headerlink" title="4. 信号量"></a>4. 信号量</h3><p>它是一个计数器，用于为多个进程提供对共享数据对象的访问。</p><h3 id="5-共享存储"><a href="#5-共享存储" class="headerlink" title="5. 共享存储"></a>5. 共享存储</h3><p>允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制，所以这是最快的一种 IPC。</p><p>需要使用信号量用来同步对共享存储的访问。</p><p>多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件，而是使用使用内存的匿名段。</p><h3 id="6-套接字"><a href="#6-套接字" class="headerlink" title="6. 套接字"></a>6. 套接字</h3><p>与其它通信机制不同的是，它可用于不同机器间的进程通信。</p><h1 id="三、死锁"><a href="#三、死锁" class="headerlink" title="三、死锁"></a>三、死锁</h1><h2 id="死锁的必要条件"><a href="#死锁的必要条件" class="headerlink" title="死锁的必要条件"></a>死锁的必要条件</h2><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/c037c901-7eae-4e31-a1e4-9d41329e5c3e.png"/> </div><br><ul><li>互斥：每个资源要么已经分配给了一个进程，要么就是可用的。</li><li>占有和等待：已经得到了某个资源的进程可以再请求新的资源。</li><li>不可抢占：已经分配给一个进程的资源不能强制性地被抢占，它只能被占有它的进程显式地释放。</li><li>环路等待：有两个或者两个以上的进程组成一条环路，该环路中的每个进程都在等待下一个进程所占有的资源。</li></ul><h2 id="死锁的处理方法"><a href="#死锁的处理方法" class="headerlink" title="死锁的处理方法"></a>死锁的处理方法</h2><h3 id="1-鸵鸟策略"><a href="#1-鸵鸟策略" class="headerlink" title="1. 鸵鸟策略"></a>1. 鸵鸟策略</h3><p>把头埋在沙子里，假装根本没发生问题。</p><p>因为解决死锁问题的代价很高，因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响，或发生死锁的概率很低，可以采用鸵鸟策略。</p><p>大多数操作系统，包括 Unix，Linux 和 Windows，处理死锁问题的办法仅仅是忽略它。</p><h3 id="2-死锁检测与死锁恢复"><a href="#2-死锁检测与死锁恢复" class="headerlink" title="2. 死锁检测与死锁恢复"></a>2. 死锁检测与死锁恢复</h3><p>不试图阻止死锁，而是当检测到死锁发生时，采取措施进行恢复。</p><p>（一）每种类型一个资源的死锁检测</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/b1fa0453-a4b0-4eae-a352-48acca8fff74.png"/> </div><br><p>上图为资源分配图，其中方框表示资源，圆圈表示进程。资源指向进程表示该资源已经分配给该进程，进程指向资源表示进程请求获取该资源。</p><p>图 a 可以抽取出环，如图 b，它满足了环路等待条件，因此会发生死锁。</p><p>每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现，从一个节点出发进行深度优先搜索，对访问过的节点进行标记，如果访问了已经标记的节点，就表示有向图存在环，也就是检测到死锁的发生。</p><p>（二）每种类型多个资源的死锁检测</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/e1eda3d5-5ec8-4708-8e25-1a04c5e11f48.png"/> </div><br><p>上图中，有三个进程四个资源，每个数据代表的含义如下：</p><ul><li>E 向量：资源总量</li><li>A 向量：资源剩余量</li><li>C 矩阵：每个进程所拥有的资源数量，每一行都代表一个进程拥有资源的数量</li><li>R 矩阵：每个进程请求的资源数量</li></ul><p>进程 P<sub>1</sub> 和 P<sub>2</sub> 所请求的资源都得不到满足，只有进程 P<sub>3</sub> 可以，让 P<sub>3</sub> 执行，之后释放 P<sub>3</sub> 拥有的资源，此时 A &#x3D; (2 2 2 0)。P<sub>2</sub> 可以执行，执行后释放 P<sub>2</sub> 拥有的资源，A &#x3D; (4 2 2 1) 。P<sub>1</sub> 也可以执行。所有进程都可以顺利执行，没有死锁。</p><p>算法总结如下：</p><p>每个进程最开始时都不被标记，执行过程有可能被标记。当算法结束时，任何没有被标记的进程都是死锁进程。</p><ol><li>寻找一个没有标记的进程 P<sub>i</sub>，它所请求的资源小于等于 A。</li><li>如果找到了这样一个进程，那么将 C 矩阵的第 i 行向量加到 A 中，标记该进程，并转回 1。</li><li>如果没有这样一个进程，算法终止。</li></ol><p>（三）死锁恢复</p><ul><li>利用抢占恢复</li><li>利用回滚恢复</li><li>通过杀死进程恢复</li></ul><h3 id="3-死锁预防"><a href="#3-死锁预防" class="headerlink" title="3. 死锁预防"></a>3. 死锁预防</h3><p>在程序运行之前预防发生死锁。</p><p>（一）破坏互斥条件</p><p>例如假脱机打印机技术允许若干个进程同时输出，唯一真正请求物理打印机的进程是打印机守护进程。</p><p>（二）破坏占有和等待条件</p><p>一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。</p><p>（三）破坏不可抢占条件</p><p>（四）破坏环路等待</p><p>给资源统一编号，进程只能按编号顺序来请求资源。</p><h3 id="4-死锁避免"><a href="#4-死锁避免" class="headerlink" title="4. 死锁避免"></a>4. 死锁避免</h3><p>在程序运行时避免发生死锁。</p><p>（一）安全状态</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/ed523051-608f-4c3f-b343-383e2d194470.png"/> </div><br><p>图 a 的第二列 Has 表示已拥有的资源数，第三列 Max 表示总共需要的资源数，Free 表示还有可以使用的资源数。从图 a 开始出发，先让 B 拥有所需的所有资源（图 b），运行结束后释放 B，此时 Free 变为 5（图 c）；接着以同样的方式运行 C 和 A，使得所有进程都能成功运行，因此可以称图 a 所示的状态时安全的。</p><p>定义：如果没有死锁发生，并且即使所有进程突然请求对资源的最大需求，也仍然存在某种调度次序能够使得每一个进程运行完毕，则称该状态是安全的。</p><p>安全状态的检测与死锁的检测类似，因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类似，可以结合着做参考对比。</p><p>（二）单个资源的银行家算法</p><p>一个小城镇的银行家，他向一群客户分别承诺了一定的贷款额度，算法要做的是判断对请求的满足是否会进入不安全状态，如果是，就拒绝请求；否则予以分配。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/d160ec2e-cfe2-4640-bda7-62f53e58b8c0.png"/> </div><br><p>上图 c 为不安全状态，因此算法会拒绝之前的请求，从而避免进入图 c 中的状态。</p><p>（三）多个资源的银行家算法</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/62e0dd4f-44c3-43ee-bb6e-fedb9e068519.png"/> </div><br><p>上图中有五个进程，四个资源。左边的图表示已经分配的资源，右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示：总资源、已分配资源以及可用资源，注意这三个为向量，而不是具体数值，例如 A&#x3D;(1020)，表示 4 个资源分别还剩下 1&#x2F;0&#x2F;2&#x2F;0。</p><p>检查一个状态是否安全的算法如下：</p><ul><li>查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行，那么系统将会发生死锁，状态是不安全的。</li><li>假若找到这样一行，将该进程标记为终止，并将其已分配资源加到 A 中。</li><li>重复以上两步，直到所有进程都标记为终止，则状态时安全的。</li></ul><p>如果一个状态不是安全的，需要拒绝进入这个状态。</p><h1 id="四、内存管理"><a href="#四、内存管理" class="headerlink" title="四、内存管理"></a>四、内存管理</h1><h2 id="虚拟内存"><a href="#虚拟内存" class="headerlink" title="虚拟内存"></a>虚拟内存</h2><p>虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存，从而让程序获得更多的可用内存。</p><p>为了更好的管理内存，操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间，这个地址空间被分割成多个块，每一块称为一页。这些页被映射到物理内存，但不需要映射到连续的物理内存，也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时，由硬件执行必要的映射，将缺失的部分装入物理内存并重新执行失败的指令。</p><p>从上面的描述中可以看出，虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存，也就是说一个程序不需要全部调入内存就可以运行，这使得有限的内存运行大程序称为可能。例如有一台计算机可以产生 16 位地址，那么一个程序的地址空间范围是 0~64K。该计算机只有 32KB 的物理内存，虚拟内存技术允许该计算机运行一个 64K 大小的程序。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/7b281b1e-0595-402b-ae35-8c91084c33c1.png"/> </div><br><h2 id="分页系统地址映射"><a href="#分页系统地址映射" class="headerlink" title="分页系统地址映射"></a>分页系统地址映射</h2><p>内存管理单元（MMU）管理着地址空间和物理内存的转换，其中的页表（Page table）存储着页（程序地址空间）和页框（物理内存空间）的映射表。</p><p>下图的页表存放着 16 个页，这 16 个页需要用 4 个比特位来进行索引定位，也就是存储页面号，剩下 12 个比特位存储偏移量。</p><p>例如对于虚拟地址（0010 000000000100），前 4 位是存储页面号 2，读取表项内容为（110 1）。该页在内存中，并且页框的地址为 （110 000000000100）。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/cf4386a1-58c9-4eca-a17f-e12b1e9770eb.png" width="500"/> </div><br><h2 id="页面置换算法"><a href="#页面置换算法" class="headerlink" title="页面置换算法"></a>页面置换算法</h2><p>在程序运行过程中，如果要访问的页面不在内存中，就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间，系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。</p><p>页面置换算法和缓存淘汰策略类似，可以将内存看成磁盘的缓存。在缓存系统中，缓存的大小有限，当有新的缓存到达时，需要淘汰一部分已经存在的缓存，这样才有空间存放新的缓存数据。</p><p>页面置换算法的主要目标是使页面置换频率最低（也可以说缺页率最低）。</p><h3 id="1-最佳"><a href="#1-最佳" class="headerlink" title="1. 最佳"></a>1. 最佳</h3><blockquote><p>Optimal</p></blockquote><p>所选择的被换出的页面将是最长时间内不再被访问，通常可以保证获得最低的缺页率。</p><p>是一种理论上的算法，因为无法知道一个页面多长时间不再被访问。</p><p>举例：一个系统为某进程分配了三个物理块，并有如下页面引用序列：</p><div align="center"><img src="https://latex.codecogs.com/gif.latex?7，0，1，2，0，3，0，4，2，3，0，3，2，1，2，0，1，7，0，1"/></div> <br><p>开始运行时，先将 7, 0, 1 三个页面装入内存。当进程要访问页面 2 时，产生缺页中断，会将页面 7 换出，因为页面 7 再次被访问的时间最长。</p><h3 id="2-最近最久未使用"><a href="#2-最近最久未使用" class="headerlink" title="2. 最近最久未使用"></a>2. 最近最久未使用</h3><blockquote><p>LRU, Least Recently Used</p></blockquote><p>虽然无法知道将来要使用的页面情况，但是可以知道过去使用页面的情况。LRU 将最近最久未使用的页面换出。</p><p>为了实现 LRU，需要在内存中维护一个所有页面的链表。当一个页面被访问时，将这个页面移到链表表头。这样就能保证链表表尾的页面时最近最久未访问的。</p><p>因为每次访问都需要更新链表，因此这种方式实现的 LRU 代价很高。</p><div align="center"><img src="https://latex.codecogs.com/gif.latex?4，7，0，7，1，0，1，2，1，2，6"/></div> <br><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/eb859228-c0f2-4bce-910d-d9f76929352b.png"/> </div><br><h3 id="3-最近未使用"><a href="#3-最近未使用" class="headerlink" title="3. 最近未使用"></a>3. 最近未使用</h3><blockquote><p>NRU, Not Recently Used</p></blockquote><p>每个页面都有两个状态位：R 与 M，当页面被访问时设置页面的 R&#x3D;1，当页面被修改时设置 M&#x3D;1。其中 R 位会定时被清零。可以将页面分成以下四类：</p><ul><li>R&#x3D;0，M&#x3D;0</li><li>R&#x3D;0，M&#x3D;1</li><li>R&#x3D;1，M&#x3D;0</li><li>R&#x3D;1，M&#x3D;1</li></ul><p>当发生缺页中断时，NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。</p><p>NRU 优先换出已经被修改的脏页面（R&#x3D;0，M&#x3D;1），而不是被频繁使用的干净页面（R&#x3D;1，M&#x3D;0）。</p><h3 id="4-先进先出"><a href="#4-先进先出" class="headerlink" title="4. 先进先出"></a>4. 先进先出</h3><blockquote><p>FIFO, First In First Out</p></blockquote><p>选择换出的页面是最先进入的页面。</p><p>该算法会将那些经常被访问的页面也被换出，从而使缺页率升高。</p><h3 id="5-第二次机会算法"><a href="#5-第二次机会算法" class="headerlink" title="5. 第二次机会算法"></a>5. 第二次机会算法</h3><p>FIFO 算法可能会把经常使用的页面置换出去，为了避免这一问题，对该算法做一个简单的修改：</p><p>当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候，检查最老页面的 R 位。如果 R 位是 0，那么这个页面既老又没有被使用，可以立刻置换掉；如果是 1，就将 R 位清 0，并把该页面放到链表的尾端，修改它的装入时间使它就像刚装入的一样，然后继续从链表的头部开始搜索。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/ecf8ad5d-5403-48b9-b6e7-f2e20ffe8fca.png"/> </div><br><h3 id="6-时钟"><a href="#6-时钟" class="headerlink" title="6. 时钟"></a>6. 时钟</h3><blockquote><p>Clock</p></blockquote><p>第二次机会算法需要在链表中移动页面，降低了效率。时钟算法使用环形链表将页面连接起来，再使用一个指针指向最老的页面。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/5f5ef0b6-98ea-497c-a007-f6c55288eab1.png"/> </div><br><h2 id="分段"><a href="#分段" class="headerlink" title="分段"></a>分段</h2><p>虚拟内存采用的是分页技术，也就是将地址空间划分成固定大小的页，每一页再与内存进行映射。</p><p>下图为一个编译器在编译过程中建立的多个表，有 4 个表是动态增长的，如果使用分页系统的一维地址空间，动态增长的特点会导致覆盖问题的出现。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/22de0538-7c6e-4365-bd3b-8ce3c5900216.png"/> </div><br><p>分段的做法是把每个表分成段，一个段构成一个独立的地址空间。每个段的长度可以不同，并且可以动态增长。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/e0900bb2-220a-43b7-9aa9-1d5cd55ff56e.png"/> </div><br><h2 id="段页式"><a href="#段页式" class="headerlink" title="段页式"></a>段页式</h2><p>程序的地址空间划分成多个拥有独立地址空间的段，每个段上的地址空间划分成大小相同的页。这样既拥有分段系统的共享和保护，又拥有分页系统的虚拟内存功能。</p><h2 id="分页与分段的比较"><a href="#分页与分段的比较" class="headerlink" title="分页与分段的比较"></a>分页与分段的比较</h2><ul><li><p>对程序员的透明性：分页透明，但是分段需要程序员显示划分每个段。</p></li><li><p>地址空间的维度：分页是一维地址空间，分段是二维的。</p></li><li><p>大小是否可以改变：页的大小不可变，段的大小可以动态改变。</p></li><li><p>出现的原因：分页主要用于实现虚拟内存，从而获得更大的地址空间；分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。</p></li></ul><h1 id="五、设备管理"><a href="#五、设备管理" class="headerlink" title="五、设备管理"></a>五、设备管理</h1><h2 id="磁盘结构"><a href="#磁盘结构" class="headerlink" title="磁盘结构"></a>磁盘结构</h2><ul><li>盘面（Platter）：一个磁盘有多个盘面；</li><li>磁道（Track）：盘面上的圆形带状区域，一个盘面可以有多个磁道；</li><li>扇区（Track Sector）：磁道上的一个弧段，一个磁道可以有多个扇区，它是最小的物理储存单位，目前主要有 512 bytes 与 4 K 两种大小；</li><li>磁头（Head）：与盘面非常接近，能够将盘面上的磁场转换为电信号（读），或者将电信号转换为盘面的磁场（写）；</li><li>制动手臂（Actuator arm）：用于在磁道之间移动磁头；</li><li>主轴（Spindle）：使整个盘面转动。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/014fbc4d-d873-4a12-b160-867ddaed9807.jpg"/> </div><br><h2 id="磁盘调度算法"><a href="#磁盘调度算法" class="headerlink" title="磁盘调度算法"></a>磁盘调度算法</h2><p>读写一个磁盘块的时间的影响因素有：</p><ul><li>旋转时间（主轴转动盘面，使得磁头移动到适当的扇区上）</li><li>寻道时间（制动手臂移动，使得磁头移动到适当的磁道上）</li><li>实际的数据传输时间</li></ul><p>其中，寻道时间最长，因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。</p><h3 id="1-先来先服务"><a href="#1-先来先服务" class="headerlink" title="1. 先来先服务"></a>1. 先来先服务</h3><blockquote><p>FCFS, First Come First Served</p></blockquote><p>按照磁盘请求的顺序进行调度。</p><p>优点是公平和简单。缺点也很明显，因为未对寻道做任何优化，使平均寻道时间可能较长。</p><h3 id="2-最短寻道时间优先"><a href="#2-最短寻道时间优先" class="headerlink" title="2. 最短寻道时间优先"></a>2. 最短寻道时间优先</h3><blockquote><p>SSTF, Shortest Seek Time First</p></blockquote><p>优先调度与当前磁头所在磁道距离最近的磁道。</p><p>虽然平均寻道时间比较低，但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近，那么在等待的磁道请求会一直等待下去，也就是出现饥饿现象。具体来说，两边的磁道请求更容易出现饥饿现象。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/4e2485e4-34bd-4967-9f02-0c093b797aaa.png"/> </div><br><h3 id="3-电梯算法"><a href="#3-电梯算法" class="headerlink" title="3. 电梯算法"></a>3. 电梯算法</h3><blockquote><p>SCAN</p></blockquote><p>电梯总是保持一个方向运行，直到该方向没有请求为止，然后改变运行方向。</p><p>电梯算法（扫描算法）和电梯的运行过程类似，总是按一个方向来进行磁盘调度，直到该方向上没有未完成的磁盘请求，然后改变方向。</p><p>因为考虑了移动方向，因此所有的磁盘请求都会被满足，解决了 SSTF 的饥饿问题。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/271ce08f-c124-475f-b490-be44fedc6d2e.png"/> </div><br><h1 id="六、链接"><a href="#六、链接" class="headerlink" title="六、链接"></a>六、链接</h1><h2 id="编译系统"><a href="#编译系统" class="headerlink" title="编译系统"></a>编译系统</h2><p>以下是一个 hello.c 程序：</p><figure class="highlight c"><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><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">()</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;hello, world\n&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 Unix 系统上，由编译器把源文件转换为目标文件。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gcc -o hello hello.c</span><br></pre></td></tr></table></figure><p>这个过程大致如下：</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/b396d726-b75f-4a32-89a2-03a7b6e19f6f.jpg" width="800"/> </div><br><ul><li>预处理阶段：处理以 # 开头的预处理命令；</li><li>编译阶段：翻译成汇编文件；</li><li>汇编阶段：将汇编文件翻译成可重定向目标文件；</li><li>链接阶段：将可重定向目标文件和 printf.o 等单独预编译好的目标文件进行合并，得到最终的可执行目标文件。</li></ul><h2 id="静态链接"><a href="#静态链接" class="headerlink" title="静态链接"></a>静态链接</h2><p>静态连接器以一组可重定向目标文件为输入，生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务：</p><ul><li>符号解析：每个符号对应于一个函数、一个全局变量或一个静态变量，符号解析的目的是将每个符号引用与一个符号定义关联起来。</li><li>重定位：链接器通过把每个符号定义与一个内存位置关联起来，然后修改所有对这些符号的引用，使得它们指向这个内存位置。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/47d98583-8bb0-45cc-812d-47eefa0a4a40.jpg"/> </div><br><h2 id="目标文件"><a href="#目标文件" class="headerlink" title="目标文件"></a>目标文件</h2><ul><li>可执行目标文件：可以直接在内存中执行；</li><li>可重定向目标文件：可与其它可重定向目标文件在链接阶段合并，创建一个可执行目标文件；</li><li>共享目标文件：这是一种特殊的可重定向目标文件，可以在运行时被动态加载进内存并链接；</li></ul><h2 id="动态链接"><a href="#动态链接" class="headerlink" title="动态链接"></a>动态链接</h2><p>静态库有以下两个问题：</p><ul><li>当静态库更新时那么整个程序都要重新进行链接；</li><li>对于 printf 这种标准函数库，如果每个程序都要有代码，这会极大浪费资源。</li></ul><p>共享库是为了解决静态库的这两个问题而设计的，在 Linux 系统中通常用 .so 后缀来表示，Windows 系统上它们被称为 DLL。它具有以下特点：</p><ul><li>在给定的文件系统中一个库只有一个文件，所有引用该库的可执行目标文件都共享这个文件，它不会被复制到引用它的可执行文件中；</li><li>在内存中，一个共享库的 .text 节（已编译程序的机器代码）的一个副本可以被不同的正在运行的进程共享。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/76dc7769-1aac-4888-9bea-064f1caa8e77.jpg"/> </div><br><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li>Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Press, 2014.</li><li>汤子瀛, 哲凤屏, 汤小丹. 计算机操作系统[M]. 西安电子科技大学出版社, 2001.</li><li>Bryant, R. E., &amp; O’Hallaron, D. R. (2004). 深入理解计算机系统.</li><li>史蒂文斯. UNIX 环境高级编程 [M]. 人民邮电出版社, 2014.</li><li><a href="https://applied-programming.github.io/Operating-Systems-Notes/">Operating System Notes</a></li><li><a href="https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/2_Structures.html">Operating-System Structures</a></li><li><a href="http://cse.csusb.edu/tongyu/courses/cs460/notes/process.php">Processes</a></li><li><a href="https://www.slideshare.net/rkolahalam/inter-process-communication-presentation1">Inter Process Communication Presentation[1]</a></li><li><a href="https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1">Decoding UCS Invicta – Part 1</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本文转载自 &lt;a href=&quot;https://github.com/CyC2018/CS-Notes&quot;&gt;CS-Note&lt;/a&gt; 项目&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&quot;一、概述&quot;&gt;&lt;a href=&quot;#一、概述&quot; class=&quot;he</summary>
      
    
    
    
    <category term="转载" scheme="https://blog.jugg.xyz/categories/%E8%BD%AC%E8%BD%BD/"/>
    
    
    <category term="Linux" scheme="https://blog.jugg.xyz/tags/Linux/"/>
    
    <category term="Windows" scheme="https://blog.jugg.xyz/tags/Windows/"/>
    
    <category term="UNIX" scheme="https://blog.jugg.xyz/tags/UNIX/"/>
    
  </entry>
  
  <entry>
    <title>让 Hexo 博客支持 PWA</title>
    <link href="https://blog.jugg.xyz/2018/04/04/frontend/PWA-for-hexo/"/>
    <id>https://blog.jugg.xyz/2018/04/04/frontend/PWA-for-hexo/</id>
    <published>2018-04-04T15:59:49.000Z</published>
    <updated>2026-05-02T11:24:14.332Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>Progressive Web Apps (PWAs) are web applications that are regular web pages or websites, but can appear to the user like traditional applications or native mobile applications. The application type attempts to combine features offered by most modern browsers with the benefits of a mobile experience.</p></blockquote><p>–摘自维基百科</p><h2 id="支持-HTTPS"><a href="#支持-HTTPS" class="headerlink" title="支持 HTTPS"></a>支持 HTTPS</h2><p>支持 PWA 的第一步，就是全站 HTTPS。我的个人博客呢，是用 Hexo 生成静态页面，然后再通过 Nginx 进行代理，所以直接申请个免费的 SSL 证书就好了。（但是被图床的 HTTP 资源折腾了一下，&#x3D;。&#x3D;这个暂且不谈。）</p><h2 id="配置-hexo-pwa-插件"><a href="#配置-hexo-pwa-插件" class="headerlink" title="配置 hexo-pwa 插件"></a>配置 hexo-pwa 插件</h2><p>Hexo 支持 PWA 非常方便，因为已经有了专门的模块 <a href="https://github.com/lavas-project/hexo-pwa">hexo-pwa</a>，直接在博客目录用一行命令安装就行了：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm install --save hexo-pwa</span></span><br></pre></td></tr></table></figure><p>然后再配置站点的_config.yml文件，在最后添加 PWA 配置：</p><figure class="highlight yml"><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><span class="line"><span class="attr">pwa:</span></span><br><span class="line">  <span class="attr">manifest:</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">/manifest.json</span></span><br><span class="line">    <span class="attr">body:</span></span><br><span class="line">      <span class="attr">name:</span> <span class="string">hexo</span></span><br><span class="line">      <span class="attr">short_name:</span> <span class="string">hexo</span></span><br><span class="line">      <span class="attr">icons:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">src:</span> <span class="string">/images/android-chrome-192x192.png</span></span><br><span class="line">          <span class="attr">sizes:</span> <span class="string">192x192</span></span><br><span class="line">          <span class="attr">type:</span> <span class="string">image/png</span></span><br><span class="line">  <span class="attr">serviceWorker:</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">/sw.js</span></span><br><span class="line">    <span class="attr">preload:</span></span><br><span class="line">      <span class="attr">urls:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">/*</span></span><br><span class="line">      <span class="attr">posts:</span> <span class="number">5</span></span><br><span class="line">    <span class="attr">opts:</span></span><br><span class="line">      <span class="attr">networkTimeoutSeconds:</span> <span class="number">5</span></span><br><span class="line">    <span class="attr">routes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">pattern:</span> <span class="type">!!js/regexp</span> <span class="string">/hm.baidu.com/</span></span><br><span class="line">        <span class="attr">strategy:</span> <span class="string">networkOnly</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">pattern:</span> <span class="type">!!js/regexp</span> <span class="string">/.*\.(js|css|jpg|jpeg|png|gif)$/</span></span><br><span class="line">        <span class="attr">strategy:</span> <span class="string">cacheFirst</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">pattern:</span> <span class="type">!!js/regexp</span> <span class="string">/\//</span></span><br><span class="line">        <span class="attr">strategy:</span> <span class="string">networkFirst</span></span><br><span class="line">  <span class="attr">priority:</span> <span class="number">5</span></span><br></pre></td></tr></table></figure><h3 id="配置内容介绍"><a href="#配置内容介绍" class="headerlink" title="配置内容介绍"></a>配置内容介绍</h3><p>这里的配置中，第一段 manifest，里面的path就是你的 manifest.json 的位置；body 则是 manifest.json 的内容，可以写满，也可以为空。为空的话就会按照你的 path 设置去读取你的 manifest.json 文件内容。这个 manifest.json 也是 PWA 的关键内容，如果嫌配置麻烦的话，可以用 <a href="https://app-manifest.firebaseapp.com/?spm=a2c4e.11153940.blogcont441967.16.18ca18eaBudCNI">App Manifest Generator</a> 这个网站一键生成。把生成完的配置文件丢到 &#x2F;source 文件夹下就行了。</p><p>第二段的 serviceWorker 是 service worker 的配置文件，path 还是填写你的 sw.js 的位置。preload 则是填写你想要缓存的网址，设置为 &#x2F;* 则是缓存所有页面，里面的 posts 是则是你想要缓存的页面数量上限。opts 和 routes 可以通过 <a href="https://googlechromelabs.github.io/sw-toolbox/#main">sw-toolbox</a> 了解。</p><p>最后的 priority 是插件优先级的设置，相关内容可以点 <a href="https://hexo.io/api/filter.html">plugin priority</a> 深入了解。</p><p>这里还有一个坑，就是在 Safari 如果添加到主页，他默认会选择你添加的那个页面的截图作为图标，而不是你设置的图标，所以这里需要单独为 iOS 用户生成一个图标: source&#x2F;apple-touch-icon.png(ps.iOS 默认用黑色填充透明图层，所以建议不要选择又透明图层的图片作为图标，或者手动用白色填充下)。更多配置参考 <a href="https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html">Configuring Web Applications</a>。<br>iOS上的坑还有很多，诸如状态栏外观、链接到外部程序等，这也是#TODO的一部分，日后再谈。<br>作为示例，这是我的 <a href="https://cfp.vim-cn.com/cbfbq/json">manifest.json</a> 。</p><p>当然了，这只是初步支持 PWA，日后支持推送、弹窗等功能后我再继续更新吧。（逃</p><p>学习资料：<a href="https://lavas.baidu.com/pwa">百度 LAVAS</a> 、<a href="http://sangka-z.com/PWA-Book-CN/">Progressive Web Apps (PWA) 中文版</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;Progressive Web Apps (PWAs) are web applications that are regular web pages or websites, but can appear to the user like tra</summary>
      
    
    
    
    <category term="前端" scheme="https://blog.jugg.xyz/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="Node.js" scheme="https://blog.jugg.xyz/tags/Node-js/"/>
    
    <category term="PWA" scheme="https://blog.jugg.xyz/tags/PWA/"/>
    
  </entry>
  
  <entry>
    <title>Linux 基础知识</title>
    <link href="https://blog.jugg.xyz/2018/04/03/repost/Linux/"/>
    <id>https://blog.jugg.xyz/2018/04/03/repost/Linux/</id>
    <published>2018-04-03T07:53:30.000Z</published>
    <updated>2026-05-02T11:24:14.334Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文转载自 <a href="https://github.com/CyC2018/CS-Notes">CS-Note</a> 项目</p></blockquote><h1 id="一、常用操作以及概念"><a href="#一、常用操作以及概念" class="headerlink" title="一、常用操作以及概念"></a>一、常用操作以及概念</h1><h2 id="快捷键"><a href="#快捷键" class="headerlink" title="快捷键"></a>快捷键</h2><ul><li>Tab：命令和文件名补全；</li><li>Ctrl+C：中断正在运行的程序；</li><li>Ctrl+D：结束键盘输入（End Of File，EOF）</li></ul><h2 id="求助"><a href="#求助" class="headerlink" title="求助"></a>求助</h2><h3 id="1-–help"><a href="#1-–help" class="headerlink" title="1. –help"></a>1. –help</h3><p>指令的基本用法与选项介绍。</p><h3 id="2-man"><a href="#2-man" class="headerlink" title="2. man"></a>2. man</h3><p>man 是 manual 的缩写，将指令的具体信息显示出来。</p><p>当执行<code>man date</code>时，有 DATE(1) 出现，其中的数字代表指令的类型，常用的数字及其类型如下：</p><table><thead><tr><th align="center">代号</th><th>类型</th></tr></thead><tbody><tr><td align="center">1</td><td>用户在 shell 环境中可以操作的指令或者可执行文件</td></tr><tr><td align="center">5</td><td>配置文件</td></tr><tr><td align="center">8</td><td>系统管理员可以使用的管理指令</td></tr></tbody></table><h3 id="3-info"><a href="#3-info" class="headerlink" title="3. info"></a>3. info</h3><p>info 与 man 类似，但是 info 将文档分成一个个页面，每个页面可以进行跳转。</p><h3 id="4-doc"><a href="#4-doc" class="headerlink" title="4. doc"></a>4. doc</h3><p>&#x2F;usr&#x2F;share&#x2F;doc 存放着软件的一整套说明文件。</p><h2 id="关机"><a href="#关机" class="headerlink" title="关机"></a>关机</h2><h3 id="1-who"><a href="#1-who" class="headerlink" title="1. who"></a>1. who</h3><p>在关机前需要先使用 who 命令查看有没有其它用户在线。</p><h3 id="2-sync"><a href="#2-sync" class="headerlink" title="2. sync"></a>2. sync</h3><p>为了加快对磁盘文件的读写速度，位于内存中的文件数据不会立即同步到磁盘上，因此关机之前需要先进行 sync 同步操作。</p><h3 id="3-shutdown"><a href="#3-shutdown" class="headerlink" title="3. shutdown"></a>3. shutdown</h3><figure class="highlight html"><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><span class="line"># shutdown [-krhc] 时间 [信息]</span><br><span class="line">-k ： 不会关机，只是发送警告信息，通知所有在线的用户</span><br><span class="line">-r ： 将系统的服务停掉后就重新启动</span><br><span class="line">-h ： 将系统的服务停掉后就立即关机</span><br><span class="line">-c ： 取消已经在进行的 shutdown 指令内容</span><br></pre></td></tr></table></figure><h2 id="PATH"><a href="#PATH" class="headerlink" title="PATH"></a>PATH</h2><p>可以在环境变量 PATH 中声明可执行文件的路径，路径之间用 : 分隔。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin</span><br></pre></td></tr></table></figure><h2 id="sudo"><a href="#sudo" class="headerlink" title="sudo"></a>sudo</h2><p>sudo 允许一般用户使用 root 可执行的命令，不过只有在 &#x2F;etc&#x2F;sudoers 配置文件中添加的用户才能使用该指令。</p><h2 id="包管理工具"><a href="#包管理工具" class="headerlink" title="包管理工具"></a>包管理工具</h2><p>RPM 和 DPKG 为最常见的两类软件包管理工具：</p><ul><li>RPM 全称为 Redhat Package Manager，最早由 Red Hat 公司制定实施，随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。</li><li>与 RPM 进行竞争的是基于 Debian 操作系统 (Ubuntu) 的 DEB 软件包管理工具 DPKG，全称为 Debian Package，功能方面与 RPM 相似。</li></ul><p>YUM 基于 RPM，具有依赖管理功能，并具有软件升级的功能。</p><h2 id="发行版"><a href="#发行版" class="headerlink" title="发行版"></a>发行版</h2><p>Linux 发行版是 Linux 内核及各种应用软件的集成版本。</p><table><thead><tr><th align="center">基于的包管理工具</th><th align="center">商业发行版</th><th align="center">社区发行版</th></tr></thead><tbody><tr><td align="center">RPM</td><td align="center">Red Hat</td><td align="center">Fedora &#x2F; CentOS</td></tr><tr><td align="center">DPKG</td><td align="center">Ubuntu</td><td align="center">Debian</td></tr></tbody></table><h2 id="VIM-三个模式"><a href="#VIM-三个模式" class="headerlink" title="VIM 三个模式"></a>VIM 三个模式</h2><ul><li>一般指令模式（Command mode）：VIM 的默认模式，可以用于移动游标查看内容；</li><li>编辑模式（Insert mode）：按下 “i” 等按键之后进入，可以对文本进行编辑；</li><li>指令列模式（Bottom-line mode）：按下 “:” 按键之后进入，用于保存退出等操作。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/5942debd-fc00-477a-b390-7c5692cc8070.jpg" width="400"/> </div><br><p>在指令列模式下，有以下命令用于离开或者保存文件。</p><table><thead><tr><th align="center">命令</th><th align="center">作用</th></tr></thead><tbody><tr><td align="center">:w</td><td align="center">写入磁盘</td></tr><tr><td align="center">:w!</td><td align="center">当文件为只读时，强制写入磁盘。到底能不能写入，与用户对该文件的权限有关</td></tr><tr><td align="center">:q</td><td align="center">离开</td></tr><tr><td align="center">:q!</td><td align="center">强制离开不保存</td></tr><tr><td align="center">:wq</td><td align="center">写入磁盘后离开</td></tr><tr><td align="center">:wq!</td><td align="center">强制写入磁盘后离开</td></tr></tbody></table><h2 id="GNU"><a href="#GNU" class="headerlink" title="GNU"></a>GNU</h2><p>GNU 计划，译为革奴计划，它的目标是创建一套完全自由的操作系统，称为 GNU，其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议，包含了以下内容：</p><ul><li>以任何目的运行此程序的自由；</li><li>再复制的自由；</li><li>改进此程序，并公开发布改进的自由。</li></ul><h2 id="开源协议"><a href="#开源协议" class="headerlink" title="开源协议"></a>开源协议</h2><ul><li><a href="https://choosealicense.com/">Choose an open source license</a></li><li><a href="http://www.ruanyifeng.com/blog/2011/05/how_to_choose_free_software_licenses.html">如何选择开源许可证？</a></li></ul><h1 id="二、磁盘"><a href="#二、磁盘" class="headerlink" title="二、磁盘"></a>二、磁盘</h1><h2 id="磁盘接口"><a href="#磁盘接口" class="headerlink" title="磁盘接口"></a>磁盘接口</h2><h3 id="1-IDE"><a href="#1-IDE" class="headerlink" title="1. IDE"></a>1. IDE</h3><p>IDE（ATA）全称 Advanced Technology Attachment，接口速度最大为 133MB&#x2F;s，因为并口线的抗干扰性太差，且排线占用空间较大，不利电脑内部散热，已逐渐被 SATA 所取代。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/924914c0-660c-4e4a-bbc0-1df1146e7516.jpg" width="400"/> </div><br><h3 id="2-SATA"><a href="#2-SATA" class="headerlink" title="2. SATA"></a>2. SATA</h3><p>SATA 全称 Serial ATA，也就是使用串口的 ATA 接口，抗干扰性强，且对数据线的长度要求比 ATA 低很多，支持热插拔等功能。SATA-II 的接口速度为 300MiB&#x2F;s，而新的 SATA-III 标准可达到 600MiB&#x2F;s 的传输速度。SATA 的数据线也比 ATA 的细得多，有利于机箱内的空气流通，整理线材也比较方便。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/f9f2a16b-4843-44d1-9759-c745772e9bcf.jpg" width=""/> </div><br><h3 id="3-SCSI"><a href="#3-SCSI" class="headerlink" title="3. SCSI"></a>3. SCSI</h3><p>SCSI 全称是 Small Computer System Interface（小型机系统接口），经历多代的发展，从早期的 SCSI-II 到目前的 Ultra320 SCSI 以及 Fiber-Channel（光纤通道），接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用，因此会使用较为先进的技术，如碟片转速 15000rpm 的高转速，且传输时 CPU 占用率较低，但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/f0574025-c514-49f5-a591-6d6a71f271f7.jpg" width=""/> </div><br><h3 id="4-SAS"><a href="#4-SAS" class="headerlink" title="4. SAS"></a>4. SAS</h3><p>SAS（Serial Attached SCSI）是新一代的 SCSI 技术，和 SATA 硬盘相同，都是采取序列式技术以获得更高的传输速度，可达到 6Gb&#x2F;s。此外也透过缩小连接线改善系统内部空间等。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/6729baa0-57d7-4817-b3aa-518cbccf824c.jpg" width=""/> </div><br><h2 id="磁盘的文件名"><a href="#磁盘的文件名" class="headerlink" title="磁盘的文件名"></a>磁盘的文件名</h2><p>Linux 中每个硬件都被当做一个文件，包括磁盘。磁盘以磁盘接口类型进行命名，常见磁盘的文件名如下：</p><ul><li>IDE 磁盘：&#x2F;dev&#x2F;hd[a-d]</li><li>SATA&#x2F;SCSI&#x2F;SAS 磁盘：&#x2F;dev&#x2F;sd[a-p]</li></ul><p>其中文件名后面的序号的确定与系统检测到磁盘的顺序有关，而与磁盘所插入的插槽位置无关。</p><h1 id="三、分区"><a href="#三、分区" class="headerlink" title="三、分区"></a>三、分区</h1><h2 id="分区表"><a href="#分区表" class="headerlink" title="分区表"></a>分区表</h2><p>磁盘分区表主要有两种格式，一种是限制较多的 MBR 分区表，一种是较新且限制较少的 GPT 分区表。</p><h3 id="1-MBR"><a href="#1-MBR" class="headerlink" title="1. MBR"></a>1. MBR</h3><p>MBR 中，第一个扇区最重要，里面有主要开机记录（Master boot record, MBR）及分区表（partition table），其中主要开机记录占 446 bytes，分区表占 64 bytes。</p><p>分区表只有 64 bytes，最多只能存储 4 个分区，这 4 个分区为主分区（Primary）和扩展分区（Extended）。其中扩展分区只有一个，它使用其它扇区用记录额外的分区表，因此通过扩展分区可以分出更多分区，这些分区称为逻辑分区。</p><p>Linux 也把分区当成文件，分区文件的命名方式为：磁盘文件名 + 编号，例如 &#x2F;dev&#x2F;sda1。注意，逻辑分区的编号从 5 开始。</p><h3 id="2-GPT"><a href="#2-GPT" class="headerlink" title="2. GPT"></a>2. GPT</h3><p>不同的磁盘有不同的扇区大小，例如 512 bytes 和最新磁盘的 4 k。GPT 为了兼容所有磁盘，在定义扇区上使用逻辑区块地址（Logical Block Address, LBA），LBA 默认大小为 512 bytes。</p><p>GPT 第 1 个区块记录了主要开机记录（MBR），紧接着是 33 个区块记录分区信息，并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录，这个部份纪录了分区表本身的位置与大小和备份分区的位置，同时放置了分区表的校验码 (CRC32)，操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误，可以使用备份分区进行恢复。</p><p>GPT 没有扩展分区概念，都是主分区，每个 LAB 可以分 4 个分区，因此总共可以分 4 * 32 &#x3D; 128 个分区。</p><p>MBR 不支持 2.2 TB 以上的硬盘，GPT 则最多支持到 2<sup>33</sup> TB &#x3D; 8 ZB。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/GUID_Partition_Table_Scheme.svg.png" width="400"/> </div><br><h2 id="开机检测程序"><a href="#开机检测程序" class="headerlink" title="开机检测程序"></a>开机检测程序</h2><h3 id="1-BIOS"><a href="#1-BIOS" class="headerlink" title="1. BIOS"></a>1. BIOS</h3><p>BIOS（Basic Input&#x2F;Output System，基本输入输出系统），它是一个固件（嵌入在硬件中的软件），BIOS 程序存放在断电后内容不会丢失的只读内存中。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/50831a6f-2777-46ea-a571-29f23c85cc21.jpg"/> </div><br><p>BIOS 是开机的时候计算机执行的第一个程序，这个程序知道可以开机的磁盘，并读取磁盘第一个扇区的主要开机记录（MBR），由主要开机记录（MBR）执行其中的开机管理程序，这个开机管理程序会加载操作系统的核心文件。</p><p>主要开机记录（MBR）中的开机管理程序提供以下功能：选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导，只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上，在启动开机管理程序时，就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。</p><p>下图中，第一扇区的主要开机记录（MBR）中的开机管理程序提供了两个选单：M1、M2，M1 指向了 Windows 操作系统，而 M2 指向其它分区的启动扇区，里面包含了另外一个开机管理程序，提供了一个指向 Linux 的选单。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/f900f266-a323-42b2-bc43-218fdb8811a8.jpg" width="600"/> </div><br><p>安装多重引导，最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉主要开机记录（MBR），而 Linux 可以选择将开机管理程序安装在主要开机记录（MBR）或者其它分区的启动扇区，并且可以设置开机管理程序的选单。</p><h3 id="2-UEFI"><a href="#2-UEFI" class="headerlink" title="2. UEFI"></a>2. UEFI</h3><p>BIOS 不可以读取 GPT 分区表，而 UEFI 可以。</p><h1 id="四、文件系统"><a href="#四、文件系统" class="headerlink" title="四、文件系统"></a>四、文件系统</h1><h2 id="分区与文件系统"><a href="#分区与文件系统" class="headerlink" title="分区与文件系统"></a>分区与文件系统</h2><p>对分区进行格式化是为了在分区上建立文件系统。一个分区通常只能格式化为一个文件系统，但是磁盘阵列等技术可以将一个分区格式化为多个文件系统。</p><h2 id="组成"><a href="#组成" class="headerlink" title="组成"></a>组成</h2><p>最主要的几个组成部分如下：</p><ul><li>inode：一个文件占用一个 inode，记录文件的属性，同时记录此文件的内容所在的 block 编号；</li><li>block：记录文件的内容，文件太大时，会占用多个 block。</li></ul><p>除此之外还包括：</p><ul><li>superblock：记录文件系统的整体信息，包括 inode 和 block 的总量、使用量、剩余量，以及文件系统的格式与相关信息等；</li><li>block bitmap：记录 block 是否被使用的位域。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/BSD_disk.png" width="800"/> </div><br><h2 id="文件读取"><a href="#文件读取" class="headerlink" title="文件读取"></a>文件读取</h2><p>对于 Ext2 文件系统，当要读取一个文件的内容时，先在 inode 中去查找文件内容所在的所有 block，然后把所有 block 的内容读出来。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/83185315-793a-453a-a927-5e8d92b5c0ef.jpg"/> </div><br><p>而对于 FAT 文件系统，它没有 inode，每个 block 中存储着下一个 block 的编号。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/075e1977-7846-4928-96c8-bb5b0268693c.jpg"/> </div><br><h2 id="磁盘碎片"><a href="#磁盘碎片" class="headerlink" title="磁盘碎片"></a>磁盘碎片</h2><p>指一个文件内容所在的 block 过于分散。</p><h2 id="block"><a href="#block" class="headerlink" title="block"></a>block</h2><p>在 Ext2 文件系统中所支持的 block 大小有 1K，2K 及 4K 三种，不同的大小限制了单个文件和文件系统的最大大小。</p><table><thead><tr><th align="center">大小</th><th align="center">1KB</th><th align="center">2KB</th><th align="center">4KB</th></tr></thead><tbody><tr><td align="center">最大单一文件</td><td align="center">16GB</td><td align="center">256GB</td><td align="center">2TB</td></tr><tr><td align="center">最大文件系统</td><td align="center">2TB</td><td align="center">8TB</td><td align="center">16TB</td></tr></tbody></table><p>一个 block 只能被一个文件所使用，未使用的部分直接浪费了。因此如果需要存储大量的小文件，那么最好选用比较小的 block。</p><h2 id="inode"><a href="#inode" class="headerlink" title="inode"></a>inode</h2><p>inode 具体包含以下信息：</p><ul><li>权限 (read&#x2F;write&#x2F;excute)；</li><li>拥有者与群组 (owner&#x2F;group)；</li><li>容量；</li><li>建立或状态改变的时间 (ctime)；</li><li>最近一次的读取时间 (atime)；</li><li>最近修改的时间 (mtime)；</li><li>定义文件特性的旗标 (flag)，如 SetUID…；</li><li>该文件真正内容的指向 (pointer)。</li></ul><p>inode 具有以下特点：</p><ul><li>每个 inode 大小均固定为 128 bytes (新的 ext4 与 xfs 可设定到 256 bytes)；</li><li>每个文件都仅会占用一个 inode。</li></ul><p>inode 中记录了文件内容所在的 block 编号，但是每个 block 非常小，一个大文件随便都需要几十万的 block。而一个 inode 大小有限，无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用是指，让 inode 记录的引用 block 块记录引用信息。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/inode_with_signatures.jpg" width="600"/> </div><br><h2 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h2><p>建立一个目录时，会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。</p><p>可以看出文件的 inode 本身不记录文件名，文件名记录在目录中，因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。</p><h2 id="日志"><a href="#日志" class="headerlink" title="日志"></a>日志</h2><p>如果突然断电，那么文件系统会发生错误，例如断电前只修改了 block bitmap，而还没有将数据真正写入 block 中。</p><p>ext3&#x2F;ext4 文件系统引入了日志功能，可以利用日志来修复文件系统。</p><h2 id="挂载"><a href="#挂载" class="headerlink" title="挂载"></a>挂载</h2><p>挂载利用目录作为文件系统的进入点，也就是说，进入目录之后就可以读取文件系统的数据。</p><h2 id="目录配置"><a href="#目录配置" class="headerlink" title="目录配置"></a>目录配置</h2><p>为了使不同 Linux 发行版本的目录结构保持一致性，Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下：</p><ul><li>&#x2F; (root, 根目录)</li><li>&#x2F;usr (unix software resource)：所有系统默认软件都会安装到这个目录；</li><li>&#x2F;var (variable)：存放系统或程序运行过程中的数据文件。</li></ul><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/linux-filesystem.png" width=""/> </div><br><h1 id="五、文件"><a href="#五、文件" class="headerlink" title="五、文件"></a>五、文件</h1><h2 id="文件属性"><a href="#文件属性" class="headerlink" title="文件属性"></a>文件属性</h2><p>用户分为三种：文件拥有者、群组以及其它人，对不同的用户有不同的文件权限。</p><p>使用 ls 查看一个文件时，会显示一个文件的信息，例如 <code>drwxr-xr-x. 3 root root 17 May 6 00:14 .config</code>，对这个信息的解释如下：</p><ul><li>drwxr-xr-x：文件类型以及权限，第 1 位为文件类型字段，后 9 位为文件权限字段</li><li>3：链接数</li><li>root：文件拥有者</li><li>root：所属群组</li><li>17：文件大小</li><li>May 6 00:14：文件最后被修改的时间</li><li>.config：文件名</li></ul><p>常见的文件类型及其含义有：</p><ul><li>d：目录</li><li>-：文件</li><li>l：链接文件</li></ul><p>9 位的文件权限字段中，每 3 个为一组，共 3 组，每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限，表示可读、可写、可执行。</p><p>文件时间有以下三种：</p><ul><li>modification time (mtime)：文件的内容更新就会更新；</li><li>status time (ctime)：文件的状态（权限、属性）更新就会更新；</li><li>access time (atime)：读取文件时就会更新。</li></ul><h2 id="文件与目录的基本操作"><a href="#文件与目录的基本操作" class="headerlink" title="文件与目录的基本操作"></a>文件与目录的基本操作</h2><h3 id="1-ls"><a href="#1-ls" class="headerlink" title="1. ls"></a>1. ls</h3><p>列出文件或者目录的信息，目录的信息就是其中包含的文件。</p><figure class="highlight html"><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><span class="line"># ls [-aAdfFhilnrRSt] file|dir</span><br><span class="line">-a ：列出全部的文件</span><br><span class="line">-d ：仅列出目录本身</span><br><span class="line">-l ：以长数据串行列出，包含文件的属性与权限等等数据</span><br></pre></td></tr></table></figure><h3 id="2-cd"><a href="#2-cd" class="headerlink" title="2. cd"></a>2. cd</h3><p>更换当前目录。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cd [相对路径或绝对路径]</span><br></pre></td></tr></table></figure><h3 id="3-mkdir"><a href="#3-mkdir" class="headerlink" title="3. mkdir"></a>3. mkdir</h3><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><span class="line"># mkdir [-mp] 目录名称</span><br><span class="line">-m ：配置目录权限</span><br><span class="line">-p ：递归创建目录</span><br></pre></td></tr></table></figure><h3 id="4-rmdir"><a href="#4-rmdir" class="headerlink" title="4. rmdir"></a>4. rmdir</h3><p>删除目录，目录必须为空。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">rmdir [-p] 目录名称</span><br><span class="line">-p ：递归删除目录</span><br></pre></td></tr></table></figure><h3 id="5-touch"><a href="#5-touch" class="headerlink" title="5. touch"></a>5. touch</h3><p>更新文件时间或者建立新文件。</p><figure class="highlight html"><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><span class="line"># touch [-acdmt] filename</span><br><span class="line">-a ： 更新 atime</span><br><span class="line">-c ： 更新 ctime，若该文件不存在则不建立新文件</span><br><span class="line">-m ： 更新 mtime</span><br><span class="line">-d ： 后面可以接更新日期而不使用当前日期，也可以使用 --date=&quot;日期或时间&quot;</span><br><span class="line">-t ： 后面可以接更新时间而不使用当前时间，格式为[YYYYMMDDhhmm]</span><br></pre></td></tr></table></figure><h3 id="6-cp"><a href="#6-cp" class="headerlink" title="6. cp"></a>6. cp</h3><p>复制文件。</p><p>如果源文件有两个以上，则目的文件一定要是目录才行。</p><figure class="highlight html"><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><span class="line">cp [-adfilprsu] source destination</span><br><span class="line">-a ：相当于 -dr --preserve=all 的意思，至于 dr 请参考下列说明</span><br><span class="line">-d ：若来源文件为链接文件，则复制链接文件属性而非文件本身</span><br><span class="line">-i ：若目标文件已经存在时，在覆盖前会先询问</span><br><span class="line">-p ：连同文件的属性一起复制过去</span><br><span class="line">-r ：递归持续复制</span><br><span class="line">-u ：destination 比 source 旧才更新 destination，或 destination 不存在的情况下才复制</span><br><span class="line">--preserve=all ：除了 -p 的权限相关参数外，还加入 SELinux 的属性, links, xattr 等也复制了</span><br></pre></td></tr></table></figure><h3 id="7-rm"><a href="#7-rm" class="headerlink" title="7. rm"></a>7. rm</h3><p>删除文件。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># rm [-fir] 文件或目录</span><br><span class="line">-r ：递归删除</span><br></pre></td></tr></table></figure><h3 id="8-mv"><a href="#8-mv" class="headerlink" title="8. mv"></a>8. mv</h3><p>移动文件。</p><figure class="highlight html"><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><span class="line"># mv [-fiu] source destination</span><br><span class="line"># mv [options] source1 source2 source3 .... directory</span><br><span class="line">-f ： force 强制的意思，如果目标文件已经存在，不会询问而直接覆盖</span><br></pre></td></tr></table></figure><h2 id="修改权限"><a href="#修改权限" class="headerlink" title="修改权限"></a>修改权限</h2><p>可以将一组权限用数字来表示，此时一组权限的 3 个位当做二进制数字的位，从左到右每个位的权值为 4、2、1，即每个权限对应的数字权值为 r : 4、w : 2、x : 1。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"># chmod [-R] xyz dirname/filename</span><br></pre></td></tr></table></figure><p>示例：将 .bashrc 文件的权限修改为 -rwxr-xr–。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"># chmod 754 .bashrc</span><br></pre></td></tr></table></figure><p>也可以使用符号来设定权限。</p><figure class="highlight html"><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><span class="line"># chmod [ugoa]  [+-=] [rwx] dirname/filename</span><br><span class="line">- u：拥有者</span><br><span class="line">- g：所属群组</span><br><span class="line">- o：其他人</span><br><span class="line">- a：所有人</span><br><span class="line">- +：添加权限</span><br><span class="line">- -：移除权限</span><br><span class="line">- =：设定权限</span><br></pre></td></tr></table></figure><p>示例：为 .bashrc 文件的所有用户添加写权限。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"># chmod a+w .bashrc</span><br></pre></td></tr></table></figure><h2 id="文件默认权限"><a href="#文件默认权限" class="headerlink" title="文件默认权限"></a>文件默认权限</h2><ul><li>文件默认权限：文件默认没有可执行权限，因此为 666，也就是 -rw-rw-rw- 。</li><li>目录默认权限：目录必须要能够进入，也就是必须拥有可执行权限，因此为 777 ，也就是 drwxrwxrwx。</li></ul><p>可以通过 umask 设置或者查看文件的默认权限，通常以掩码的形式来表示，例如 002 表示其它用户的权限去除了一个 2 的权限，也就是写权限，因此建立新文件时默认的权限为 -rw-rw-r–。</p><h2 id="目录的权限"><a href="#目录的权限" class="headerlink" title="目录的权限"></a>目录的权限</h2><p>文件名不是存储在一个文件的内容中，而是存储在一个文件所在的目录中。因此，拥有文件的 w 权限并不能对文件名进行修改。</p><p>目录存储文件列表，一个目录的权限也就是对其文件列表的权限。因此，目录的 r 权限表示可以读取文件列表；w 权限表示可以修改文件列表，具体来说，就是添加删除文件，对文件名进行修改；x 权限可以让该目录成为工作目录，x 权限是 r 和 w 权限的基础，如果不能使一个目录成为工作目录，也就没办法读取文件列表以及对文件列表进行修改了。</p><h2 id="链接"><a href="#链接" class="headerlink" title="链接"></a>链接</h2><figure class="highlight html"><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><span class="line"># ln [-sf] source_filename dist_filename</span><br><span class="line">-s ：默认是 hard link，加 -s 为 symbolic link</span><br><span class="line">-f ：如果目标文件存在时，先删除目标文件</span><br></pre></td></tr></table></figure><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/b8081c84-62c4-4019-b3ee-4bd0e443d647.jpg"/> </div><br><h3 id="1-实体链接"><a href="#1-实体链接" class="headerlink" title="1. 实体链接"></a>1. 实体链接</h3><p>在目录下创建一个条目，记录着文件名与 inode 编号，这个 inode 就是源文件的 inode。</p><p>删除任意一个条目，文件还是存在，只要引用数量不为 0。</p><p>有以下限制：不能跨越文件系统、不能对目录进行链接。</p><figure class="highlight html"><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><span class="line"># ln /etc/crontab .</span><br><span class="line"># ll -i /etc/crontab crontab</span><br><span class="line">34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab</span><br><span class="line">34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab</span><br></pre></td></tr></table></figure><h3 id="2-符号链接"><a href="#2-符号链接" class="headerlink" title="2. 符号链接"></a>2. 符号链接</h3><p>符号链接文件保存着源文件所在的绝对路径，在读取时会定位到源文件上，可以理解为 Windows 的快捷方式。</p><p>当源文件被删除了，链接文件就打不开了。</p><p>可以为目录建立链接。</p><figure class="highlight html"><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><span class="line"># ll -i /etc/crontab /root/crontab2</span><br><span class="line">34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab</span><br><span class="line">53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -&gt; /etc/crontab</span><br></pre></td></tr></table></figure><h2 id="获取文件内容"><a href="#获取文件内容" class="headerlink" title="获取文件内容"></a>获取文件内容</h2><h3 id="1-cat"><a href="#1-cat" class="headerlink" title="1. cat"></a>1. cat</h3><p>取得文件内容。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># cat [-AbEnTv] filename</span><br><span class="line">-n ：打印出行号，连同空白行也会有行号，-b 不会</span><br></pre></td></tr></table></figure><h3 id="2-tac"><a href="#2-tac" class="headerlink" title="2. tac"></a>2. tac</h3><p>是 cat 的反向操作，从最后一行开始打印。</p><h3 id="3-more"><a href="#3-more" class="headerlink" title="3. more"></a>3. more</h3><p>和 cat 不同的是它可以一页一页查看文件内容，比较适合大文件的查看。</p><h3 id="4-less"><a href="#4-less" class="headerlink" title="4. less"></a>4. less</h3><p>和 more 类似，但是多了一个向前翻页的功能。</p><h3 id="5-head"><a href="#5-head" class="headerlink" title="5. head"></a>5. head</h3><p>取得文件前几行。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># head [-n number] filename</span><br><span class="line">-n ：后面接数字，代表显示几行的意思</span><br></pre></td></tr></table></figure><h3 id="6-tail"><a href="#6-tail" class="headerlink" title="6. tail"></a>6. tail</h3><p>是 head 的反向操作，只是取得是后几行。</p><h3 id="7-od"><a href="#7-od" class="headerlink" title="7. od"></a>7. od</h3><p>以字符或者十六进制的形式显示二进制文件。</p><h2 id="指令与文件搜索"><a href="#指令与文件搜索" class="headerlink" title="指令与文件搜索"></a>指令与文件搜索</h2><h3 id="1-which"><a href="#1-which" class="headerlink" title="1. which"></a>1. which</h3><p>指令搜索。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># which [-a] command</span><br><span class="line">-a ：将所有指令列出，而不是只列第一个</span><br></pre></td></tr></table></figure><h3 id="2-whereis"><a href="#2-whereis" class="headerlink" title="2. whereis"></a>2. whereis</h3><p>文件搜索。速度比较快，因为它只搜索几个特定的目录。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"># whereis [-bmsu] dirname/filename</span><br></pre></td></tr></table></figure><h3 id="3-locate"><a href="#3-locate" class="headerlink" title="3. locate"></a>3. locate</h3><p>文件搜索。可以用关键字或者正则表达式进行搜索。</p><p>locate 使用 &#x2F;var&#x2F;lib&#x2F;mlocate&#x2F; 这个数据库来进行搜索，它存储在内存中，并且每天更新一次，所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># locate [-ir] keyword</span><br><span class="line">-r：正则表达式</span><br></pre></td></tr></table></figure><h3 id="4-find"><a href="#4-find" class="headerlink" title="4. find"></a>4. find</h3><p>文件搜索。可以使用文件的属性和权限进行搜索。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># find [basedir] [option]</span><br><span class="line">example: find . -name &quot;shadow*&quot;</span><br></pre></td></tr></table></figure><p>（一）与时间有关的选项</p><figure class="highlight html"><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><span class="line">-mtime  n ：列出在 n 天前的那一天修改过内容的文件</span><br><span class="line">-mtime +n ：列出在 n 天之前 (不含 n 天本身) 修改过内容的文件</span><br><span class="line">-mtime -n ：列出在 n 天之内 (含 n 天本身) 修改过内容的文件</span><br><span class="line">-newer file ： 列出比 file 更新的文件</span><br></pre></td></tr></table></figure><p>+4、4 和 -4 的指示的时间范围如下：</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/658fc5e7-79c0-4247-9445-d69bf194c539.png" width=""/> </div><br><p>（二）与文件拥有者和所属群组有关的选项</p><figure class="highlight html"><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><span class="line">-uid n</span><br><span class="line">-gid n</span><br><span class="line">-user name</span><br><span class="line">-group name</span><br><span class="line">-nouser ：搜索拥有者不存在 /etc/passwd 的文件</span><br><span class="line">-nogroup：搜索所属群组不存在于 /etc/group 的文件</span><br></pre></td></tr></table></figure><p>（三）与文件权限和名称有关的选项</p><figure class="highlight html"><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><span class="line">-name filename</span><br><span class="line">-size [+-]SIZE：搜寻比 SIZE 还要大 (+) 或小 (-) 的文件。这个 SIZE 的规格有：c: 代表 byte，k: 代表 1024bytes。所以，要找比 50KB 还要大的文件，就是 -size +50k</span><br><span class="line">-type TYPE</span><br><span class="line">-perm mode  ：搜索权限等于 mode 的文件</span><br><span class="line">-perm -mode ：搜索权限包含 mode 的文件</span><br><span class="line">-perm /mode ：搜索权限包含任一 mode 的文件</span><br></pre></td></tr></table></figure><h1 id="六、压缩与打包"><a href="#六、压缩与打包" class="headerlink" title="六、压缩与打包"></a>六、压缩与打包</h1><h2 id="压缩文件名"><a href="#压缩文件名" class="headerlink" title="压缩文件名"></a>压缩文件名</h2><p>Linux 底下有很多压缩文件名，常见的如下：</p><table><thead><tr><th>扩展名</th><th>压缩程序</th></tr></thead><tbody><tr><td>*.Z</td><td>compress</td></tr><tr><td>*.zip</td><td>zip</td></tr><tr><td>*.gz</td><td>gzip</td></tr><tr><td>*.bz2</td><td>bzip2</td></tr><tr><td>*.xz</td><td>xz</td></tr><tr><td>*.tar</td><td>tar 程序打包的数据，没有经过压缩</td></tr><tr><td>*.tar.gz</td><td>tar 程序打包的文件，经过 gzip 的压缩</td></tr><tr><td>*.tar.bz2</td><td>tar 程序打包的文件，经过 bzip2 的压缩</td></tr><tr><td>*.tar.xz</td><td>tar 程序打包的文件，经过 xz 的压缩</td></tr></tbody></table><h2 id="压缩指令"><a href="#压缩指令" class="headerlink" title="压缩指令"></a>压缩指令</h2><h3 id="1-gzip"><a href="#1-gzip" class="headerlink" title="1. gzip"></a>1. gzip</h3><p>gzip 是 Linux 使用最广的压缩指令，可以解开 compress、zip 与 gzip 所压缩的文件。</p><p>经过 gzip 压缩过，源文件就不存在了。</p><p>有 9 个不同的压缩等级可以使用。</p><p>可以使用 zcat、zmore、zless 来读取压缩文件的内容。</p><figure class="highlight html"><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><span class="line">$ gzip [-cdtv#] filename</span><br><span class="line">-c ：将压缩的数据输出到屏幕上</span><br><span class="line">-d ：解压缩</span><br><span class="line">-t ：检验压缩文件是否出错</span><br><span class="line">-v ：显示压缩比等信息</span><br><span class="line">-# ： # 为数字的意思，代表压缩等级，数字越大压缩比越高，默认为 6</span><br></pre></td></tr></table></figure><h3 id="2-bzip2"><a href="#2-bzip2" class="headerlink" title="2. bzip2"></a>2. bzip2</h3><p>提供比 gzip 更高的压缩比。</p><p>查看命令：bzcat、bzmore、bzless、bzgrep。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ bzip2 [-cdkzv#] filename</span><br><span class="line">-k ：保留源文件</span><br></pre></td></tr></table></figure><h3 id="3-xz"><a href="#3-xz" class="headerlink" title="3. xz"></a>3. xz</h3><p>提供比 bzip2 更佳的压缩比。</p><p>可以看到，gzip、bzip2、xz 的压缩比不断优化。不过要注意的是，压缩比越高，压缩的时间也越长。</p><p>查看命令：xzcat、xzmore、xzless、xzgrep。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ xz [-dtlkc#] filename</span><br></pre></td></tr></table></figure><h2 id="打包"><a href="#打包" class="headerlink" title="打包"></a>打包</h2><p>压缩指令只能对一个文件进行压缩，而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包，也可以使用 gip、bzip2、xz 将打包文件进行压缩。</p><figure class="highlight html"><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><span class="line">$ tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename...  ==打包压缩</span><br><span class="line">$ tar [-z|-j|-J] [tv] [-f 已有的 tar 文件]              ==查看</span><br><span class="line">$ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录]    ==解压缩</span><br><span class="line">-z ：使用 zip；</span><br><span class="line">-j ：使用 bzip2；</span><br><span class="line">-J ：使用 xz；</span><br><span class="line">-c ：新建打包文件；</span><br><span class="line">-t ：查看打包文件里面有哪些文件；</span><br><span class="line">-x ：解打包或解压缩的功能；</span><br><span class="line">-v ：在压缩/解压缩的过程中，显示正在处理的文件名；</span><br><span class="line">-f : filename：要处理的文件；</span><br><span class="line">-C 目录 ： 在特定目录解压缩。</span><br></pre></td></tr></table></figure><table><thead><tr><th align="center">使用方式</th><th>命令</th></tr></thead><tbody><tr><td align="center">打包压缩</td><td>tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称</td></tr><tr><td align="center">查 看</td><td>tar -jtv -f filename.tar.bz2</td></tr><tr><td align="center">解压缩</td><td>tar -jxv -f filename.tar.bz2 -C 要解压缩的目录</td></tr></tbody></table><h1 id="七、Bash"><a href="#七、Bash" class="headerlink" title="七、Bash"></a>七、Bash</h1><p>可以通过 Shell 请求内核提供服务，Bash 正是 Shell 的一种。</p><h2 id="特性"><a href="#特性" class="headerlink" title="特性"></a>特性</h2><ul><li>命令历史：记录使用过的命令</li><li>命令与文件补全：快捷键：tab</li><li>命名别名：例如 lm 是 ls -al 的别名</li><li>shell scripts</li><li>通配符：例如 ls -l &#x2F;usr&#x2F;bin&#x2F;X* 列出 &#x2F;usr&#x2F;bin 下面所有以 X 开头的文件</li></ul><h2 id="变量操作"><a href="#变量操作" class="headerlink" title="变量操作"></a>变量操作</h2><p>对一个变量赋值直接使用 &#x3D;。</p><p>对变量取用需要在变量前加上 $ ，也可以用 ${} 的形式；</p><p>输出变量使用 echo 命令。</p><figure class="highlight bash"><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><span class="line">$ x=abc</span><br><span class="line">$ <span class="built_in">echo</span> <span class="variable">$x</span></span><br><span class="line">$ <span class="built_in">echo</span> <span class="variable">$&#123;x&#125;</span></span><br></pre></td></tr></table></figure><p>变量内容如果有空格，必须使用双引号或者单引号。</p><ul><li>双引号内的特殊字符可以保留原本特性，例如 x&#x3D;”lang is $LANG”，则 x 的值为 lang is zh_TW.UTF-8；</li><li>单引号内的特殊字符就是特殊字符本身，例如 x&#x3D;’lang is $LANG’，则 x 的值为 lang is $LANG。</li></ul><p>可以使用 `指令` 或者 $(指令) 的方式将指令的执行结果赋值给变量。例如 version&#x3D;$(uname -r)，则 version 的值为 4.15.0-22-generic。</p><p>可以使用 export 命令将自定义变量转成环境变量，环境变量可以在子程序中使用，所谓子程序就是由当前 Bash 而产生的子 Bash。</p><p>Bash 的变量可以声明为数组和整数数字。注意数字类型没有浮点数。如果不进行声明，默认是字符串类型。变量的声明使用 declare 命令：</p><figure class="highlight html"><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><span class="line">$ declare [-aixr] variable</span><br><span class="line">-a ： 定义为数组类型</span><br><span class="line">-i ： 定义为整数类型</span><br><span class="line">-x ： 定义为环境变量</span><br><span class="line">-r ： 定义为 readonly 类型</span><br></pre></td></tr></table></figure><p>使用 [ ] 来对数组进行索引操作：</p><figure class="highlight bash"><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><span class="line">$ array[1]=a</span><br><span class="line">$ array[2]=b</span><br><span class="line">$ <span class="built_in">echo</span> <span class="variable">$&#123;array[1]&#125;</span></span><br></pre></td></tr></table></figure><h2 id="指令搜索顺序"><a href="#指令搜索顺序" class="headerlink" title="指令搜索顺序"></a>指令搜索顺序</h2><ul><li>以绝对或相对路径来执行指令，例如 &#x2F;bin&#x2F;ls 或者 .&#x2F;ls ；</li><li>由别名找到该指令来执行；</li><li>由 Bash 内建的指令来执行；</li><li>按 $PATH 变量指定的搜索路径的顺序找到第一个指令来执行。</li></ul><h2 id="数据流重定向"><a href="#数据流重定向" class="headerlink" title="数据流重定向"></a>数据流重定向</h2><p>重定向指的是使用文件代替标准输入、标准输出和标准错误输出。</p><table><thead><tr><th align="center">1</th><th align="center">代码</th><th align="center">运算符</th></tr></thead><tbody><tr><td align="center">标准输入 (stdin)</td><td align="center">0</td><td align="center">&lt; 或 &lt;&lt;</td></tr><tr><td align="center">标准输出 (stdout)</td><td align="center">1</td><td align="center">&gt; 或 &gt;&gt;</td></tr><tr><td align="center">标准错误输出 (stderr)</td><td align="center">2</td><td align="center">2&gt; 或 2&gt;&gt;</td></tr></tbody></table><p>其中，有一个箭头的表示以覆盖的方式重定向，而有两个箭头的表示以追加的方式重定向。</p><p>可以将不需要的标准输出以及标准错误输出重定向到 &#x2F;dev&#x2F;null，相当于扔进垃圾箱。</p><p>如果需要将标准输出以及标准错误输出同时重定向到一个文件，需要将某个输出转换为另一个输出，例如 2&gt;&amp;1 表示将标准错误输出转换为标准输出。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ find /home -name .bashrc &gt; list 2&gt;&amp;1</span><br></pre></td></tr></table></figure><h1 id="八、管线指令"><a href="#八、管线指令" class="headerlink" title="八、管线指令"></a>八、管线指令</h1><p>管线是将一个命令的标准输出作为另一个命令的标准输入，在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管线。</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><span class="line">$ <span class="built_in">ls</span> -al /etc | less</span><br></pre></td></tr></table></figure><h2 id="提取指令"><a href="#提取指令" class="headerlink" title="提取指令"></a>提取指令</h2><p>cut 对数据进行切分，取出想要的部分。</p><p>切分过程一行一行地进行。</p><figure class="highlight html"><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><span class="line">$ cut</span><br><span class="line">-d ：分隔符</span><br><span class="line">-f ：经过 -d 分隔后，使用 -f n 取出第 n 个区间</span><br><span class="line">-c ：以字符为单位取出区间</span><br></pre></td></tr></table></figure><p>示例 1：last 显示登入者的信息，取出用户名。</p><figure class="highlight html"><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><span class="line">$ last</span><br><span class="line">root pts/1 192.168.201.101 Sat Feb 7 12:35 still logged in</span><br><span class="line">root pts/1 192.168.201.101 Fri Feb 6 12:13 - 18:46 (06:33)</span><br><span class="line">root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16)</span><br><span class="line"></span><br><span class="line">$ last | cut -d &#x27; &#x27; -f 1</span><br></pre></td></tr></table></figure><p>示例 2：将 export 输出的信息，取出第 12 字符以后的所有字符串。</p><figure class="highlight html"><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><span class="line">$ export</span><br><span class="line">declare -x HISTCONTROL=&quot;ignoredups&quot;</span><br><span class="line">declare -x HISTSIZE=&quot;1000&quot;</span><br><span class="line">declare -x HOME=&quot;/home/dmtsai&quot;</span><br><span class="line">declare -x HOSTNAME=&quot;study.centos.vbird&quot;</span><br><span class="line">.....(其他省略).....</span><br><span class="line"></span><br><span class="line">$ export | cut -c 12-</span><br></pre></td></tr></table></figure><h2 id="排序指令"><a href="#排序指令" class="headerlink" title="排序指令"></a>排序指令</h2><p><strong>sort</strong>  用于排序。</p><figure class="highlight html"><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><span class="line">$ sort [-fbMnrtuk] [file or stdin]</span><br><span class="line">-f ：忽略大小写</span><br><span class="line">-b ：忽略最前面的空格</span><br><span class="line">-M ：以月份的名字来排序，例如 JAN，DEC</span><br><span class="line">-n ：使用数字</span><br><span class="line">-r ：反向排序</span><br><span class="line">-u ：相当于 unique，重复的内容只出现一次</span><br><span class="line">-t ：分隔符，默认为 tab</span><br><span class="line">-k ：指定排序的区间</span><br></pre></td></tr></table></figure><p>示例：&#x2F;etc&#x2F;passwd 文件内容以 : 来分隔，要求以第三列进行排序。</p><figure class="highlight html"><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><span class="line">$ cat /etc/passwd | sort -t &#x27;:&#x27; -k 3</span><br><span class="line">root:x:0:0:root:/root:/bin/bash</span><br><span class="line">dmtsai:x:1000:1000:dmtsai:/home/dmtsai:/bin/bash</span><br><span class="line">alex:x:1001:1002::/home/alex:/bin/bash</span><br><span class="line">arod:x:1002:1003::/home/arod:/bin/bash</span><br></pre></td></tr></table></figure><p><strong>uniq</strong>  可以将重复的数据只取一个。</p><figure class="highlight html"><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><span class="line">$ uniq [-ic]</span><br><span class="line">-i ：忽略大小写</span><br><span class="line">-c ：进行计数</span><br></pre></td></tr></table></figure><p>示例：取得每个人的登录总次数</p><figure class="highlight html"><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><span class="line">$ last | cut -d &#x27; &#x27; -f 1 | sort | uniq -c</span><br><span class="line">1</span><br><span class="line">6 (unknown</span><br><span class="line">47 dmtsai</span><br><span class="line">4 reboot</span><br><span class="line">7 root</span><br><span class="line">1 wtmp</span><br></pre></td></tr></table></figure><h2 id="双向输出重定向"><a href="#双向输出重定向" class="headerlink" title="双向输出重定向"></a>双向输出重定向</h2><p>输出重定向会将输出内容重定向到文件中，而  <strong>tee</strong>  不仅能够完成这个功能，还能保留屏幕上的输出。也就是说，使用 tee 指令，一个输出会同时传送到文件和屏幕上。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ tee [-a] file</span><br></pre></td></tr></table></figure><h2 id="字符转换指令"><a href="#字符转换指令" class="headerlink" title="字符转换指令"></a>字符转换指令</h2><p><strong>tr</strong>  用来删除一行中的字符，或者对字符进行替换。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ tr [-ds] SET1 ...</span><br><span class="line">-d ： 删除行中 SET1 这个字符串</span><br></pre></td></tr></table></figure><p>示例，将 last 输出的信息所有小写转换为大写。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ last | tr &#x27;[a-z]&#x27; &#x27;[A-Z]&#x27;</span><br></pre></td></tr></table></figure><p>  <strong>col</strong>  将 tab 字符转为空格字符。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ col [-xb]</span><br><span class="line">-x ： 将 tab 键转换成对等的空格键</span><br></pre></td></tr></table></figure><p><strong>expand</strong>  将 tab 转换一定数量的空格，默认是 8 个。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ expand [-t] file</span><br><span class="line">-t ：tab 转为空格的数量</span><br></pre></td></tr></table></figure><p><strong>join</strong>  将有相同数据的那一行合并在一起。</p><figure class="highlight html"><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><span class="line">$ join [-ti12] file1 file2</span><br><span class="line">-t ：分隔符，默认为空格</span><br><span class="line">-i ：忽略大小写的差异</span><br><span class="line">-1 ：第一个文件所用的比较字段</span><br><span class="line">-2 ：第二个文件所用的比较字段</span><br></pre></td></tr></table></figure><p><strong>paste</strong>  直接将两行粘贴在一起。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ paste [-d] file1 file2</span><br><span class="line">-d ：分隔符，默认为 tab</span><br></pre></td></tr></table></figure><h2 id="分区指令"><a href="#分区指令" class="headerlink" title="分区指令"></a>分区指令</h2><p><strong>split</strong>  将一个文件划分成多个文件。</p><figure class="highlight html"><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><span class="line">$ split [-bl] file PREFIX</span><br><span class="line">-b ：以大小来进行分区，可加单位，例如 b, k, m 等</span><br><span class="line">-l ：以行数来进行分区。</span><br><span class="line">- PREFIX ：分区文件的前导名称</span><br></pre></td></tr></table></figure><h1 id="九、正则表达式"><a href="#九、正则表达式" class="headerlink" title="九、正则表达式"></a>九、正则表达式</h1><h2 id="grep"><a href="#grep" class="headerlink" title="grep"></a>grep</h2><p>g&#x2F;re&#x2F;p（globally search a regular expression and print)，使用正则表示式进行全局查找并打印。</p><figure class="highlight html"><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><span class="line">$ grep [-acinv] [--color=auto] 搜寻字符串 filename</span><br><span class="line">-c ： 统计个数</span><br><span class="line">-i ： 忽略大小写</span><br><span class="line">-n ： 输出行号</span><br><span class="line">-v ： 反向选择，也就是显示出没有 搜寻字符串 内容的那一行</span><br><span class="line">--color=auto ：找到的关键字加颜色显示</span><br></pre></td></tr></table></figure><p>示例：把含有 the 字符串的行提取出来（注意默认会有 –color&#x3D;auto 选项，因此以下内容在 Linux 中有颜色显示 the 字符串）</p><figure class="highlight html"><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><span class="line">$ grep -n &#x27;the&#x27; regular_express.txt</span><br><span class="line">8:I can&#x27;t finish the test.</span><br><span class="line">12:the symbol &#x27;*&#x27; is represented as start.</span><br><span class="line">15:You are the best is mean you are the no. 1.</span><br><span class="line">16:The world Happy is the same with &quot;glad&quot;.</span><br><span class="line">18:google is the best tools for search keyword</span><br></pre></td></tr></table></figure><p>因为 { 和 } 在 shell 是有特殊意义的，因此必须要使用转义字符进行转义。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ grep -n &#x27;go\&#123;2,5\&#125;g&#x27; regular_express.txt</span><br></pre></td></tr></table></figure><h2 id="printf"><a href="#printf" class="headerlink" title="printf"></a>printf</h2><p>用于格式化输出。</p><p>它不属于管道命令，在给 printf 传数据时需要使用 $( ) 形式。</p><figure class="highlight html"><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><span class="line">$ printf &#x27;%10s %5i %5i %5i %8.2f \n&#x27; $(cat printf.txt)</span><br><span class="line">    DmTsai    80    60    92    77.33</span><br><span class="line">     VBird    75    55    80    70.00</span><br><span class="line">       Ken    60    90    70    73.33</span><br></pre></td></tr></table></figure><h2 id="awk"><a href="#awk" class="headerlink" title="awk"></a>awk</h2><p>是由 Alfred Aho，Peter Weinberger, 和 Brian Kernighan 创造，awk 这个名字就是这三个创始人名字的首字母。</p><p>awk 每次处理一行，处理的最小单位是字段，每个字段的命名方式为：$n，n 为字段号，从 1 开始，$0 表示一整行。</p><p>示例：取出登录用户的用户名和 IP</p><figure class="highlight html"><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><span class="line">$ last -n 5</span><br><span class="line">dmtsai pts/0 192.168.1.100 Tue Jul 14 17:32 still logged in</span><br><span class="line">dmtsai pts/0 192.168.1.100 Thu Jul 9 23:36 - 02:58 (03:22)</span><br><span class="line">dmtsai pts/0 192.168.1.100 Thu Jul 9 17:23 - 23:36 (06:12)</span><br><span class="line">dmtsai pts/0 192.168.1.100 Thu Jul 9 08:02 - 08:17 (00:14)</span><br><span class="line">dmtsai tty1 Fri May 29 11:55 - 12:11 (00:15)</span><br></pre></td></tr></table></figure><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ last -n 5 | awk &#x27;&#123;print $1 &quot;\t&quot; $3&#125;&#x27;</span><br></pre></td></tr></table></figure><p>可以根据字段的某些条件进行匹配，例如匹配字段小于某个值的那一行数据。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ awk &#x27;条件类型 1 &#123;动作 1&#125; 条件类型 2 &#123;动作 2&#125; ...&#x27; filename</span><br></pre></td></tr></table></figure><p>示例：&#x2F;etc&#x2F;passwd 文件第三个字段为 UID，对 UID 小于 10 的数据进行处理。</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></pre></td><td class="code"><pre><span class="line">$ cat /etc/passwd | awk &#x27;BEGIN &#123;FS=&quot;:&quot;&#125; $3 &lt; 10 &#123;print $1 &quot;\t &quot; $3&#125;&#x27;</span><br><span class="line">root 0</span><br><span class="line">bin 1</span><br><span class="line">daemon 2</span><br></pre></td></tr></table></figure><p>awk 变量：</p><table><thead><tr><th align="center">变量名称</th><th>代表意义</th></tr></thead><tbody><tr><td align="center">NF</td><td>每一行拥有的字段总数</td></tr><tr><td align="center">NR</td><td>目前所处理的是第几行数据</td></tr><tr><td align="center">FS</td><td>目前的分隔字符，默认是空格键</td></tr></tbody></table><p>示例：显示正在处理的行号以及每一行有多少字段</p><figure class="highlight html"><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><span class="line">$ last -n 5 | awk &#x27;&#123;print $1 &quot;\t lines: &quot; NR &quot;\t columns: &quot; NF&#125;&#x27;</span><br><span class="line">dmtsai lines: 1 columns: 10</span><br><span class="line">dmtsai lines: 2 columns: 10</span><br><span class="line">dmtsai lines: 3 columns: 10</span><br><span class="line">dmtsai lines: 4 columns: 10</span><br><span class="line">dmtsai lines: 5 columns: 9</span><br></pre></td></tr></table></figure><h1 id="十、进程管理"><a href="#十、进程管理" class="headerlink" title="十、进程管理"></a>十、进程管理</h1><h2 id="查看进程"><a href="#查看进程" class="headerlink" title="查看进程"></a>查看进程</h2><h3 id="1-ps"><a href="#1-ps" class="headerlink" title="1. ps"></a>1. ps</h3><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><span class="line"><span class="comment"># ps -l</span></span><br></pre></td></tr></table></figure><p>示例二：查看系统所有进程</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># ps aux</span></span><br></pre></td></tr></table></figure><p>示例三：查看特定的进程</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># ps aux | grep threadx</span></span><br></pre></td></tr></table></figure><h3 id="2-top"><a href="#2-top" class="headerlink" title="2. top"></a>2. top</h3><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><span class="line"><span class="comment"># top -d 2</span></span><br></pre></td></tr></table></figure><h3 id="3-pstree"><a href="#3-pstree" class="headerlink" title="3. pstree"></a>3. pstree</h3><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><span class="line"><span class="comment"># pstree -A</span></span><br></pre></td></tr></table></figure><h3 id="4-netstat"><a href="#4-netstat" class="headerlink" title="4. netstat"></a>4. netstat</h3><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><span class="line"><span class="comment"># netstat -anp | grep port</span></span><br></pre></td></tr></table></figure><h2 id="进程状态"><a href="#进程状态" class="headerlink" title="进程状态"></a>进程状态</h2><table><thead><tr><th align="center">状态</th><th>说明</th></tr></thead><tbody><tr><td align="center">R</td><td>running or runnable (on run queue)</td></tr><tr><td align="center">D</td><td>uninterruptible sleep (usually I&#x2F;O)</td></tr><tr><td align="center">S</td><td>interruptible sleep (waiting for an event to complete)</td></tr><tr><td align="center">Z</td><td>zombie (terminated but not reaped by its parent)</td></tr><tr><td align="center">T</td><td>stopped (either by a job control signal or because it is being traced)</td></tr><tr><td align="center"><br></td><td></td></tr></tbody></table><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/76a49594323247f21c9b3a69945445ee.png" width=""/> </div><br><h2 id="SIGCHLD"><a href="#SIGCHLD" class="headerlink" title="SIGCHLD"></a>SIGCHLD</h2><p>当一个子进程改变了它的状态时（停止运行，继续运行或者退出），有两件事会发生在父进程中：</p><ul><li>得到 SIGCHLD 信号；</li><li>waitpid() 或者 wait() 调用会返回。</li></ul><p>其中子进程发送的 SIGCHLD 信号包含了子进程的信息，包含了进程 ID、进程状态、进程使用 CPU 的时间等。</p><p>在子进程退出时，它的进程描述符不会立即释放，这是为了让父进程得到子进程信息，父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。</p><div align="center"> <img src="https://oem-1251690846.cos.ap-guangzhou.myqcloud.com/img/flow.png" width=""/> </div><br><h2 id="wait"><a href="#wait" class="headerlink" title="wait()"></a>wait()</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">pid_t</span> <span class="title function_">wait</span><span class="params">(<span class="type">int</span> *status)</span></span><br></pre></td></tr></table></figure><p>父进程调用 wait() 会一直阻塞，直到收到一个子进程退出的 SIGCHLD 信号，之后 wait() 函数会销毁子进程并返回。</p><p>如果成功，返回被收集的子进程的进程 ID；如果调用进程没有子进程，调用就会失败，此时返回 -1，同时 errno 被置为 ECHILD。</p><p>参数 status 用来保存被收集的子进程退出时的一些状态，如果对这个子进程是如何死掉的毫不在意，只想把这个子进程消灭掉，可以设置这个参数为 NULL。</p><h2 id="waitpid"><a href="#waitpid" class="headerlink" title="waitpid()"></a>waitpid()</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">pid_t</span> <span class="title function_">waitpid</span><span class="params">(<span class="type">pid_t</span> pid, <span class="type">int</span> *status, <span class="type">int</span> options)</span></span><br></pre></td></tr></table></figure><p>作用和 wait() 完全相同，但是多了两个可由用户控制的参数 pid 和 options。</p><p>pid 参数指示一个子进程的 ID，表示只关心这个子进程退出的 SIGCHLD 信号。如果 pid&#x3D;-1 时，那么和 wait() 作用相同，都是关心所有子进程退出的 SIGCHLD 信号。</p><p>options 参数主要有 WNOHANG 和 WUNTRACED 两个选项，WNOHANG 可以使 waitpid() 调用变成非阻塞的，也就是说它会立即返回，父进程可以继续执行其它任务。</p><h2 id="孤儿进程"><a href="#孤儿进程" class="headerlink" title="孤儿进程"></a>孤儿进程</h2><p>一个父进程退出，而它的一个或多个子进程还在运行，那么这些子进程将成为孤儿进程。</p><p>孤儿进程将被 init 进程（进程号为 1）所收养，并由 init 进程对它们完成状态收集工作。</p><p>由于孤儿进程会被 init 进程收养，所以孤儿进程不会对系统造成危害。</p><h2 id="僵尸进程"><a href="#僵尸进程" class="headerlink" title="僵尸进程"></a>僵尸进程</h2><p>一个子进程的进程描述符在子进程退出时不会释放，只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出，而父进程并没有调用 wait() 或 waitpid()，那么子进程的进程描述符仍然保存在系统中，这种进程称之为僵尸进程。</p><p>僵尸进程通过 ps 命令显示出来的状态为 Z（zombie）。</p><p>系统所能使用的进程号是有限的，如果产生大量僵尸进程，将因为没有可用的进程号而导致系统不能产生新的进程。</p><p>要消灭系统中大量的僵尸进程，只需要将其父进程杀死，此时僵尸进程就会变成孤儿进程，从而被 init 所收养，这样 init 就会释放所有的僵尸进程所占有的资源，从而结束僵尸进程。</p><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li>鸟哥. 鸟 哥 的 Linux 私 房 菜 基 础 篇 第 三 版[J]. 2009.</li><li><a href="https://www.ibm.com/developerworks/cn/linux/l-cn-rpmdpkg/index.html">Linux 平台上的软件包管理</a></li><li><a href="http://liubigbin.github.io/2016/03/11/Linux-%E4%B9%8B%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B%E3%80%81%E5%83%B5%E6%AD%BB%E8%BF%9B%E7%A8%8B%E4%B8%8E%E5%AD%A4%E5%84%BF%E8%BF%9B%E7%A8%8B/">Linux 之守护进程、僵死进程与孤儿进程</a></li><li><a href="https://stackoverflow.com/questions/185899/what-is-the-difference-between-a-symbolic-link-and-a-hard-link">What is the difference between a symbolic link and a hard link?</a></li><li><a href="https://idea.popcount.org/2012-12-11-linux-process-states/">Linux process states</a></li><li><a href="https://en.wikipedia.org/wiki/GUID_Partition_Table">GUID Partition Table</a></li><li><a href="https://blog.csdn.net/kevinhg/article/details/7001719">详解 wait 和 waitpid 函数</a></li><li><a href="https://blog.csdn.net/tianlesoftware/article/details/6009110">IDE、SATA、SCSI、SAS、FC、SSD 硬盘类型介绍</a></li><li><a href="http://www.mpchunter.com/s3000/akai-ib-301s-scsi-interface-for-s2800s3000/">Akai IB-301S SCSI Interface for S2800,S3000</a></li><li><a href="https://en.wikipedia.org/wiki/Parallel_ATA">Parallel ATA</a></li><li><a href="http://www.thessdreview.com/our-reviews/adata-xpg-sx900-256gb-sata-3-ssd-review-expanded-capacity-and-sandforce-driven-speed/4/">ADATA XPG SX900 256GB SATA 3 SSD Review – Expanded Capacity and SandForce Driven Speed</a></li><li><a href="https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1">Decoding UCS Invicta – Part 1</a></li><li><a href="https://zh.wikipedia.org/wiki/%E7%A1%AC%E7%9B%98">硬盘</a></li><li><a href="http://www.differencebetween.info/difference-between-sas-and-sata">Difference between SAS and SATA</a></li><li><a href="https://zh.wikipedia.org/wiki/BIOS">BIOS</a></li><li><a href="https://www.cs.rutgers.edu/~pxk/416/notes/13-fs-studies.html">File system design case studies</a></li><li><a href="https://classes.soe.ucsc.edu/cmps111/Fall08/proj4.shtml">Programming Project #4</a></li><li><a href="http://web.cs.ucla.edu/classes/fall14/cs111/scribe/11a/index.html">FILE SYSTEM DESIGN</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本文转载自 &lt;a href=&quot;https://github.com/CyC2018/CS-Notes&quot;&gt;CS-Note&lt;/a&gt; 项目&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&quot;一、常用操作以及概念&quot;&gt;&lt;a href=&quot;#一、常用操作以及概</summary>
      
    
    
    
    <category term="转载" scheme="https://blog.jugg.xyz/categories/%E8%BD%AC%E8%BD%BD/"/>
    
    
    <category term="Linux" scheme="https://blog.jugg.xyz/tags/Linux/"/>
    
    <category term="Shell" scheme="https://blog.jugg.xyz/tags/Shell/"/>
    
  </entry>
  
  <entry>
    <title>提问的智慧</title>
    <link href="https://blog.jugg.xyz/2018/04/03/repost/How-To-Ask-Questions-The-Smart-Way/"/>
    <id>https://blog.jugg.xyz/2018/04/03/repost/How-To-Ask-Questions-The-Smart-Way/</id>
    <published>2018-04-03T03:23:04.000Z</published>
    <updated>2026-05-02T11:24:14.333Z</updated>
    
    <content type="html"><![CDATA[<p>#提问的智慧</p><p><strong>How To Ask Questions The Smart Way</strong></p><p>Copyright © 2001,2006,2014 Eric S. Raymond, Rick Moen</p><p>本指南英文版版权为 Eric S. Raymond, Rick Moen 所有。</p><p>原文网址:<a href="http://www.catb.org/~esr/faqs/smart-questions.html">http://www.catb.org/~esr&#x2F;faqs&#x2F;smart-questions.html</a></p><p>Copyleft 2001 by D.H.Grand(nOBODY&#x2F;Ginux), 2010 by Gasolin, 2015 by Ryan Wu</p><p>本中文指南是基于原文 3.10 版以及 2010 年由 <a href="https://github.com/gasolin">Gasolin</a> 所翻译版本的最新翻译；</p><p>协助指出翻译问题，<strong>请<a href="https://github.com/ryanhanwu/smartquestions/issues/new">发Issue</a>，或直接<a href="https://github.com/ryanhanwu/smartquestions/compare/">发Pull Request</a>给我。</strong></p><p>本文另有繁体中文版: <a href="https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way">https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way</a></p><h2 id="原文版本历史"><a href="#原文版本历史" class="headerlink" title="原文版本历史"></a><a href="https://github.com/ryanhanwu/smartquestions/blob/master/history.md">原文版本历史</a></h2><h2 id="声明"><a href="#声明" class="headerlink" title="声明"></a>声明</h2><p>许多项目在他们的使用协助&#x2F;说明网页中链接了本指南，这么做很好，我们也鼓励大家都这么做。但如果你是负责管理这个项目网页的人，请在超链接附近的显着位置上注明：</p><p><strong>本指南不提供此项目的实际支持服务！</strong></p><p>我们已经深刻领教到少了上述声明所带来的痛苦。因为少了这点声明，我们不停地被一些白痴纠缠。这些白痴认为既然我们发布了这本指南，那么我们就有责任解决世上所有的技术问题。</p><p>如果你是因为需要某些协助而正在阅读这本指南，并且最后离开是因为发现从本指南作者们身上得不到直接的协助，那么你就是我们所说的那些白痴之一。别问我们问题，我们只会忽略你。我们在这本指南中是教你如何从那些真正懂得你所遇到软件或硬件问题的人取得协助，而99%的情况下那不会是我们。除非你确定本指南的作者之一刚好是你所遇到的问题领域的专家，否则请不要打扰我们，这样大家都会开心一点。</p><h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>在<a href="http://www.catb.org/~esr/faqs/hacker-howto.html">黑客</a>的世界里，当你拋出一个技术问题时，最终是否能得到有用的回答，往往取决于你所提问和追问的方式。本指南将教你如何正确的提问以获得你满意的答案。</p><p>不只是黑客，现在开放源代码（Open Source）软件已经相当盛行，你常常也可以由其他有经验的使用者身上得到好答案，这是件**<em>好事</em>**；使用者比起黑客来，往往对那些新手常遇到的问题更宽容一些。然而，将有经验的使用者视为黑客，并采用本指南所提的方法与他们沟通，同样也是能从他们身上得到满意回答的最有效方式。</p><p>首先你应该明白，黑客们喜爱有挑战性的问题，或者能激发我们思维的好问题。如果我们并非如此，那我们也不会成为你想询问的对象。如果你给了我们一个值得反复咀嚼玩味的好问题，我们自会对你感激不尽。好问题是激励，是厚礼。好问题可以提高我们的理解力，而且通常会暴露我们以前从没意识到或者思考过的问题。对黑客而言，”好问题！”是诚挚的大力称赞。</p><p>尽管如此，黑客们有着蔑视或傲慢面对简单问题的坏名声，这有时让我们看起来对新手、无知者似乎较有敌意，但其实不是那样的。</p><p>我们不讳言我们对那些不愿思考、或者在发问前不做他们该做的事的人的蔑视。那些人是时间杀手 -– 他们只想索取，从不付出，消耗我们可用在更有趣的问题或更值得回答的人身上的时间。我们称这样的人为 <strong>失败者（撸瑟）</strong> （由于历史原因，我们有时把它拼作 <strong>lusers</strong>）。</p><p>我们意识到许多人只是想使用我们写的软件，他们对学习技术细节没有兴趣。对大多数人而言，电脑只是种工具，是种达到目的的手段而已。他们有自己的生活并且有更要紧的事要做。我们了解这点，也从不指望每个人都对这些让我们着迷的技术问题感兴趣。尽管如此，我们回答问题的风格是指向那些真正对此有兴趣并愿意主动参与解决问题的人，这一点不会变，也不该变。如果连这都变了，我们就是在降低做自己最擅长的事情上的效率。</p><p>我们（在很大程度上）是自愿的，从繁忙的生活中抽出时间来解答疑惑，而且时常被提问淹没。所以我们无情的滤掉一些话题，特别是拋弃那些看起来像失败者的家伙，以便更高效的利用时间来回答**赢家（winner）**的问题。</p><p>如果你厌恶我们的态度，高高在上，或过于傲慢，不妨也设身处地想想。我们并没有要求你向我们屈服 – 事实上，我们大多数人非常乐意与你平等地交流，只要你付出小小努力来满足基本要求，我们就会欢迎你加入我们的文化。但让我们帮助那些不愿意帮助自己的人是没有效率的。无知没有关系，但装白痴就是不行。</p><p>所以，你不必在技术上很在行才能吸引我们的注意，但你必须表现出能引导你变得在行的特质 – 机敏、有想法、善于观察、乐于主动参与解决问题。如果你做不到这些使你与众不同的事情，我们建议你花点钱找家商业公司签个技术支持服务合同，而不是要求黑客个人无偿地帮助你。</p><p>如果你决定向我们求助，当然你也不希望被视为失败者，更不愿成为失败者中的一员。能立刻得到快速并有效答案的最好方法，就是像赢家那样提问 – 聪明、自信、有解决问题的思路，只是偶尔在特定的问题上需要获得一点帮助。</p><p>（欢迎对本指南提出改进意见。你可以 email 你的建议至 <a href="esr@thyrsus.com">esr@thyrsus.com</a> 或 <a href="respond-auto@linuxmafia.com">respond-auto@linuxmafia.com</a>。然而请注意，本文并非<a href="http://www.ietf.org/rfc/rfc1855.txt">网络礼节</a>的通用指南，而我们通常会拒绝无助于在技术论坛得到有用答案的建议。）</p><h2 id="在提问之前"><a href="#在提问之前" class="headerlink" title="在提问之前"></a>在提问之前</h2><p>在你准备要通过电子邮件、新闻群组或者聊天室提出技术问题前，请先做到以下事情：</p><ol><li>尝试在你准备提问的论坛的旧文章中搜索答案。</li><li>尝试上网搜索以找到答案。</li><li>尝试阅读手册以找到答案。</li><li>尝试阅读常见问题文件（FAQ）以找到答案。</li><li>尝试自己检查或试验以找到答案</li><li>向你身边的强者朋友打听以找到答案。</li><li>如果你是程序开发者，请尝试阅读源代码以找到答案</li></ol><p>当你提出问题的时候，请先表明你已经做了上述的努力；这将有助于树立你并不是一个不劳而获且浪费别人的时间的提问者。如果你能一并表达在做了上述努力的过程中所**<em>学到</em>**的东西会更好，因为我们更乐于回答那些表现出能从答案中学习的人的问题。</p><p>运用某些策略，比如先用Google搜索你所遇到的各种错误信息（既搜索<a href="http://groups.google.com/">Google论坛</a>，也搜索网页），这样很可能直接就找到了能解决问题的文件或邮件列表线索。即使没有结果，在邮件列表或新闻组寻求帮助时加上一句 <strong>我在Google中搜过下列句子但没有找到什么有用的东西</strong> 也是件好事，即使它只是表明了搜索引擎不能提供哪些帮助。这么做（加上搜索过的字串）也让遇到相似问题的其他人能被搜索引擎引导到你的提问来。</p><p>别着急，不要指望几秒钟的Google搜索就能解决一个复杂的问题。在向专家求助之前，再阅读一下常见问题文件（FAQ）、放轻松、坐舒服一些，再花点时间思考一下这个问题。相信我们，他们能从你的提问看出你做了多少阅读与思考，如果你是有备而来，将更有可能得到解答。不要将所有问题一股脑拋出，只因你的第一次搜索没有找到答案（或者找到太多答案）。</p><p>准备好你的问题，再将问题仔细的思考过一遍，因为草率的发问只能得到草率的回答，或者根本得不到任何答案。越是能表现出在寻求帮助前你为解决问题所付出的努力，你越有可能得到实质性的帮助。</p><p>小心别问错了问题。如果你的问题基于错误的假设，某个普通黑客（J. Random Hacker）多半会一边在心里想着<strong>蠢问题…</strong>， 一边用无意义的字面解释来答复你，希望着你会从问题的回答（而非你想得到的答案）中汲取教训。</p><p>绝不要自以为**<em>够格</em><strong>得到答案，你没有；你并没有。毕竟你没有为这种服务支付任何报酬。你将会是自己去</strong><em>挣到</em>**一个答案，靠提出有内涵的、有趣的、有思维激励作用的问题 –一个有潜力能贡献社区经验的问题，而不仅仅是被动的从他人处索取知识。</p><p>另一方面，表明你愿意在找答案的过程中做点什么是一个非常好的开端。<strong>谁能给点提示？</strong>、<strong>我的这个例子里缺了什么？<strong>以及</strong>我应该检查什么地方</strong>比<strong>请把我需要的确切的过程贴出来</strong>更容易得到答复。因为你表现出只要有人能指个正确方向，你就有完成它的能力和决心。</p><h2 id="当你提问时"><a href="#当你提问时" class="headerlink" title="当你提问时"></a>当你提问时</h2><h3 id="慎选提问的论坛"><a href="#慎选提问的论坛" class="headerlink" title="慎选提问的论坛"></a>慎选提问的论坛</h3><p>小心选择你要提问的场合。如果你做了下述的事情，你很可能被忽略掉或者被看作失败者：</p><ul><li>在与主题不合的论坛上贴出你的问题</li><li>在探讨进阶技术问题的论坛张贴非常初级的问题；反之亦然</li><li>在太多的不同新闻群组上重复转贴同样的问题（cross-post）</li><li>向既非熟人也没有义务解决你问题的人发送私人电邮</li></ul><p>黑客会剔除掉那些搞错场合的问题，以保护他们沟通的渠道不被无关的东西淹没。你不会想让这种事发生在自己身上的。</p><p>因此，第一步是找到对的论坛。再说一次，Google和其它搜索引擎还是你的朋友，用它们来找到与你遭遇到困难的软硬件问题最相关的网站。通常那儿都有常见问题（FAQ）、邮件列表及相关说明文件的链接。如果你的努力（包括**<em>阅读</em>**FAQ）都没有结果，网站上也许还有报告Bug（Bug-reporting）的流程或链接，如果是这样，连过去看看。</p><p>向陌生的人或论坛发送邮件最可能是风险最大的事情。举例来说，别假设一个提供丰富内容的网页的作者会想充当你的免费顾问。不要对你的问题是否会受到欢迎做太乐观的估计 – 如果你不确定，那就向别处发送，或者压根别发。</p><p>在选择论坛、新闻群组或邮件列表时，别太相信名字，先看看FAQ或者许可书以弄清楚你的问题是否切题。发文前先翻翻已有的话题，这样可以让你感受一下那里的文化。事实上，事先在新闻组或邮件列表的历史记录中搜索与你问题相关的关键词是个极好的主意，也许这样就找到答案了。即使没有，也能帮助你归纳出更好的问题。</p><p>别像机关枪似的一次”扫射”所有的帮助渠道，这就像大喊大叫一样会使人不快。要一个一个地来。</p><p>搞清楚你的主题！最典型的错误之一是在某种致力于跨平台可移植的语言、套件或工具的论坛中提关于Unix或Windows操作系统程序界面的问题。如果你不明白为什么这是大错，最好在搞清楚这之间差异之前什么也别问。</p><p>一般来说，在仔细挑选的公共论坛中提问，会比在私有论坛中提同样的问题更容易得到有用的回答。有几个理由可以支持这点，一是看潜在的回复者有多少，二是看观众有多少。黑客较愿意回答那些能帮助到许多人的问题。</p><p>可以理解的是，老练的黑客和一些热门软件的作者正在接受过多的错发信息。就像那根最后压垮骆驼背的稻草一样，你的加入也有可能使情况走向极端 – 已经好几次了，一些热门软件的作者从自己软件的支持中抽身出来，因为伴随而来涌入其私人邮箱的无用邮件变得无法忍受。</p><h3 id="Stack-Overflow"><a href="#Stack-Overflow" class="headerlink" title="Stack Overflow"></a>Stack Overflow</h3><p>搜索，<strong><em>然后</em></strong> 在 Stack Exchange 问。</p><p>近年来，Stack Exchange community 社区已经成为回答技术及其他问题的主要渠道，尤其是那些开放源码的项目。</p><p>因为 Google 索引是即时的，在看 Stack Exchange 之前先在 Google 搜索。有很高的机率某人已经问了一个类似的问题，而且 Stack Exchange 网站们往往会是搜索结果中最前面几个。如果你在 Google 上没有找到任何答案，你再到特定相关主题的网站去找。用标签（Tag）搜索能让你更缩小你的搜索结果。</p><p>Stack Exchange 已经成长到<a href="http://stackexchange.com/sites">超过一百个网站</a>，以下是最常用的几个站：</p><ul><li>Super User 是问一些通用的电脑问题，如果你的问题跟代码或是写程序无关，只是一些网络连线之类的，请到这里。</li><li>Stack Overflow 是问写程序有关的问题。</li><li>Server Fault 是问服务器和网管相关的问题。</li></ul><h3 id="网站和IRC论坛"><a href="#网站和IRC论坛" class="headerlink" title="网站和IRC论坛"></a>网站和IRC论坛</h3><p>本地的使用者群组（user group），或者你所用的 Linux 发行版本也许正在宣传他们的网页论坛或 IRC 频道，并提供新手帮助（在一些非英语国家，新手论坛很可能还是邮件列表）， 这些地方是开始提问的好首选，特别是当你觉得遇到的也许只是相对简单或者很普通的问题时。经过宣传的 IRC 频道是公开欢迎提问的地方，通常可以即时得到回应。</p><p>事实上，如果程序出的问题只发生在特定 Linux 发行版提供的版本（这很常见），最好先去该发行版的论坛或邮件列表中提问，再到程序本身的论坛或邮件列表提问。（否则）该项目的黑客可能仅仅回复 “用**<em>我们的</em>**版本”。</p><p>在任何论坛发文以前，先确认一下有没有搜索功能。如果有，就试着搜索一下问题的几个关键词，也许这会有帮助。如果在此之前你已做过通用的网页搜索（你也该这样做），还是再搜索一下论坛，搜索引擎有可能没来得及索引此论坛的全部内容。</p><p>通过论坛或 IRC 频道来提供使用者支持服务有增长的趋势，电子邮件则大多为项目开发者间的交流而保留。所以最好先在论坛或 IRC 中寻求与该项目相关的协助。</p><h3 id="第二步，使用项目邮件列表"><a href="#第二步，使用项目邮件列表" class="headerlink" title="第二步，使用项目邮件列表"></a>第二步，使用项目邮件列表</h3><p>当某个项目提供开发者邮件列表时，要向列表而不是其中的个别成员提问，即使你确信他能最好地回答你的问题。查一查项目的文件和首页，找到项目的邮件列表并使用它。有几个很好的理由支持我们采用这种办法：</p><ul><li>任何好到需要向个别开发者提出的问题，也将对整个项目群组有益。反之，如果你认为自己的问题对整个项目群组来说太愚蠢，也不能成为骚扰个别开发者的理由。</li><li>向列表提问可以分散开发者的负担，个别开发者（尤其是项目领导人）也许太忙以至于没法回答你的问题。</li><li>大多数邮件列表都会被存档，那些被存档的内容将被搜索引擎索引。如果你向列表提问并得到解答，将来其它人可以通过网页搜索找到你的问题和答案，也就不用再次发问了。</li><li>如果某些问题经常被问到，开发者可以利用此信息来改进说明文件或软件本身，以使其更清楚。如果只是私下提问，就没有人能看到最常见问题的完整场景。</li></ul><p>如果一个项目既有”使用者” 也有”开发者”（或”黑客”）邮件列表或论坛，而你又不会动到那些源代码，那么就向”使用者”列表或论坛提问。不要假设自己会在开发者列表中受到欢迎，那些人多半会将你的提问视为干扰他们开发的噪音。</p><p>然而，如果你**<em>确信</em>**你的问题很特别，而且在”使用者” 列表或论坛中几天都没有回复，可以试试前往”开发者”列表或论坛发问。建议你在张贴前最好先暗地里观察几天以了解那里的行事方式（事实上这是参与任何私有或半私有列表的好主意）</p><p>如果你找不到一个项目的邮件列表，而只能查到项目维护者的电子邮件地址，尽管向他发信。即使是在这种情况下，也别假设（项目）邮件列表不存在。在你的电子邮件中，请陈述你已经试过但没有找到合适的邮件列表，也提及你不反对将自己的邮件转发给他人（许多人认为，即使没什么秘密，私人电子邮件也不应该被公开。通过允许将你的电子邮件转发他人，你给了相应人员处置你邮件的选择）。</p><h3 id="使用有意义且描述明确的标题"><a href="#使用有意义且描述明确的标题" class="headerlink" title="使用有意义且描述明确的标题"></a>使用有意义且描述明确的标题</h3><p>在邮件列表、新闻群组或论坛中，大约50字以内的标题是抓住资深专家注意力的好机会。别用喋喋不休的<strong>帮帮忙</strong>、<strong>跪求</strong>、<strong>急</strong>（更别说**救命啊！！！！**这样让人反感的话，用这种标题会被条件反射式地忽略）来浪费这个机会。不要妄想用你的痛苦程度来打动我们，而是在这点空间中使用极简单扼要的描述方式来提出问题。</p><p>一个好标题范例是<strong>目标 – 差异</strong>式的描述，许多技术支持组织就是这样做的。在<strong>目标</strong>部分指出是哪一个或哪一组东西有问题，在<strong>差异</strong>部分则描述与期望的行为不一致的地方。</p><blockquote><p>蠢问题：救命啊！我的笔电不能正常显示了！</p></blockquote><blockquote><p>聪明问题：X.org 6.8.1的鼠标游标会变形，某牌显卡 MV1005 芯片组。</p></blockquote><blockquote><p>更聪明问题：X.org 6.8.1的鼠标游标，在某牌显卡 MV1005 芯片组环境下 - 会变形。</p></blockquote><p>编写<strong>目标 – 差异</strong> 式描述的过程有助于你组织对问题的细緻思考。是什么被影响了？ 仅仅是鼠标游标或者还有其它图形？只在 X.org 的 X 版中出现？或只是出现在6.8.1版中？ 是针对某牌显卡芯片组？或者只是其中的 MV1005 型号？ 一个黑客只需瞄一眼就能够立即明白你的环境**<em>和</em>**你遇到的问题。</p><p>总而言之，请想像一下你正在一个只显示标题的存档讨论串（Thread）索引中查寻。让你的标题更好地反映问题，可使下一个搜索类似问题的人能够关注这个讨论串，而不用再次提问相同的问题。</p><p>如果你想在回复中提出问题，记得要修改内容标题，以表明你是在问一个问题， 一个看起来像 <strong>Re: 测试</strong> 或者 <strong>Re: 新bug</strong> 的标题很难引起足够重视。另外，在不影响连贯性之下，适当引用并删减前文的内容，能给新来的读者留下线索。</p><p>对于讨论串，不要直接点击回复来开始一个全新的讨论串，这将限制你的观众。因为有些邮件阅读程序，比如 mutt ，允许使用者按讨论串排序并通过折叠讨论串来隐藏消息，这样做的人永远看不到你发的消息。</p><p>仅仅改变标题还不够。mutt 和其它一些邮件阅读程序还会检查邮件标题以外的其它信息，以便为其指定讨论串。所以宁可发一个全新的邮件。</p><p>在网页论坛上，好的提问方式稍有不同，因为讨论串与特定的信息紧密结合，并且通常在讨论串外就看不到里面的内容，故通过回复提问，而非改变标题是可接受的。不是所有论坛都允许在回复中出现分离的标题，而且这样做了基本上没有人会去看。不过，通过回复提问，这本身就是暧昧的做法，因为它们只会被正在查看该标题的人读到。所以，除非你**<em>只想</em>**在该讨论串当前活跃的人群中提问，不然还是另起炉灶比较好。</p><h3 id="使问题容易回复"><a href="#使问题容易回复" class="headerlink" title="使问题容易回复"></a>使问题容易回复</h3><p>以**请将你的回复寄到……**来结束你的问题多半会使你得不到回答。如果你觉得花几秒钟在邮件客户端设置一下回复地址都麻烦，我们也觉得花几秒钟思考你的问题更麻烦。如果你的邮件程序不支持这样做，<a href="http://linuxmafia.com/faq/Mail/muas.html">换个好点的</a>；如果是操作系统不支持这种邮件程序，也换个好点的。</p><p>在论坛，要求通过电子邮件回复是非常无礼的，除非你相信回复的信息可能比较敏感（而且有人会为了某些未知的原因，只让你而不是整个论坛知道答案）。如果你只是想在有人回复讨论串时得到电子邮件提醒，可以要求网页论坛发送给你。几乎所有论坛都支持诸如<strong>追踪此讨论串</strong>、<strong>有回复时发送邮件提醒</strong>等功能。</p><h3 id="用清晰、正确、精准并语法正确的语句"><a href="#用清晰、正确、精准并语法正确的语句" class="headerlink" title="用清晰、正确、精准并语法正确的语句"></a>用清晰、正确、精准并语法正确的语句</h3><p>我们从经验中发现，粗心的提问者通常也会粗心的写程序与思考（我敢打包票）。回答粗心大意者的问题很不值得，我们宁愿把时间耗在别处。</p><p>正确的拼字、标点符号和大小写是很重要的。一般来说，如果你觉得这样做很麻烦，不想在乎这些，那我们也觉得麻烦，不想在乎你的提问。花点额外的精力斟酌一下字句，用不着太僵硬与正式 – 事实上，黑客文化很看重能准确地使用非正式、俚语和幽默的语句。但它**<em>必须很</em>**准确，而且有迹象表明你是在思考和关注问题。</p><p>正确地拼写、使用标点和大小写，不要将<strong>its</strong>混淆为<strong>it’s</strong>，<strong>loose</strong>搞成<strong>lose</strong>或者将<strong>discrete</strong>弄成<strong>discreet</strong>。不要<strong>全部用大写</strong>，这会被视为无礼的大声嚷嚷（全部小写也好不到哪去，因为不易阅读。<a href="http://en.wikipedia.org/wiki/Alan_Cox">Alan Cox</a>也许可以这样做，但你不行。）</p><p>更白话的说，如果你写得像是个半文盲[译注：<a href="http://zh.wikipedia.org/zh-tw/%E5%B0%8F%E7%99%BD">小白</a>]），那多半得不到理睬。也不要使用即时通讯中的简写或<a href="http://zh.wikipedia.org/zh-tw/%E7%81%AB%E6%98%9F%E6%96%87">火星文</a>，如将<strong>的</strong>简化为<strong>ㄉ</strong>会使你看起来像一个为了少打几个键而省字的小白。更糟的是，如果像个小孩似地鬼画符那绝对是在找死，可以肯定没人会理你（或者最多是给你一大堆指责与挖苦）。</p><p>如果在使用非母语的论坛提问，你可以犯点拼写和语法上的小错，但决不能在思考上马虎（没错，我们通常能弄清两者的分别）。同时，除非你知道回复者使用的语言，否则请使用英语书写。繁忙的黑客一般会直接删除用他们看不懂语言写的消息。在网络上英语是通用语言，用英语书写可以将你的问题在尚未被阅读就被直接删除的可能性降到最低。</p><p>如果英文是你的外语（Second language），提示潜在回复者你有潜在的语言困难是很好的：<br>[译注：以下附上原文以供使用]</p><blockquote><p>English is not my native language; please excuse typing errors.</p></blockquote><ul><li>英文不是我的母语，请原谅我的错字或语法</li></ul><blockquote><p>If you speak $LANGUAGE, please email&#x2F;PM me;<br>I may need assistance translating my question.</p></blockquote><ul><li>如果你说<strong>某语言</strong>，请寄信&#x2F;私讯给我；我需要有人协助我翻译我的问题</li></ul><blockquote><p>I am familiar with the technical terms,<br>but some slang expressions and idioms are difficult for me.</p></blockquote><ul><li>我对技术名词很熟悉，但对于俗语或是特别用法比较不甚了解。</li></ul><blockquote><p>I’ve posted my question in $LANGUAGE and English.<br>I’ll be glad to translate responses, if you only use one or the other.</p></blockquote><ul><li>我把我的问题用<strong>某语言</strong>和英文写出来，如果你只用一种语言回答，我会乐意将其翻译成另一种。</li></ul><h3 id="使用易于读取且标准的文件格式发送问题"><a href="#使用易于读取且标准的文件格式发送问题" class="headerlink" title="使用易于读取且标准的文件格式发送问题"></a>使用易于读取且标准的文件格式发送问题</h3><p>如果你人为地将问题搞得难以阅读，它多半会被忽略，人们更愿读易懂的问题，所以：</p><ul><li>使用纯文字而不是HTML (<a href="http://archive.birdhouse.org/etc/evilmail.html">关闭HTML</a>并不难）</li><li>使用MIME附件通常是可以的，前提是真正有内容（譬如附带的源代码或patch），而不仅仅是邮件程序生成的模板（譬如只是信件内容的拷贝）。</li><li>不要发送一段文字只是单行句子但多次断行的邮件（这使得回复部分内容非常困难）。设想你的读者是在80个字符宽的终端机上阅读邮件，最好设置你的断行点小于80字。</li><li>但是，也**<em>不要</em>**用任何固定断行资料（譬如日志档案拷贝或会话记录）。档案应该原样包含，让回复者有信心他们看到的是和你看到的一样的东西。</li><li>在英语论坛中，不要使用<strong>Quoted-Printable</strong> MIME编码发送消息。这种编码对于张贴非ASCII语言可能是必须的，但很多邮件程序并不支持这种编码。当它们分断时，那些文本中四处散布的**&#x3D;20**符号既难看也分散注意力，甚至有可能破坏内容的语意。</li><li>绝对，**<em>永远</em>**不要指望黑客们阅读使用封闭格式编写的文档，像是微软公司的Word或Excel文件等。大多数黑客对此的反应就像有人将还在冒热气的猪粪倒在你门口阶梯上时你的反应一样。即便他们能够处理，他们也很厌恶这么做。</li><li>如果你从使用Windows的电脑发送电子邮件，关闭微软愚蠢的<strong>智能引号</strong>功能 （从[选项] &gt; [校订] &gt; [自动校正选项], 按掉<strong>智能引号</strong>单选框），以免在你的邮件中到处散布垃圾字符。</li><li>在论坛，勿滥用<strong>表情符号</strong>和<strong>HTML</strong>功能（当它们提供时）。一两个表情符号通常没有问题，但花哨的彩色文本倾向于使人认为你是个无能之辈。过滥地使用表情符号、色彩和字体会使你看来像个傻笑的小姑娘。这通常不是个好主意，除非你只是对sex而不是有用的回复更有兴趣。</li></ul><p>如果你使用图形用户界面的邮件程序（如微软公司的Outlook或者其它类似的），注意它们的默认设置不一定满足这些要求。大多数这类程序有基于选单的<strong>查看源代码</strong>命令，用它来检查发送文件夹中的消息，以确保发送的是没有多余杂质的纯文本文件。</p><h3 id="精确的描述问题并言之有物"><a href="#精确的描述问题并言之有物" class="headerlink" title="精确的描述问题并言之有物"></a>精确的描述问题并言之有物</h3><ul><li>仔细、清楚地描述你的问题或Bug的症状。</li><li>描述问题发生的环境（机器配置、操作系统、应用程序、以及相关的信息），提供经销商的发行版和版本号（如：<strong>Fedora Core 4</strong>、<strong>Slackware 9.1</strong>等）。</li><li>描述在提问前你是怎样去研究和理解这个问题的。</li><li>描述在提问前为确定问题而采取的诊断步骤。</li><li>描述最近做过什么可能相关的硬件或软件变更。</li><li>尽可能的提供一个可以<strong>重现这个问题的既定环境</strong>的方法</li></ul><p>尽量去揣测一个黑客会怎样反问你，在他提问的时候预先给他答案。</p><p>以上几点中，当你报告的是你认为可能在代码中的问题时，给黑客一个可以重现你的问题的环境尤其重要。当你这么做时，你得到有效的回答的机会和速度都会大大的提升。</p><p><a href="http://www.chiark.greenend.org.uk/~sgtatham/">Simon Tatham</a>写过一篇名为《<a href="http://www.chiark.greenend.org.uk/~sgtatham/bugs-tw.html">如何有效的报告Bug</a>》的出色文章。强力推荐你也读一读。</p><h3 id="话不在多而在精"><a href="#话不在多而在精" class="headerlink" title="话不在多而在精"></a>话不在多而在精</h3><p>你需要提供精确有内容的信息。这并不是要求你简单的把成堆的出错代码或者资料完全转录到你的提问中。如果你有庞大而复杂的测试样例能重现程序挂掉的情境，尽量将它剪裁得越小越好。</p><p>这样做的用处至少有三点。<br>第一，表现出你为简化问题付出了努力，这可以使你得到回答的机会增加；<br>第二，简化问题使你更有可能得到**<em>有用</em>**的答案；<br>第三，在精炼你的bug报告的过程中，你很可能就自己找到了解决方法或权宜之计。</p><h3 id="别动辄声称找到Bug"><a href="#别动辄声称找到Bug" class="headerlink" title="别动辄声称找到Bug"></a>别动辄声称找到Bug</h3><p>当你在使用软件中遇到问题，除非你非常、<strong><em>非常</em><strong>的有根据，不要动辄声称找到了Bug。提示：除非你能提供解决问题的源代码补丁，或者对前一版本的回归测试表现出不正确的行为，否则你都多半不够完全确信。这同样适用在网页和文件，如果你（声称）发现了文件的</strong>Bug</strong>，你应该能提供相应位置的修正或替代文件。</p><p>请记得，还有许多其它使用者没遇到你发现的问题，否则你在阅读文件或搜索网页时就应该发现了（你在抱怨前<a href="#%E5%9C%A8%E6%8F%90%E9%97%AE%E4%B9%8B%E5%89%8D">已经做了这些，是吧</a>？）。这也意味着很有可能是你弄错了而不是软件本身有问题。</p><p>编写软件的人总是非常辛苦地使它尽可能完美。如果你声称找到了Bug，也就是在质疑他们的能力，即使你是对的，也有可能会冒犯到其中某部分人。这尤其严重当你在标题中嚷嚷着有<strong>Bug</strong>。</p><p>提问时，即使你私下非常确信已经发现一个真正的Bug，最好写得像是**<em>你</em>**做错了什么。如果真的有Bug，你会在回复中看到这点。这样做的话，如果真有Bug，维护者就会向你道歉，这总比你惹恼别人然后欠别人一个道歉要好一点。</p><h3 id="可以低声下气，但还是要先做功课"><a href="#可以低声下气，但还是要先做功课" class="headerlink" title="可以低声下气，但还是要先做功课"></a>可以低声下气，但还是要先做功课</h3><p>有些人明白他们不该粗鲁或傲慢的提问并要求得到答复，但他们选择另一个极端 – 低声下气：<strong>我知道我只是个可悲的新手，一个撸瑟，但…</strong>。这既使人困扰，也没有用，尤其是伴随着与实际问题含糊不清的描述时更令人反感。</p><p>别用原始灵长类动物的把戏来浪费你我的时间。取而代之的是，尽可能清楚地描述背景条件和你的问题情况。这比低声下气更好地定位了你的位置。</p><p>有时网页论坛会设有专为新手提问的版面，如果你真的认为遇到了初学者的问题，到那去就是了，但一样别那么低声下气。</p><h3 id="描述问题症状而非猜测"><a href="#描述问题症状而非猜测" class="headerlink" title="描述问题症状而非猜测"></a>描述问题症状而非猜测</h3><p>告诉黑客们你认为问题是怎样造成的并没什么帮助。（如果你的推断如此有效，还用向别人求助吗？），因此要确信你原原本本告诉了他们问题的症状，而不是你的解释和理论；让黑客们来推测和诊断。如果你认为陈述自己的猜测很重要，清楚地说明这只是你的猜测，并描述为什么它们不起作用。</p><p><em><strong>蠢问题</strong></em></p><blockquote><p>我在编译内核时接连遇到 SIG11 错误，<br>我怀疑某条飞线搭在主板的走线上了，这种情况应该怎样检查最好？</p></blockquote><p><em><strong>聪明问题</strong></em></p><blockquote><p>我的组装电脑是 FIC-PA2007 主机板搭载 AMD K6&#x2F;233 CPU（威盛 Apollo VP2芯片组），<br>256MB Corsair PC133 SDRAM内存，在编译内核时，从开机20分钟以后就频频产生 SIG11 错误，<br>但是在头20分钟内从没发生过相同的问题。重新启动也没有用，但是关机一晚上就又能工作20分钟。<br>所有内存都换过了，没有效果。相关部分的标准编译记录如下…。</p></blockquote><p>由于以上这点似乎让许多人觉得难以配合，这里有句话可以提醒你：<strong>所有的诊断专家都来自密苏里州。</strong> 美国国务院的官方座右铭则是：<strong>让我看看</strong>（出自国会议员 Willard D. Vandiver 在1899年时的讲话：<strong>我来自一个出产玉米，棉花，牛蒡和民主党人的国家，滔滔雄辩既不能说服我，也不会让我满意。我来自密苏里州，你必须让我看看。</strong>） 针对诊断者而言，这并不是一种怀疑，而只是一种真实而有用的需求，以便让他们看到的是与你看到的原始证据尽可能一致的东西，而不是你的猜测与归纳的结论。所以，大方的展示给我们看吧！</p><h3 id="按发生时间先后列出问题症状"><a href="#按发生时间先后列出问题症状" class="headerlink" title="按发生时间先后列出问题症状"></a>按发生时间先后列出问题症状</h3><p>问题发生前的一系列操作，往往就是对找出问题最有帮助的线索。因此，你的说明里应该包含你的操作步骤，以及机器和软件的反应，直到问题发生。在命令行处理的情况下，提供一段操作记录（例如运行脚本工具所生成的），并引用相关的若干行（如20行）记录会非常有帮助。</p><p>如果挂掉的程序有诊断选项（如 -v 的详述开关），试着选择这些能在记录中增加调试信息的选项。记住，<strong>多</strong>不等于<strong>好</strong>。试着选取适当的调试级别以便提供有用的信息而不是让读者淹没在垃圾中。</p><p>如果你的说明很长（如超过四个段落），在开头简述问题，接下来再按时间顺序详述会有所帮助。这样黑客们在读你的记录时就知道该注意哪些内容了。</p><h3 id="描述目标而不是过程"><a href="#描述目标而不是过程" class="headerlink" title="描述目标而不是过程"></a>描述目标而不是过程</h3><p>如果你想弄清楚如何做某事（而不是报告一个Bug），在开头就描述你的目标，然后才陈述重现你所卡住的特定步骤。</p><p>经常寻求技术帮助的人在心中有个更高层次的目标，而他们在自以为能达到目标的特定道路上被卡住了，然后跑来问该怎么走，但没有意识到这条路本身就有问题。结果要费很大的劲才能搞定。</p><p><strong>蠢问题</strong></p><blockquote><p>我怎样才能从某绘图程序的颜色选择器中取得十六进制的的RGB值？</p></blockquote><p><strong>聪明问题</strong></p><blockquote><p>我正试着用替换一幅图片的色码成自己选定的色码，我现在知道的唯一方法是编辑每个色码区块，<br>但却无法从某绘图程序的颜色选择器取得十六进制的的RGB值。</p></blockquote><p>第二种提问法比较聪明，你可能得到像是<strong>建议采用另一个更合适的工具</strong>的回复。</p><h3 id="别要求使用私人电邮回复"><a href="#别要求使用私人电邮回复" class="headerlink" title="别要求使用私人电邮回复"></a>别要求使用私人电邮回复</h3><p>黑客们认为问题的解决过程应该公开、透明，此过程中如果更有经验的人注意到不完整或者不当之处，最初的回复才能够、也应该被纠正。同时，作为提供帮助者也能因为能力和学识被其它同行看到而得到某种奖励。</p><p>当你要求私下回复时，这个过程和奖励都被中止。别这样做，让**<em>回复者</em>**来决定是否私下回答 – 如果他真这么做了，通常是因为他认为问题编写太差或者太肤浅，以至于对其它人没有兴趣。</p><p>这条规则存在一条有限的例外，如果你确信提问可能会引来大量雷同的回复时，那么这个神奇的提问句会是<strong>向我发电邮，我将为论坛归纳这些回复</strong>。试着将邮件列表或新闻群组从洪水般的雷同回复中解救出来是非常有礼貌的 – 但你必须信守诺言。</p><h3 id="清楚明确的表达你的问题以及需求"><a href="#清楚明确的表达你的问题以及需求" class="headerlink" title="清楚明确的表达你的问题以及需求"></a>清楚明确的表达你的问题以及需求</h3><p>漫无边际的提问近乎无休无止的时间黑洞。最有可能给你有用答案的人通常也正是最忙的人（他们忙是因为要亲自完成大部分工作）。这样的人对无节制的时间黑洞相当厌恶，所以他们也倾向于厌恶那些漫无边际的提问。</p><p>如果你明确表述需要回答者做什么（如提供指点、发送一段代码、检查你的补丁、或是其他等等），就最有可能得到有用的答案。因为这会定出一个时间和精力的上限，便于回答者能集中精力来帮你。这么做很棒。</p><p>要理解专家们所处的世界，请把专业技能想像为充裕的资源，而回复的时间则是稀缺的资源。你要求他们奉献的时间越少，你越有可能从真正专业而且很忙的专家那里得到解答。</p><p>所以，界定一下你的问题，使专家花在辨识你的问题和回答所需要付出的时间减到最少，这技巧对你有用答案相当有帮助 – 但这技巧通常和简化问题有所区别。因此，问**我想更好的理解X，可否指点一下哪有好一点说明？<strong>通常比问</strong>你能解释一下X吗？**更好。如果你的代码不能运作，通常请别人看看哪里有问题，比要求别人替你改正要明智得多。</p><h3 id="询问有关代码的问题时"><a href="#询问有关代码的问题时" class="headerlink" title="询问有关代码的问题时"></a>询问有关代码的问题时</h3><p>别要求他人帮你有问题的代码调试而不提示一下应该从何入手。张贴几百行的代码，然后说一声：<strong>它不会动</strong>会让你完全被忽略。只贴几十行代码，然后说一句：**在第七行以后，我期待它显示 <x>，但实际出现的是 <y>**比较有可能让你得到回应。</p><p>最有效描述程序问题的方法是提供最精简的Bug展示测试示例（bug-demonstrating test case）。什么是最精简的测试示例? 那是问题的缩影；一小个程序片段能<strong>刚好</strong>展示出程序的异常行为，而不包含其他令人分散注意力的内容。怎么制作最精简的测试示例？如果你知道哪一行或哪一段代码会造成异常的行为，复制下来并加入足够重现这个状况的代码（例如，足以让这段代码能被编译&#x2F;直译&#x2F;被应用程序处理）。如果你无法将问题缩减到一个特定区块，就复制一份代码并移除不影响产生问题行为的部分。总之，测试示例越小越好（查看<a href="#%E8%AF%9D%E4%B8%8D%E5%9C%A8%E5%A4%9A%E8%80%8C%E5%9C%A8%E7%B2%BE">话不在多而在精</a>一节）。</p><p>一般而言，要得到一段相当精简的测试示例并不太容易，但永远先尝试这样做的是种好习惯。这种方式可以帮助你了解如何自行解决这个问题 —- 而且即使你的尝试不成功，黑客们也会看到你在尝试取得答案的过程中付出了努力，这可以让他们更愿意与你合作。</p><p>如果你只是想让别人帮忙审查（Review）一下代码，在信的开头就要说出来，并且一定要提到你认为哪一部分特别需要关注以及为什么。</p><h3 id="别把自己家庭作业的问题贴上来"><a href="#别把自己家庭作业的问题贴上来" class="headerlink" title="别把自己家庭作业的问题贴上来"></a>别把自己家庭作业的问题贴上来</h3><p>黑客们很擅长分辨哪些问题是家庭作业式的问题；因为我们中的大多数都曾自己解决这类问题。同样，这些问题得由**<em>你</em>**来搞定，你会从中学到东西。你可以要求给点提示，但别要求得到完整的解决方案。</p><p>如果你怀疑自己碰到了一个家庭作业式的问题，但仍然无法解决，试试在使用者群组，论坛或（最后一招）在项目的<strong>使用者</strong>邮件列表或论坛中提问。尽管黑客们**<em>会</em>**看出来，但一些有经验的使用者也许仍会给你一些提示。</p><h3 id="去掉无意义的提问句"><a href="#去掉无意义的提问句" class="headerlink" title="去掉无意义的提问句"></a>去掉无意义的提问句</h3><p>避免用无意义的话结束提问，例如<strong>有人能帮我吗？<strong>或者</strong>这有答案吗？</strong>。</p><p>首先：如果你对问题的描述不是很好，这样问更是画蛇添足。</p><p>其次：由于这样问是画蛇添足，黑客们会很厌烦你 – 而且通常会用逻辑上正确，但毫无意义的回答来表示他们的蔑视， 例如：<strong>没错，有人能帮你</strong>或者<strong>不，没答案</strong>。</p><p>一般来说，避免用 <strong>是或否</strong>、<strong>对或错</strong>、<strong>有或没有</strong>类型的问句，除非你想得到<a href="http://homepage.ntlworld.com./jonathan.deboynepollard/FGA/questions-with-yes-or-no-answers.html">是或否类型的回答</a>。</p><h3 id="即使你很急也不要在标题写紧急"><a href="#即使你很急也不要在标题写紧急" class="headerlink" title="即使你很急也不要在标题写紧急"></a>即使你很急也不要在标题写<strong>紧急</strong></h3><p>这是你的问题，不是我们的。宣称<strong>紧急</strong>极有可能事与愿违：大多数黑客会直接删除无礼和自私地企图即时引起关注的问题。更严重的是，<strong>紧急</strong>这个字（或是其他企图引起关注的标题）通常会被垃圾信过滤器过滤掉 – 你希望能看到你问题的人可能永远也看不到。</p><p>有半个例外的情况是，如果你是在一些很高调，会使黑客们兴奋的地方，也许值得这样去做。在这种情况下，如果你有时间压力，也很有礼貌地提到这点，人们也许会有兴趣回答快一点。</p><p>当然，这风险很大，因为黑客们兴奋的点多半与你的不同。譬如从 NASA 国际空间站（International Space Station）发这样的标题没有问题，但用自我感觉良好的慈善行为或政治原因发肯定不行。事实上，张贴诸如**紧急：帮我救救这个毛绒绒的小海豹！**肯定让你被黑客忽略或惹恼他们，即使他们认为毛绒绒的小海豹很重要。</p><p>如果你觉得这点很不可思议，最好再把这份指南剩下的内容多读几遍，直到你弄懂了再发文。</p><h3 id="礼多人不怪，而且有时还很有帮助"><a href="#礼多人不怪，而且有时还很有帮助" class="headerlink" title="礼多人不怪，而且有时还很有帮助"></a>礼多人不怪，而且有时还很有帮助</h3><p>彬彬有礼，多用<strong>请</strong>和<strong>谢谢您的关注</strong>，或<strong>谢谢你的关照</strong>。让大家都知道你对他们花时间免费提供帮助心存感激。</p><p>坦白说，这一点并没有比清晰、正确、精准并合法语法和避免使用专用格式重要（也不能取而代之）。黑客们一般宁可读有点唐突但技术上鲜明的Bug报告，而不是那种有礼但含糊的报告。（如果这点让你不解，记住我们是按问题能教我们什么来评价问题的价值的）</p><p>然而，如果你有一串的问题待解决，客气一点肯定会增加你得到有用回应的机会。</p><p>（我们注意到，自从本指南发布后，从资深黑客那里得到的唯一严重缺陷反馈，就是对预先道谢这一条。一些黑客觉得<strong>先谢了</strong>意味着事后就不用再感谢任何人的暗示。我们的建议是要么先说<strong>先谢了</strong>，<strong><em>然后</em><strong>事后再对回复者表示感谢，或者换种方式表达感激，譬如用</strong>谢谢你的关注</strong>或<strong>谢谢你的关照</strong>。）</p><h3 id="问题解决后，加个简短的补充说明"><a href="#问题解决后，加个简短的补充说明" class="headerlink" title="问题解决后，加个简短的补充说明"></a>问题解决后，加个简短的补充说明</h3><p>问题解决后，向所有帮助过你的人发个说明，让他们知道问题是怎样解决的，并再一次向他们表示感谢。如果问题在新闻组或者邮件列表中引起了广泛关注，应该在那里贴一个说明比较恰当。</p><p>最理想的方式是向最初提问的话题回复此消息，并在标题中包含<strong>已修正</strong>，<strong>已解决</strong>或其它同等含义的明显标记。在人来人往的邮件列表里，一个看见讨论串<strong>问题 X</strong>和<strong>问题的X - 已解决</strong>的潜在回复者就明白不用再浪费时间了（除非他个人觉得<strong>问题 X</strong>的有趣），因此可以利用此时间去解决其它问题。</p><p>补充说明不必很长或是很深入；简单的一句<strong>你好，原来是网线出了问题！谢谢大家 – Bill</strong>比什么也不说要来的好。事实上，除非结论真的很有技术含量，否则简短可爱的小结比长篇大论更好。说明问题是怎样解决的，但大可不必将解决问题的过程复述一遍。</p><p>对于有深度的问题，张贴调试记录的摘要是有帮助的。描述问题的最终状态，说明是什么解决了问题，在此**<em>之后</em>**才指明可以避免的盲点。避免盲点的部分应放在正确的解决方案和其它总结材料之后，而不要将此信息搞成侦探推理小说。列出那些帮助过你的名字，会让你交到更多朋友。</p><p>除了有礼貌和有内涵以外，这种类型的补充也有助于他人在邮件列表&#x2F;新闻群组&#x2F;论坛中搜索到真正解决你问题的方案，让他们也从中受益。</p><p>至少，这种补充有助于让每位参与协助的人因问题的解决而从中得到满足感。如果你自己不是技术专家或者黑客，那就相信我们，这种感觉对于那些你向他们求助的大师或者专家而言，是非常重要的。问题悬而未决会让人灰心；黑客们渴望看到问题被解决。好人有好报，满足他们的渴望，你会在下次提问时尝到甜头。</p><p>思考一下怎样才能避免他人将来也遇到类似的问题，自问写一份文件或加个常见问题（FAQ）会不会有帮助。如果是的话就将它们发给维护者。</p><p>在黑客中，这种良好的后继行动实际上比传统的礼节更为重要，也是你如何透过善待他人而赢得声誉的方式，这是非常有价值的资产。</p><h2 id="如何解读答案"><a href="#如何解读答案" class="headerlink" title="如何解读答案"></a>如何解读答案</h2><p><a id="RTFM"></a></p><h3 id="RTFM和STFW：如何知道你已完全搞砸了"><a href="#RTFM和STFW：如何知道你已完全搞砸了" class="headerlink" title="RTFM和STFW：如何知道你已完全搞砸了"></a>RTFM和STFW：如何知道你已完全搞砸了</h3><p>有一个古老而神圣的传统：如果你收到<strong>RTFM （Read The Fucking Manual）<strong>的回应，回答者认为你</strong>应该去读他妈的手册</strong>。当然，基本上他是对的，你应该去读一读。</p><p>RTFM 有一个年轻的亲戚。如果你收到<strong>STFW（Search The Fucking Web）<strong>的回应，回答者认为你</strong>应该到他妈的网上搜索</strong>过了。那人多半也是对的，去搜索一下吧。（更温和一点的说法是 <strong><a href="http://lmgtfy.com/">Google是你的朋友</a></strong>！）</p><p>在论坛，你也可能被要求去爬爬论坛的旧文。事实上，有人甚至可能热心地为你提供以前解决此问题的讨论串。但不要依赖这种关照，提问前应该先搜索一下旧文。</p><p>通常，用这两句之一回答你的人会给你一份包含你需要内容的手册或者一个网址，而且他们打这些字的时候也正在读着。这些答复意味着回答者认为</p><ul><li><strong>你需要的信息非常容易获得</strong>；</li><li><strong>你自己去搜索这些信息比灌给你能让你学到更多</strong>。</li></ul><p>你不应该因此不爽；<strong>依照黑客的标准，他已经表示了对你一定程度的关注，而没有对你的要求视而不见</strong>。你应该对他祖母般的慈祥表示感谢。</p><h3 id="如果还是搞不懂"><a href="#如果还是搞不懂" class="headerlink" title="如果还是搞不懂"></a>如果还是搞不懂</h3><p>如果你看不懂回应，别立刻要求对方解释。像你以前试着自己解决问题时那样（利用手册，FAQ，网络，身边的高手），先试着去搞懂他的回应。如果你真的需要对方解释，记得表现出你已经从中学到了点什么。</p><p>比方说，如果我回答你：<strong>看来似乎是 zentry 卡住了；你应该先清除它。</strong>，然后，这是一个**<em>很糟的</em>**后续问题回应：<strong>zentry是什么？</strong> **<em>好</em>**的问法应该是这样：<strong>哦~~~我看过说明了但是只有 -z 和 -p 两个参数中提到了 zentries，而且还都没有清楚的解释如何清除它。你是指这两个中的哪一个吗？还是我看漏了什么？</strong></p><h3 id="处理无礼的回应"><a href="#处理无礼的回应" class="headerlink" title="处理无礼的回应"></a>处理无礼的回应</h3><p>很多黑客圈子中看似无礼的行为并不是存心冒犯。相反，它是直接了当，一针见血式的交流风格，这种风格更注重解决问题，而不是使人感觉舒服而却模模糊糊。</p><p>如果你觉得被冒犯了，试着平静地反应。如果有人真的做了出格的事，邮件列表、新闻群组或论坛中的前辈多半会招呼他。如果这**<em>没有</em><strong>发生而你却发火了，那么你发火对象的言语可能在黑客社区中看起来是正常的，而</strong><em>你</em>**将被视为有错的一方，这将伤害到你获取信息或帮助的机会。</p><p>另一方面，你偶而真的会碰到无礼和无聊的言行。与上述相反，对真正的冒犯者狠狠地打击，用犀利的语言将其驳得体无完肤都是可以接受的。然而，在行事之前一定要非常非常的有根据。纠正无礼的言论与开始一场毫无意义的口水战仅一线之隔，黑客们自己莽撞地越线的情况并不鲜见。如果你是新手或外人，避开这种莽撞的机会并不高。如果你想得到的是信息而不是消磨时光，这时最好不要把手放在键盘上以免冒险。</p><p>（有些人断言很多黑客都有轻度的自闭症或亚斯伯格综合症，缺少用于润滑人类社会<strong>正常</strong>交往所需的神经。这既可能是真也可能是假的。如果你自己不是黑客，兴许你认为我们脑袋有问题还能帮助你应付我们的古怪行为。只管这么干好了，我们不在乎。我们**<em>喜欢</em>**我们现在这个样子，并且通常对病患标记都有站得住脚的怀疑。）</p><p>在下一节，我们会谈到另一个问题，当**<em>你</em><strong>行为不当时所会受到的</strong>冒犯**。</p><h2 id="如何避免扮演失败者"><a href="#如何避免扮演失败者" class="headerlink" title="如何避免扮演失败者"></a>如何避免扮演失败者</h2><p>在黑客社区的论坛中有那么几次你可能会搞砸 – 以本指南所描述到的或类似的方式。而你会在公开场合中被告知你是如何搞砸的，也许攻击的言语中还会带点夹七夹八的颜色。</p><p>这种事发生以后，你能做的最糟糕的事莫过于哀嚎你的遭遇、宣称被口头攻击、要求道歉、高声尖叫、憋闷气、威胁诉诸法律、向其雇主报怨、忘了关马桶盖等等。相反地，你该这么做：</p><p>熬过去，这很正常。事实上，它是有益健康且合理的。</p><p>社区的标准不会自行维持，它们是通过参与者积极而**<em>公开地</em>**执行来维持的。不要哭嚎所有的批评都应该通过私下的邮件传送，它不是这样运作的。当有人评论你的一个说法有误或者提出不同看法时，坚持声称受到个人攻击也毫无益处，这些都是失败者的态度。</p><p>也有其它的黑客论坛，受过高礼节要求的误导，禁止参与者张贴任何对别人帖子挑毛病的消息，并声称<strong>如果你不想帮助用户就闭嘴。</strong> 结果造成有想法的参与者纷纷离开，这么做只会使它们沦为毫无意义的嘮叨与无用的技术论坛。</p><p>夸张的讲法是：你要的是<strong>友善</strong>（以上述方式）还是有用？两个里面挑一个。</p><p>记着：当黑客说你搞砸了，并且（无论多么刺耳）告诉你别再这样做时，他正在为关心<strong>你</strong>和<strong>他的社区</strong>而行动。对他而言，不理你并将你从他的生活中滤掉更简单。如果你无法做到感谢，至少要表现地有点尊严，别大声哀嚎，也别因为自己是个有戏剧性超级敏感的灵魂和自以为有资格的新来者，就指望别人像对待脆弱的洋娃娃那样对你。</p><p>有时候，即使你没有搞砸（或者只是在他的想像中你搞砸了），有些人也会无缘无故地攻击你本人。在这种情况下，抱怨倒是**<em>真的</em>**会把问题搞砸。</p><p>这些来找麻烦的人要么是毫无办法但自以为是专家的不中用家伙，要么就是测试你是否真会搞砸的心理专家。其它读者要么不理睬，要么用自己的方式对付他们。这些来找麻烦的人在给他们自己找麻烦，这点你不用操心。</p><p>也别让自己卷入口水战，最好不要理睬大多数的口水战 – 当然，是在你检验它们只是口水战，而并未指出你有搞砸的地方，且也没有巧妙地将问题真正的答案藏于其后（这也是有可能的）。</p><h2 id="不该问的问题"><a href="#不该问的问题" class="headerlink" title="不该问的问题"></a>不该问的问题</h2><p>以下是几个经典蠢问题，以及黑客没回答时心中所想的：</p><p>问题：<a href="#q1">我能在哪找到 X 程序或 X 资源？</a></p><p>问题：<a href="#q2">我怎样用 X 做 Y？</a></p><p>问题：<a href="#q3">如何设定我的 shell 提示？</a></p><p>问题：<a href="#q4">我可以用 Bass-o-matic 文件转换工具将 AcmeCorp 档案转换为 TeX 格式吗？</a></p><p>问题：<a href="#q5">我的程序&#x2F;设定&#x2F;SQL语句没有用</a></p><p>问题：<a href="#q6">我的 Windows 电脑有问题，你能帮我吗？</a></p><p>问题：<a href="#q7">我的程序不会动了，我认为系统工具 X 有问题</a></p><p>问题：<a href="#q8">我在安装 Linux（或者 X ）时有问题，你能帮我吗？</a></p><p>问题：<a href="#q9">我怎么才能破解 root 帐号&#x2F;窃取 OP 特权&#x2F;读别人的邮件呢？</a></p><hr><p><a id="q1"></a></p><blockquote><p>问题：我能在哪找到 X 程序或 X 资源？</p></blockquote><p>回答：就在我找到它的地方啊，白痴 – 搜索引擎的那一头。天哪！难道还有人不会用 <a href="http://www.google.com/">Google</a> 吗？</p><p><a id="q2"></a></p><blockquote><p>问题：我怎样用 X 做 Y？</p></blockquote><p>回答：如果你想解决的是 Y ，提问时别给出可能并不恰当的方法。这种问题说明提问者不但对 X 完全无知，也对 Y 要解决的问题糊涂，还被特定形势禁锢了思维。最好忽略这种人，等他们把问题搞清楚了再说。</p><p><a id="q3"></a></p><blockquote><p>问题：如何设定我的 shell 提示？？</p></blockquote><p>回答：如果你有足够的智慧提这个问题，你也该有足够的智慧去 <a href="#RTFM">RTFM</a>，然后自己去找出来。</p><p><a id="q4"></a></p><blockquote><p>问题：我可以用 Bass-o-matic 文件转换工具将 AcmeCorp 档案转换为 TeX 格式吗？</p></blockquote><p>回答：试试看就知道了。如果你试过，你既知道了答案，就不用浪费我的时间了。</p><p><a id="q5"></a></p><blockquote><p>问题：我的程序&#x2F;设定&#x2F;SQL语句没有用</p></blockquote><p>回答：这不算是问题吧，我对要我问你二十个问题才找得出你真正问题的问题没兴趣 – 我有更有意思的事要做呢。在看到这类问题的时候，我的反应通常不外如下三种</p><ul><li>你还有什么要补充的吗？</li><li>真糟糕，希望你能搞定。</li><li>这关我有什么屁事？</li></ul><p><a id="q6"></a></p><blockquote><p>问题：我的 Windows 电脑有问题，你能帮我吗？</p></blockquote><p>回答：能啊，扔掉萎软的垃圾，换个像 Linux 或 BSD 的开放源代码操作系统吧。</p><p>注意：如果程序有官方版 Windows 或者与 Windows 有互动（如Samba），你**<em>可以</em>**问与Windows相关的问题， 只是别对问题是由 Windows 操作系统而不是程序本身造成的回复感到惊讶， 因为 Windows 一般来说实在太烂，这种说法通常都是对的。</p><p><a id="q7"></a></p><blockquote><p>问题：我的程序不会动了，我认为系统工具 X 有问题</p></blockquote><p>回答：你完全有可能是第一个注意到被成千上万用户反复使用的系统调用与函数库档案有明显缺陷的人，更有可能的是你完全没有根据。不同凡响的说法需要不同凡响的证据，当你这样声称时，你必须有清楚而详尽的缺陷说明文件作后盾。</p><p><a id="q8"></a></p><blockquote><p>问题：我在安装 Linux（或者 X ）时有问题，你能帮我吗？</p></blockquote><p>回答：不能，我只有亲自在你的电脑上动手才能找到毛病。还是去找你当地的 Linux 使用群组者寻求实际的指导吧（你能在<a href="http://www.linux.org/groups/index.html">这儿</a>找到使用者群组的清单）。</p><p>注意：如果安装问题与某 Linux 的发行版有关，在它的邮件列表、论坛或本地使用者群组中提问也许是恰当的。此时，应描述问题的准确细节。在此之前，先用 <strong>Linux</strong> 和**<em>所有</em>**被怀疑的硬件作关键词仔细搜索。</p><p><a id="q9"></a></p><blockquote><p>问题：我怎么才能破解 root 帐号&#x2F;窃取 OP 特权&#x2F;读别人的邮件呢？</p></blockquote><p>回答：想要这样做，说明了你是个卑鄙小人；想找个黑客帮你，说明你是个白痴！</p><h2 id="好问题与蠢问题"><a href="#好问题与蠢问题" class="headerlink" title="好问题与蠢问题"></a>好问题与蠢问题</h2><p>最后，我将透过举一些例子，来说明怎样聪明的提问；同一个问题的两种问法被放在一起，一种是愚蠢的，另一种才是明智的。</p><p><strong><em>蠢问题</em></strong>：</p><blockquote><p>我可以在哪儿找到关于 Foonly Flurbamatic 的资料？</p></blockquote><p>这种问法无非想得到 <a href="#RTFM">STFW</a> 这样的回答。</p><p><strong><em>聪明问题</em></strong>：</p><blockquote><p>我用Google搜索过 “Foonly Flurbamatic 2600”，但是没找到有用的结果。谁知道上哪儿去找对这种设备编程的资料？</p></blockquote><p>这个问题已经 STFW 过了，看起来他真的遇到了麻烦。</p><p><strong><em>蠢问题</em></strong></p><blockquote><p>我从 foo 项目找来的源码没法编译。它怎么这么烂？</p></blockquote><p>他觉得都是别人的错，这个傲慢自大的提问者</p><p><strong><em>聪明问题</em></strong></p><blockquote><p>foo 项目代码在 Nulix 6.2 版下无法编译通过。我读过了 FAQ，但里面没有提到跟 Nulix 有关的问题。这是我编译过程的记录，我有什么做的不对的地方吗？</p></blockquote><p>提问者已经指明了环境，也读过了FAQ，还列出了错误，并且他没有把问题的责任推到别人头上，他的问题值得被关注。</p><p><strong><em>蠢问题</em></strong></p><blockquote><p>我的主机板有问题了，谁来帮我？</p></blockquote><p>某黑客对这类问题的回答通常是：<strong>好的，还要帮你拍拍背和换尿布吗？</strong>，然后按下删除键。</p><p><strong><em>聪明问题</em></strong></p><blockquote><p>我在 S2464 主机板上试过了 X 、 Y 和 Z ，但没什么作用，我又试了 A 、 B 和 C 。请注意当我尝试 C 时的奇怪现象。显然 florbish 正在 grommicking，但结果出人意料。通常在 Athlon MP 主机板上引起 grommicking 的原因是什么？有谁知道接下来我该做些什么测试才能找出问题？</p></blockquote><p>这个家伙，从另一个角度来看，值得去回答他。他表现出了解决问题的能力，而不是坐等天上掉答案。</p><p>在最后一个问题中，注意<strong>告诉我答案</strong>和<strong>给我启示，指出我还应该做什么诊断工作</strong>之间微妙而又重要的区别。</p><p>事实上，后一个问题源自于 2001 年 8 月在 Linux 内核邮件列表（lkml）上的一个真实的提问。我（Eric）就是那个提出问题的人。我在 Tyan S2464 主板上观察到了这种无法解释的锁定现象，列表成员们提供了解决这一问题的重要信息。</p><p>通过我的提问方法，我给了别人可以咀嚼玩味的东西；我设法让人们很容易参与并且被吸引进来。我显示了自己具备和他们同等的能力，并邀请他们与我共同探讨。通过告诉他们我所走过的弯路，以避免他们再浪费时间，我也表明了对他们宝贵时间的尊重。</p><p>事后，当我向每个人表示感谢，并且赞赏这次良好的讨论经历的时候， 一个 Linux 内核邮件列表的成员表示，他觉得我的问题得到解决并非由于我是这个列表中的**<em>名人</em>**，而是因为我用了正确的方式来提问。</p><p>黑客从某种角度来说是拥有丰富知识但缺乏人情味的家伙；我相信他是对的，如果我**<em>像</em>**个乞讨者那样提问，不论我是谁，一定会惹恼某些人或者被他们忽视。他建议我记下这件事，这直接导致了本指南的出现。</p><h2 id="如果得不到回答"><a href="#如果得不到回答" class="headerlink" title="如果得不到回答"></a>如果得不到回答</h2><p>如果仍得不到回答，请不要以为我们觉得无法帮助你。有时只是看到你问题的人不知道答案罢了。没有回应不代表你被忽视，虽然不可否认这种差别很难区分。</p><p>总的来说，简单的重复张贴问题是个很糟的点子。这将被视为无意义的喧闹。有点耐心，知道你问题答案的人可能生活在不同的时区，可能正在睡觉，也有可能你的问题一开始就没有组织好。</p><p>你可以通过其他渠道获得帮助，这些渠道通常更适合初学者的需要。</p><p>有许多网上的以及本地的使用者群组，由热情的软件爱好者（即使他们可能从没亲自写过任何软件）组成。通常人们组建这样的团体来互相帮助并帮助新手。</p><p>另外，你可以向很多商业公司寻求帮助，不论公司大还是小。别为要付费才能获得帮助而感到沮丧！毕竟，假使你的汽车发动机汽缸密封圈爆掉了– 完全可能如此 –你还得把它送到修车铺，并且为维修付费。就算软件没花费你一分钱，你也不能强求技术支持总是免费的。</p><p>对像是 Linux 这种大众化的软件，每个开发者至少会对应到上万名使用者。根本不可能由一个人来处理来自上万名使用者的求助电话。要知道，即使你要为这些协助付费，和你所购买的同类软件相比，你所付出的也是微不足道的（通常封闭源代码软件的技术支持费用比开放源代码软件的要高得多，且内容也没那么丰富）。</p><h2 id="如何更好地回答问题"><a href="#如何更好地回答问题" class="headerlink" title="如何更好地回答问题"></a>如何更好地回答问题</h2><p><strong><em>态度和善一点</em></strong>。问题带来的压力常使人显得无礼或愚蠢，其实并不是这样。</p><p><strong><em>对初犯者私下回复</em></strong>。对那些坦诚犯错之人没有必要当众羞辱，一个真正的新手也许连怎么搜索或在哪找常见问题都不知道。</p><p><strong><em>如果你不确定，一定要说出来</em></strong>！一个听起来权威的错误回复比没有还要糟，别因为听起来像个专家很好玩，就给别人乱指路。要谦虚和诚实，给提问者与同行都树个好榜样。</p><p><strong><em>如果帮不了忙，也别妨碍他</em></strong>。不要在实际步骤上开玩笑，那样也许会毁了使用者的设置 –有些可怜的呆瓜会把它当成真的指令。</p><p><strong><em>试探性的反问以引出更多的细节</em></strong>。如果你做得好，提问者可以学到点东西 –你也可以。试试将蠢问题转变成好问题，别忘了我们都曾是新手。</p><p>尽管对那些懒虫抱怨一声 RTFM 是正当的，能指出文件的位置（即使只是建议个 Google 搜索关键词）会更好。</p><p><strong><em>如果你决定回答，就请给出好的答案</em></strong>。当别人正在用错误的工具或方法时别建议笨拙的权宜之计（wordaround），应推荐更好的工具，重新界定问题。</p><p><strong><em>正面的回答问题</em></strong>！如果这个提问者已经很深入的研究而且也表明已经试过 X 、 Y 、 Z 、 A 、 B 、 C 但没得到结果，回答 <strong>试试看 A 或是 B</strong> 或者 <strong>试试X 、 Y 、 Z 、 A 、 B 、 C</strong> 并附上一个链接一点用都没有。</p><p><strong><em>帮助你的社区从问题中学习</em></strong>。当回复一个好问题时，问问自己<strong>如何修改相关文件或常见问题文件以免再次解答同样的问题？</strong>，接着再向文件维护者发一份补丁。</p><p>如果你是在研究一番后才做出的回答，<strong><em>展现你的技巧而不是直接端出结果</em></strong>。毕竟<strong>授人以鱼不如授人以渔</strong>。</p><h2 id="相关资源"><a href="#相关资源" class="headerlink" title="相关资源"></a>相关资源</h2><p>如果你需要个人电脑、Unix 系统和网络如何运作的基础知识，参阅<a href="http://en.tldp.org/HOWTO/Unix-and-Internet-Fundamentals-HOWTO/">Unix系统和网络基本原理</a>。</p><p>当你发布软件或补丁时，试着按<a href="http://en.tldp.org/HOWTO/Software-Release-Practice-HOWTO/index.html">软件发布实践</a>操作。</p><h2 id="鸣谢"><a href="#鸣谢" class="headerlink" title="鸣谢"></a>鸣谢</h2><p>Evelyn Mitchel贡献了一些愚蠢问题例子并启发了编写<strong>如何更好地回答问题</strong>这一节， Mikhail Ramendik贡献了一些特别有价值的建议和改进。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;#提问的智慧&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How To Ask Questions The Smart Way&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Copyright © 2001,2006,2014 Eric S. Raymond, Rick Moen&lt;/p&gt;
&lt;p&gt;本指南</summary>
      
    
    
    
    <category term="转载" scheme="https://blog.jugg.xyz/categories/%E8%BD%AC%E8%BD%BD/"/>
    
    
  </entry>
  
  <entry>
    <title>Vim 从入门到精通</title>
    <link href="https://blog.jugg.xyz/2018/02/19/repost/Vim-Galore/"/>
    <id>https://blog.jugg.xyz/2018/02/19/repost/Vim-Galore/</id>
    <published>2018-02-19T06:49:13.000Z</published>
    <updated>2026-05-02T11:24:14.335Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Vim-从入门到精通"><a href="#Vim-从入门到精通" class="headerlink" title="Vim 从入门到精通"></a>Vim 从入门到精通</h1><blockquote><p>本文主要在翻译 <a href="https://github.com/mhinz/vim-galore">mhinz&#x2F;vim-galore</a><br>的基础添加了一些我在使用 Vim 及开发 Vim 插件的过程中积累的一些知识和常用插件列表。</p></blockquote><p><strong>Vim 中文同步聊天室</strong></p><ul><li>telegram： <a href="https://t.me/VimHub">@VimHub</a></li><li>gitter: <a href="https://gitter.im/vim-china/Lobby">vim-china&#x2F;Lobby</a></li><li>IRC: <a href="https://webchat.freenode.net/?channels=vim-china">#vim-china</a></li></ul><!-- vim-markdown-toc GFM --><h1 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h1><h2 id="什么是-Vim？"><a href="#什么是-Vim？" class="headerlink" title="什么是 Vim？"></a>什么是 Vim？</h2><p><a href="https://github.com/vim/vim">Vim</a> 是一个历史悠久的文本编辑器，可以追溯到<br><a href="https://en.wikipedia.org/wiki/QED_(text_editor)">qed</a>。<br><a href="https://en.wikipedia.org/wiki/Bram_Moolenaar">Bram Moolenaar</a> 于<br>1991 年发布初始版本。</p><p>Linux、Mac 用户，可以使用包管理器安装 Vim，对于 Windows 用户，可以从<br><a href="https://share.weiyun.com/da2be5937ac0e2bd3abc26355fad1204">我的网盘</a> 下载。<br>该版本可轻易添加 <code>python</code> 、<code>python3</code> 、<code>lua</code> 等支持，只需要安装 python、lua<br>即可。</p><p>项目在 <a href="https://github.com/vim/vim">Github</a> 上开发，项目讨论请订阅<br><a href="https://groups.google.com/forum/#!forum/vim_dev"><code>vim_dev</code></a> 邮件列表。</p><p>通过阅读 <a href="http://www.viemu.com/a-why-vi-vim.html">Why, oh WHY, do those #?@! nutheads use vi?</a><br>来对 Vim 进行大致的了解。</p><h2 id="Vim-哲学"><a href="#Vim-哲学" class="headerlink" title="Vim 哲学"></a>Vim 哲学</h2><p>Vim 采用模式编辑的理念，即它提供了多种模式，按键在不同的模式下作用不同。<br>你可以在<strong>普通模式</strong> 下浏览文件，在<strong>插入模式</strong>下插入文本，<br>在<strong>可视模式</strong>下选择行，在<strong>命令模式</strong>下执行命令等等。起初这听起来可能很复杂，<br>但是这有一个很大的优点：不需要通过同时按住多个键来完成操作，<br>大多数时候你只需要依次按下这些按键即可。越常用的操作，所需要的按键数量越少。</p><p>和模式编辑紧密相连的概念是 <strong>操作符</strong> 和 <strong>动作</strong>。<strong>操作符</strong> 指的是开始某个行为，<br>例如：修改、删除或者选择文本，之后你要用一个 <strong>动作</strong> 来指定需要操作的文本区域。<br>比如，要改变括号内的文本，需要执行 <code>ci(</code> （读做 <code>change inner parentheses</code>）；<br>删除整个段落的内容，需要执行 <code>dap</code> （读做：<code>delete around paragraph</code>）。</p><p>如果你能看见 Vim 老司机操作，你会发现他们使用 Vim 脚本语言就如同钢琴师弹钢琴一样。复杂的操作只需要几个按键就能完成。他们甚至不用刻意去想，因为这已经成为<a href="https://en.wikipedia.org/wiki/Muscle_memory">肌肉记忆</a>了。这减少<a href="https://en.wikipedia.org/wiki/Cognitive_load">认识负荷</a>并帮助人们专注于实际任务。</p><h2 id="入门"><a href="#入门" class="headerlink" title="入门"></a>入门</h2><p>Vim 自带一个交互式的教程，内含你需要了解的最基础的信息，你可以通过终端运行以下命令打开教程：</p><pre><code>$ vimtutor</code></pre><p>不要因为这个看上去很无聊而跳过，按照此教程多练习。你以前用的 IDE 或者其他编辑器很少是有“模式”概念的，因此一开始你会很难适应模式切换。但是你 Vim 使用的越多，<a href="https://en.wikipedia.org/wiki/Muscle_memory">肌肉记忆</a> 将越容易形成。</p><p>Vim 基于一个 <a href="https://en.wikipedia.org/wiki/Vi">vi</a> 克隆，叫做 <a href="https://en.wikipedia.org/wiki/Stevie_(text_editor)">Stevie</a>，支持两种运行模式：”compatible” 和 “nocompatible”。在兼容模式下运行 Vim 意味着使用 vi 的默认设置，而不是 Vim 的默认设置。除非你新建一个用户的 <code>vimrc</code> 或者使用 <code>vim -N</code> 命令启动 Vim，否则就是在兼容模式下运行 Vim！请大家不要在兼容模式下运行 Vim。</p><p>下一步</p><ol><li>创建你自己的 <a href="#%E7%B2%BE%E7%AE%80%E7%9A%84-vimrc">vimrc</a>。</li><li>在第一周准备<a href="#%E5%A4%87%E5%BF%98%E5%BD%95">备忘录</a>。</li><li>通读<a href="#%E5%9F%BA%E7%A1%80-1">基础</a>章节了解 Vim 还有哪些功能。</li><li>按需学习！Vim 是学不完的。如果你遇到了问题，先上网寻找解决方案，你的问题可能已经被解决了。Vim 拥有大量的参考文档，知道如何利用这些参考文档很有必要：<a href="#%E8%8E%B7%E5%8F%96%E7%A6%BB%E7%BA%BF%E5%B8%AE%E5%8A%A9">获取离线帮助</a>。</li><li>浏览<a href="#%E9%99%84%E5%8A%A0%E8%B5%84%E6%BA%90">附加资源</a>。</li></ol><p>最后一个建议：使用<a href="#%E6%8F%92%E4%BB%B6%E7%AE%A1%E7%90%86">插件</a>之前，请先掌握 Vim 的基本操作。很多插件都只是对 Vim 自带功能的封装。</p><p>返回主目录 <a href="#%E7%AE%80%E4%BB%8B">:arrow_heading_up:</a></p><h2 id="精简的-vimrc"><a href="#精简的-vimrc" class="headerlink" title="精简的 vimrc"></a>精简的 vimrc</h2><p>用户的 vimrc 配置文件可以放在 <code>~/.vimrc</code>，或者为了更好的分离放在 <code>~/.vim/vimrc</code>，后者更便于通过版本控制软件备份和同步整个配置，比方说 Github。</p><p>你可以在网上找到许多精简的 vimrc 配置文件，我的版本可能并不是最简单的版本，但是我的版本提供了一套我认为良好的，非常适合入门的设置。</p><p>最终你需要阅读完那些设置，然后自行决定需要使用哪些。:-)</p><p>精简的 vimrc 地址：<a href="contents/minimal-vimrc.vim">minimal-vimrc</a></p><p>如果你有兴趣，这里是我（原作者）的 <a href="https://github.com/mhinz/dotfiles/blob/master/vim/vimrc">vimrc</a>。</p><p><strong>建议</strong>：大多数插件作者都维护不止一个插件并且将他们的 vimrc 放在 Github 上展示（通常放在叫做 “vim-config” 或者 “dotfiles” 的仓库中），所以当你发现你喜欢的插件时，去插件维护者的 Github 主页看看有没有这样的仓库。</p><p>返回主目录 <a href="#%E7%AE%80%E4%BB%8B">:arrow_heading_up:</a></p><h2 id="我正在使用什么样的-Vim"><a href="#我正在使用什么样的-Vim" class="headerlink" title="我正在使用什么样的 Vim"></a>我正在使用什么样的 Vim</h2><p>使用 <code>:version</code> 命令将向你展示当前正在运行的 Vim 的所有相关信息，包括它是如何编译的。</p><p>第一行告诉你这个二进制文件的编译时间和版本号，比如：7.4。接下来的一行呈现 <code>Included patches: 1-1051</code>，这是补丁版本包。因此你 Vim 确切的版本号是 7.4.1051。</p><p>另一行显示着一些像 <code>Tiny version without GUI</code> 或者 <code>Huge version with GUI</code> 的信息。很显然这些信息告诉你当前的 Vim 是否支持 GUI，例如：从终端中运行 <code>gvim</code> 或者从终端模拟器中的 Vim 内运行 <code>:gui</code> 命令。另一个重要的信息是 <code>Tiny</code> 和 <code>Huge</code>。Vim 的特性集区分被叫做 <code>tiny</code>，<code>small</code>，<code>normal</code>，<code>big</code> and <code>huge</code>，所有的都实现不同的功能子集。</p><p><code>:version</code> 主要的输出内容是特性列表。<code>+clipboard</code> 意味这剪贴板功能被编译支持了，<code>-clipboard</code> 意味着剪贴板特性没有被编译支持。</p><p>一些功能特性需要编译支持才能正常工作。例如：为了让 <code>:prof</code> 工作，你需要使用 <code>huge</code> 模式编译的 Vim，因为那种模式启用了 <code>+profile</code> 特性。</p><p>如果你的输出情况并不是那样，并且你是从包管理器安装 Vim 的，确保你安装了 <code>vim-x</code>，<code>vim-x11</code>，<code>vim-gtk</code>，<code>vim-gnome</code> 这些包或者相似的，因为这些包通常都是 <code>huge</code> 模式编译的。</p><p>你也可以运行下面这段代码来测试 Vim 版本以及功能支持：</p><figure class="highlight vim"><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><span class="line"><span class="comment">&quot; Do something if running at least Vim 7.4.42 with +profile enabled.</span></span><br><span class="line"><span class="keyword">if</span> (<span class="variable">v:version</span> &gt; <span class="number">704</span> || <span class="variable">v:version</span> == <span class="number">704</span> &amp;&amp; <span class="built_in">has</span>(<span class="string">&#x27;patch42&#x27;</span>)) &amp;&amp; <span class="built_in">has</span>(<span class="string">&#x27;profile&#x27;</span>)</span><br><span class="line">  <span class="comment">&quot; do stuff</span></span><br><span class="line"><span class="keyword">endif</span></span><br></pre></td></tr></table></figure><p>相关帮助：</p><pre><code>:h :version:h feature-list:h +feature-list:h has-patch</code></pre><p>返回主目录 <a href="#%E7%AE%80%E4%BB%8B">:arrow_heading_up:</a></p><h2 id="备忘录"><a href="#备忘录" class="headerlink" title="备忘录"></a>备忘录</h2><p>为了避免版权问题，我只贴出链接：</p><ul><li><a href="http://people.csail.mit.edu/vgod/vim/vim-cheat-sheet-en.png">http://people.csail.mit.edu/vgod/vim/vim-cheat-sheet-en.png</a></li><li><a href="https://cdn.shopify.com/s/files/1/0165/4168/files/preview.png">https://cdn.shopify.com/s/files/1/0165/4168/files/preview.png</a></li><li><a href="http://www.nathael.org/Data/vi-vim-cheat-sheet.svg">http://www.nathael.org/Data/vi-vim-cheat-sheet.svg</a></li><li><a href="http://michael.peopleofhonoronly.com/vim/vim_cheat_sheet_for_programmers_screen.png">http://michael.peopleofhonoronly.com/vim/vim_cheat_sheet_for_programmers_screen.png</a></li><li><a href="http://www.rosipov.com/images/posts/vim-movement-commands-cheatsheet.png">http://www.rosipov.com/images/posts/vim-movement-commands-cheatsheet.png</a></li></ul><p>或者在 Vim 中快速打开备忘录：<a href="https://github.com/lifepillar/vim-cheat40">vim-cheat40</a>。</p><p>返回主目录 <a href="#%E7%AE%80%E4%BB%8B">:arrow_heading_up:</a></p><h1 id="基础"><a href="#基础" class="headerlink" title="基础"></a>基础</h1><h2 id="缓冲区，窗口，标签"><a href="#缓冲区，窗口，标签" class="headerlink" title="缓冲区，窗口，标签"></a>缓冲区，窗口，标签</h2><p>Vim 是一个文本编辑器。每次文本都是作为<strong>缓冲区</strong>的一部分显示的。每一份文件都是在他们自己独有的缓冲区打开的，插件显示的内容也在它们自己的缓冲区中。</p><p>缓冲区有很多属性，比如这个缓冲区的内容是否可以修改，或者这个缓冲区是否和文件相关联，是否需要同步保存到磁盘上。</p><p><strong>窗口</strong> 是缓冲区上一层的视窗。如果你想同时查看几个文件或者查看同一文件的不同位置，那样你会需要窗口。</p><p>请别把他们叫做 <em>分屏</em> 。你可以把一个窗口分割成两个，但是这并没有让这两个窗口完全 <em>分离</em> 。</p><p>窗口可以水平或者竖直分割并且现有窗口的高度和宽度都是可以被调节设置的，因此，如果你需要多种窗口布局，请考虑使用标签。</p><p><strong>标签页</strong> （标签）是窗口的集合。因此当你想使用多种窗口布局时候请使用标签。</p><p>简单的说，如果你启动 Vim 的时候没有附带任何参数，你会得到一个包含着一个呈现一个缓冲区的窗口的标签。</p><p>顺带提一下，缓冲区列表是全局可见的，你可以在任何标签中访问任何一个缓冲区。</p><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="已激活、已载入、已列出、已命名的缓冲区"><a href="#已激活、已载入、已列出、已命名的缓冲区" class="headerlink" title="已激活、已载入、已列出、已命名的缓冲区"></a>已激活、已载入、已列出、已命名的缓冲区</h2><p>用类似 <code>vim file1</code> 的命令启动 Vim 。这个文件的内容将会被加载到缓冲区中，你现在有一个<strong>已载入的缓冲区</strong>。如果你在 Vim 中保存这个文件，缓冲区内容将会被同步到磁盘上（写回文件中）。</p><p>由于这个缓冲区也在一个窗口上显示，所以他也是一个<strong>已激活的缓冲区</strong>。如果你现在通过 <code>:e file2</code> 命令加载另一个文件，<code>file1</code> 将会变成一个<strong>隐藏的缓冲区</strong>，并且 <code>file2</code> 变成已激活缓冲区。</p><p>使用 <code>:ls</code> 我们能够列出所有可以列出的缓冲区。插件缓冲区和帮助缓冲区通常被标记为不可以列出的缓冲区，因为那并不是你经常需要在编辑器中编辑的常规文件。通过 <code>:ls!</code> 命令可以显示被放入缓冲区列表的和未被放入列表的缓冲区。</p><p><strong>未命名的缓冲区</strong>是一种没有关联特定文件的缓冲区，这种缓冲区经常被插件使用。比如 <code>:enew</code> 将会创建一个无名临时缓冲区。添加一些文本然后使用 <code>:w /tmp/foo</code> 将他写入到磁盘，这样这个缓冲区就会变成一个<strong>已命名的缓冲区</strong>。</p><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="参数列表"><a href="#参数列表" class="headerlink" title="参数列表"></a>参数列表</h2><p><a href="#%E7%BC%93%E5%86%B2%E5%8C%BA%E7%AA%97%E5%8F%A3%E6%A0%87%E7%AD%BE">全局缓冲区列表</a>是 Vim 的特性。在这之前的 vi 中，仅仅只有参数列表，参数列表在 Vim 中依旧可以使用。</p><p>每一个通过 shell 命令传递给 Vim 的文件名都被记录在一个参数列表中。可以有多个参数列表：默认情况下所有参数都被放在全局参数列表下，但是你可以使用 <code>:arglocal</code> 命令去创建一个新的本地窗口的参数列表。</p><p>使用 <code>:args</code> 命令可以列出当前参数。使用 <code>:next</code>，<code>:previous</code>，<code>:first</code>，<code>:last</code> 命令可以在切换在参数列表中的文件。通过使用 <code>:argadd</code>，<code>:argdelete</code> 或者 <code>:args</code> 等命令加上一个文件列表可以改变参数列表。</p><p>偏爱缓冲区列表还是参数列表完全是个人选择，我的印象中大多数人都是使用缓冲区列表的。</p><p>然而参数列表在有些情况下被大量使用：批处理<br>使用 <code>:argdo</code>！ 一个简单的重构例子：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">args</span> **/*.[ch]</span><br><span class="line">:<span class="keyword">argdo</span> %s/foo/bar/ge | <span class="keyword">update</span></span><br></pre></td></tr></table></figure><p>这条命令将替换掉当前目录下以及当前目录的子目录中所有的 C 源文件和头文件中的“foo”，并用“bar”代替。</p><p>相关帮助：<code>:h argument-list</code></p><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="按键映射"><a href="#按键映射" class="headerlink" title="按键映射"></a>按键映射</h2><p>使用 <code>:map</code> 命令家族你可以定义属于你自己的快捷键。该家族的每一个命令都限定在特定的模式下。从技术上来说 Vim 自带高达 12 中模式，其中 6 种可以被映射。另外一些命令作用于多种模式：</p><table><thead><tr><th>递归</th><th>非递归</th><th>模式</th></tr></thead><tbody><tr><td><code>:map</code></td><td><code>:noremap</code></td><td>normal, visual, operator-pending</td></tr><tr><td><code>:nmap</code></td><td><code>:nnoremap</code></td><td>normal</td></tr><tr><td><code>:xmap</code></td><td><code>:xnoremap</code></td><td>visual</td></tr><tr><td><code>:cmap</code></td><td><code>:cnoremap</code></td><td>command-line</td></tr><tr><td><code>:omap</code></td><td><code>:onoremap</code></td><td>operator-pending</td></tr><tr><td><code>:imap</code></td><td><code>:inoremap</code></td><td>insert</td></tr></tbody></table><p>例如：这个自定义的快捷键只在普通模式下工作。</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">nmap</span> <span class="symbol">&lt;space&gt;</span> :<span class="keyword">echo</span> <span class="string">&quot;foo&quot;</span><span class="symbol">&lt;cr&gt;</span></span><br></pre></td></tr></table></figure><p>使用 <code>:nunmap &lt;space&gt;</code> 可以取消这个映射。</p><p>对于更少数，不常见的模式（或者他们的组合），查看 <code>:h map-modes</code>。</p><p>到现在为止还好，对新手而言有一个问题会困扰他们：<code>:nmap</code> 是<strong>递归执行</strong>的！结果是，右边执行可能的映射。</p><p>你自定义了一个简单的映射去输出“Foo”：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">nmap</span> <span class="keyword">b</span> :<span class="keyword">echo</span> <span class="string">&quot;Foo&quot;</span><span class="symbol">&lt;cr&gt;</span></span><br></pre></td></tr></table></figure><p>但是如果你想要映射 <code>b</code> （回退一个单词）的默认功能到一个键上呢？</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">nmap</span> <span class="keyword">a</span> <span class="keyword">b</span></span><br></pre></td></tr></table></figure><p>如果你敲击<kbd>a</kbd>，我们期望着光标回退到上一个单词，但是实际情况是“Foo”被输出到命令行里！因为在右边，<code>b</code> 已经被映射到别的行为上了，换句话说就是 <code>:echo &quot;Foo&quot;&lt;cr&gt;</code>。</p><p>解决此问题的正确方法是使用一种 <em>非递归</em> 的映射代替：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">nnoremap</span> <span class="keyword">a</span> <span class="keyword">b</span></span><br></pre></td></tr></table></figure><p>经验法则：除非递归是必须的，否则总是使用非递归映射。</p><p>通过不给一个右值来检查你的映射。比如<code>:nmap</code> 显示所以普通模式下的映射，<code>:nmap &lt;leader&gt;</code> 显示所有以 <code>&lt;leader&gt;</code> 键开头的普通模式下的映射。</p><p>如果你想禁止用标准映射，把他们映射到特殊字符 <code>&lt;nop&gt;</code> 上，例如：<code>:noremap &lt;left&gt; &lt;nop&gt;</code>。</p><p>相关帮助：</p><figure class="highlight vim"><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><span class="line">:h key-notation</span><br><span class="line">:h mapping</span><br><span class="line">:h <span class="number">05.3</span></span><br></pre></td></tr></table></figure><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="映射前置键"><a href="#映射前置键" class="headerlink" title="映射前置键"></a>映射前置键</h2><p>映射前置键（Leader 键）本身就是一个按键映射，默认为 <kbd>\</kbd>。我们可以通过在 <code>map</code> 中调用 <code>&lt;leader&gt;</code> 来为把它添加到其他按键映射中。</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;leader&gt;</span>h :<span class="keyword">helpgrep</span><span class="symbol">&lt;space&gt;</span></span><br></pre></td></tr></table></figure><p>这样，我们只需要先按 <kbd>\</kbd> 然后连续按 <kbd>\h</kbd> 就可以激活这个映射 <code>:helpgrep&lt;space&gt;</code>。如果你想通过先按 <kbd>空格</kbd> 键来触发，只需要这样做：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> mapleader = <span class="string">&#x27; &#x27;</span></span><br><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;leader&gt;</span>h :<span class="keyword">helpgrep</span><span class="symbol">&lt;space&gt;</span></span><br></pre></td></tr></table></figure><p>另外，还有一个叫 <code>&lt;localleader&gt;</code> 的，可以把它理解为局部环境中的 <code>&lt;leader&gt;</code>，默认值依然为 <kbd>\</kbd>。当我们需要只对某一个条件下（比如，特定文件类型的插件）的缓冲区设置特别的 <code>&lt;leader&gt;</code> 键，那么我们就可以通过修改当前环境下的 <code>&lt;localleader&gt;</code> 来实现。</p><p><strong>注意</strong>：如果你打算设置 Leader 键，请确保在设置按键映射之前，先设置好 Leader 键。如果你先设置了含有 Leader 键的映射，然后又修改了 Leader 键，那么之前映射内的 Leader 键是不会因此而改变的。你可以通过执行 <code>:nmap &lt;leader&gt;</code> 来查看普通模式中已绑定给 Leader 键的所有映射。</p><p>请参阅 <code>:h mapleader</code> 与 <code>:h maploacalleader</code> 来获取更多帮助。</p><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="寄存器"><a href="#寄存器" class="headerlink" title="寄存器"></a>寄存器</h2><p>寄存器就是存储文本的地方。我们常用的「复制」操作就是把文本存储到寄存器，「 粘贴」 操作就是把文本从寄存器中读出来。顺便，在 Vim 中复制的快捷键是 <kbd>y</kbd>，粘贴的快捷键是 <kbd>p</kbd>。</p><p>Vim 为我们提供了如下的寄存器：</p><table><thead><tr><th>类型</th><th>标识</th><th>读写者</th><th>是否为只读</th><th>包含的字符来源</th></tr></thead><tbody><tr><td>Unnamed</td><td><code>&quot;</code></td><td>vim</td><td>否</td><td>最近一次的复制或删除操作 (<code>d</code>, <code>c</code>, <code>s</code>, <code>x</code>, <code>y</code>)</td></tr><tr><td>Numbered</td><td><code>0</code>至<code>9</code></td><td>vim</td><td>否</td><td>寄存器 <code>0</code>: 最近一次复制。寄存器 <code>1</code>: 最近一次删除。寄存器 <code>2</code>: 倒数第二次删除，以此类推。对于寄存器 <code>1</code> 至 <code>9</code>，他们其实是只读的最多包含 9 个元素的队列。这里的队列即为数据类型 <a href="https://en.wikipedia.org/wiki/Queue_(abstract_data_type)">queue</a></td></tr><tr><td>Small delete</td><td><code>-</code></td><td>vim</td><td>否</td><td>最近一次行内删除</td></tr><tr><td>Named</td><td><code>a</code>至<code>z</code>, <code>A</code>至<code>Z</code></td><td>用户</td><td>否</td><td>如果你通过复制操作存储文本至寄存器 <code>a</code>，那么 <code>a</code> 中的文本就会被完全覆盖。如果你存储至 <code>A</code>，那么会将文本添加给寄存器 <code>a</code>，不会覆盖之前已有的文本</td></tr><tr><td>Read-only</td><td><code>:</code>与<code>.</code>和<code>%</code></td><td>vim</td><td>是</td><td><code>:</code>: 最近一次使用的命令，<code>.</code>: 最近一次添加的文本，<code>%</code>: 当前的文件名</td></tr><tr><td>Alternate buffer</td><td><code>#</code></td><td>vim</td><td>否</td><td>大部分情况下，这个寄存器是当前窗口中，上一次访问的缓冲区。请参阅 <code>:h alternate-file</code> 来获取更多帮助</td></tr><tr><td>Expression</td><td><code>=</code></td><td>用户</td><td>否</td><td>复制 VimL 代码时，这个寄存器用于存储代码片段的执行结果。比如，在插入模式下复制 <code>&lt;c-r&gt;=5+5&lt;cr&gt;</code>，那么这个寄存器就会存入 10</td></tr><tr><td>Selection</td><td><code>+</code>和<code>*</code></td><td>vim</td><td>否</td><td><code>*</code> 和 <code>+</code> 是 <a href="#%E5%89%AA%E8%B4%B4%E6%9D%BF">剪贴板</a> 寄存器</td></tr><tr><td>Drop</td><td><code>~</code></td><td>vim</td><td>是</td><td>最后一次拖拽添加至 Vim 的文本（需要 “+dnd” 支持，暂时只支持 GTK GUI。请参阅 <code>:help dnd</code> 及 <code>:help quote~</code>）</td></tr><tr><td>Black hole</td><td><code>_</code></td><td>vim</td><td>否</td><td>一般称为黑洞寄存器。对于当前操作，如果你不希望在其他寄存器中保留文本，那就在命令前加上 <code>_</code>。比如，<code>&quot;_dd</code> 命令不会将文本放到寄存器 <code>&quot;</code>、<code>1</code>、<code>+</code> 或 <code>*</code> 中</td></tr><tr><td>Last search pattern</td><td><code>/</code></td><td>vim</td><td>否</td><td>最近一次通过 <code>/</code>、<code>?</code> 或 <code>:global</code> 等命令调用的匹配条件</td></tr></tbody></table><p>只要不是只读的寄存器，用户都有权限修改它的内容，比如：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">let</span> @/ = <span class="string">&#x27;register&#x27;</span></span><br></pre></td></tr></table></figure><p>这样，我们按 <kbd>n</kbd> 的时候就会跳转到单词”register” 出现的地方。</p><p>有些时候，你的操作可能已经修改了寄存器，而你没有察觉到。请参阅 <code>:h registers</code> 获取更多帮助。</p><p>上面提到过，复制的命令是 <kbd>y</kbd>，粘贴的命令是 <kbd>p</kbd> 或者 <kbd>P</kbd>。但请注意，Vim 会区分「字符选取」与「行选取」。请参阅 <code>:h linewise</code> 获取更多帮助。</p><p><strong>行选取</strong>：<br>命令 <code>yy</code> 或 <code>Y</code> 都是复制当前行。这时移动光标至其他位置，按下 <code>p</code> 就可以在光标下方粘贴复制的行，按下 <code>P</code> 就可以在光标上方粘贴至复制的行。</p><p><strong>字符选取</strong>：<br>命令 <code>0yw</code> 可以复制第一个单词。这时移动光标至其他位置，按下 <code>p</code> 就可以在当前行、光标后的位置粘贴单词，按下 <code>P</code> 就可以在当前行、光标前的位置粘贴单词。</p><p><strong>将文本存到指定的寄存器中</strong>：<br>命令 <code>&quot;aY</code> 可以将当前行复制，并存储到寄存器 <code>a</code> 中。这时移动光标至其他位置，通过命令 <code>&quot;AY</code> 就可以把这一行的内容扩展到寄存器 <code>a</code> 中，而之前存储的内容也不会丢失。</p><p>为了便于理解和记忆，建议大家现在就试一试上面提到的这些操作。操作过程中，你可以随时通过 <code>:reg</code> 来查看寄存器的变化。</p><p><strong>有趣的是</strong>：<br>在 Vim 中，<code>y</code> 是复制命令，源于单词 “yanking”。而在 Emacs 中，”yanking” 代表的是粘贴（或者说，重新插入刚才删掉的内容），而并不是复制。</p><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="范围"><a href="#范围" class="headerlink" title="范围"></a>范围</h2><p>范围 (Ranges) 其实很好理解，但很多 Vim 用户的理解不到位。</p><ul><li>很多命令都可以加一个数字，用于指明操作范围</li><li>范围可以是一个行号，用于指定某一行</li><li>范围也可以是一对通过 <code>,</code> 或 <code>;</code> 分割的行号</li><li>大部分命令，默认只作用于当前行</li><li>只有 <code>:write</code> 和 <code>:global</code> 是默认作用于所有行的</li></ul><p>范围的使用是十分直观的。以下为一些例子（其中，<code>:d</code> 为 <code>:delete</code> 的缩写）：</p><table><thead><tr><th>命令</th><th>操作的行</th></tr></thead><tbody><tr><td><code>:d</code></td><td>当前行</td></tr><tr><td><code>:.d</code></td><td>当前行</td></tr><tr><td><code>:1d</code></td><td>第一行</td></tr><tr><td><code>:$d</code></td><td>最后一行</td></tr><tr><td><code>:1,$d</code></td><td>所有行</td></tr><tr><td><code>:%d</code></td><td>所有行（这是 <code>1,$</code> 的语法糖）</td></tr><tr><td><code>:.,5d</code></td><td>当前行至第 5 行</td></tr><tr><td><code>:,5d</code></td><td>同样是当前行至第 5 行</td></tr><tr><td><code>:,+3d</code></td><td>当前行及接下来的 3 行</td></tr><tr><td><code>:1,+3d</code></td><td>第一行至当前行再加 3 行</td></tr><tr><td><code>:,-3d</code></td><td>当前行及向上的 3 行（Vim 会弹出提示信息，因为这是一个保留的范围）</td></tr><tr><td><code>:3,&#39;xdelete</code></td><td>第三行至<a href="#%E6%A0%87%E6%B3%A8">标注</a> 为 x 的那一行</td></tr><tr><td><code>:/^foo/,$delete</code></td><td>当前行以下，以字符 “foo” 开头的那一行至结尾</td></tr><tr><td><code>:/^foo/+1,$delete</code></td><td>当前行以下，以字符 “foo” 开头的那一行的下一行至结尾</td></tr></tbody></table><p>需要注意的是，<code>;</code> 也可以用于表示范围。区别在于，<code>a,b</code> 的 <code>b</code> 是以当前行作为参考的。而 <code>a;b</code> 的 <code>b</code> 是以 <code>a</code> 行作为参考的。举个例子，现在你的光标在第 5 行。这时 <code>:1,+1d</code> 会删除第 1 行至第 6 行，而 <code>:1;+1d</code> 会删除第 1 行和第 2 行。</p><p>如果你想设置多个寻找条件，只需要在条件前加上 <code>/</code>，比如：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:/foo//bar//quux/d</span><br></pre></td></tr></table></figure><p>这就会删除当前行之后的某一行。定位方式是，先在当前行之后寻找第一个包含 “foo” 字符的那一行，然后在找到的这一行之后寻找第一个包含 “bar” 字符的那一行，然后再在找到的这一行之后寻找第一个包含 “quux” 的那一行。删除的就是最后找到的这一行。</p><p>有时，Vim 会在命令前自动添加范围。举个例子，如果你先通过 <code>V</code> 命令进入行选取模式，选中一些行后按下 <code>:</code> 进入命令模式，这时候你会发现 Vim 自动添加了 <code>&#39;&lt;,&#39;&gt;</code> 范围。这表示，接下来的命令会使用之前选取的行号作为范围。但如果后续命令不支持范围，Vim 就会报错。为了避免这样的情况发生，有些人会设置这样的按键映射：<code>:vnoremap foo :&lt;c-u&gt;command</code>，组合键 <kbd>Ctrl + u</kbd> 可以清除当前命令行中的内容。</p><p>另一个例子是在普通模式中按下 <code>!!</code>，命令行中会出现 <code>:.!</code>。如果这时你如果输入一个外部命令，那么当前行的内容就会被这个外部命令的输出替换。你也可以通过命令 <code>:?^$?+1,/^$/-1!ls</code> 把当前段落的内容替换成外部命令 <code>ls</code> 的输出，原理是向前和向后各搜索一个空白行，删除这两个空白行之间的内容，并将外部命令 <code>ls</code> 的输出放到这两个空白行之间。</p><p>请参阅以下两个命令来获取更多帮助：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:h cmdline-ranges</span><br><span class="line">:h <span class="number">10.3</span></span><br></pre></td></tr></table></figure><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="标注"><a href="#标注" class="headerlink" title="标注"></a>标注</h2><p>你可以使用标注功能来标记一个位置，也就是记录文件某行的某个位置。</p><table><thead><tr><th>标注</th><th>设置者</th><th>使用</th></tr></thead><tbody><tr><td><code>a</code>-<code>z</code></td><td>用户</td><td>仅对当前的一个文件生效，也就意味着只可以在当前文件中跳转</td></tr><tr><td><code>A</code>-<code>Z</code></td><td>用户</td><td>全局标注，可以作用于不同文件。大写标注也称为「文件标注」。跳转时有可能会切换到另一个缓冲区</td></tr><tr><td><code>0</code>-<code>9</code></td><td>viminfo</td><td><code>0</code> 代表 viminfo 最后一次被写入的位置。实际使用中，就代表 Vim 进程最后一次结束的位置。<code>1</code> 代表 Vim 进程倒数第二次结束的位置，以此类推</td></tr></tbody></table><p>如果想跳转到指定的标注，你可以先按下 <code>&#39;</code> &#x2F; <code>g&#39;</code> 或者 <code>`</code> &#x2F; <code>g`</code> 然后按下标注名。</p><p>如果你想定义当前文件中的标注，可以先按下 <code>m</code> 再按下标注名。比如，按下 <code>mm</code> 就可以把当前位置标注为 <code>m</code>。在这之后，如果你的光标切换到了文件的其他位置，只需要通过 <code>&#39;m</code> 或者 <code>`m</code>即可回到刚才标注的行。区别在于，<code>&#39;m</code>会跳转回被标记行的第一个非空字符，而<code>`m</code>会跳转回被标记行的被标记列。根据 viminfo 的设置，你可以在退出 Vim 的时候保留小写字符标注。请参阅<code>:h viminfo-&#39;</code> 来获取更多帮助。</p><p>如果你想定义全局的标注，可以先按下 <code>m</code> 再按下大写英文字符。比如，按下 <code>mM</code> 就可以把当前文件的当前位置标注为 <code>M</code>。在这之后，就算你切换到其他的缓冲区，依然可以通过 <code>&#39;M</code> 或 <code>`M</code> 跳转回来。</p><p>关于跳转，还有以下的方式：</p><table><thead><tr><th>按键</th><th>跳转至</th></tr></thead><tbody><tr><td><code>&#39;[</code> 与 <code>`[</code></td><td>上一次修改或复制的第一行或第一个字符</td></tr><tr><td><code>&#39;]</code> 与 <code>`]</code></td><td>上一次修改或复制的最后一行或最后一个字符</td></tr><tr><td><code>&#39;&lt;</code> 与 <code>`&lt;</code></td><td>上一次在可视模式下选取的第一行或第一个字符</td></tr><tr><td><code>&#39;&gt;</code> 与 <code>`&gt;</code></td><td>上一次在可视模式下选取的最后一行或最后一个字符</td></tr><tr><td><code>&#39;&#39;</code> 与 <code>`&#39;</code></td><td>上一次跳转之前的光标位置</td></tr><tr><td><code>&#39;&quot;</code> 与 <code>`&quot;</code></td><td>上一次关闭当前缓冲区时的光标位置</td></tr><tr><td><code>&#39;^</code> 与 <code>`^</code></td><td>上一次插入字符后的光标位置</td></tr><tr><td><code>&#39;.</code> 与 <code>`.</code></td><td>上一次修改文本后的光标位置</td></tr><tr><td><code>&#39;(</code> 与 <code>`(</code></td><td>当前句子的开头</td></tr><tr><td><code>&#39;)</code> 与 <code>`)</code></td><td>当前句子的结尾</td></tr><tr><td><code>&#39;{</code> 与 <code>`{</code></td><td>当前段落的开头</td></tr><tr><td><code>&#39;}</code> 与 <code>`}</code></td><td>当前段落的结尾</td></tr></tbody></table><p>标注也可以搭配 <a href="#%E8%8C%83%E5%9B%B4">范围</a> 一起使用。前面提到过，如果你在可视模式下选取一些文本，然后按下 <code>:</code>，这时候你会发现命令行已经被填充了 <code>:&#39;&lt;,&#39;&gt;</code>。对照上面的表格，现在你应该明白了，这段代表的就是可视模式下选取的范围。</p><p>请使用 <code>:marks</code> 命令来显示所有的标注，参阅 <code>:h mark-motions</code> 来获取关于标注的更多帮助。</p><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="补全"><a href="#补全" class="headerlink" title="补全"></a>补全</h2><p>Vim 在插入模式中为我们提供了多种补全方案。如果有多个补全结果，Vim 会弹出一个菜单供你选择。</p><p>常见的补全有标签、项目中引入的模块或库中的方法名、文件名、字典及当前缓冲区的字段。</p><p>针对不同的补全方案，Vim 为我们提供了不同的按键映射。这些映射都是在<strong>插入模式中</strong>通过 <kbd>Ctrl</kbd> + <kbd>x</kbd> 来触发：</p><table><thead><tr><th>映射</th><th>类型</th><th>帮助文档</th></tr></thead><tbody><tr><td><code>&lt;c-x&gt;&lt;c-l&gt;</code></td><td>整行</td><td><code>:h i^x^l</code></td></tr><tr><td><code>&lt;c-x&gt;&lt;c-n&gt;</code></td><td>当前缓冲区中的关键字</td><td><code>:h i^x^n</code></td></tr><tr><td><code>&lt;c-x&gt;&lt;c-k&gt;</code></td><td>字典（请参阅 <code>:h &#39;dictionary&#39;</code>）中的关键字</td><td><code>:h i^x^k</code></td></tr><tr><td><code>&lt;c-x&gt;&lt;c-t&gt;</code></td><td>同义词字典（请参阅 <code>:h &#39;thesaurus&#39;</code>）中的关键字</td><td><code>:h i^x^t</code></td></tr><tr><td><code>&lt;c-x&gt;&lt;c-i&gt;</code></td><td>当前文件以及包含的文件中的关键字</td><td><code>:h i^x^i</code></td></tr><tr><td><code>&lt;c-x&gt;&lt;c-]&gt;</code></td><td>标签</td><td><code>:h i^x^]</code></td></tr><tr><td><code>&lt;c-x&gt;&lt;c-f&gt;</code></td><td>文件名</td><td><code>:h i^x^f</code></td></tr><tr><td><code>&lt;c-x&gt;&lt;c-d&gt;</code></td><td>定义或宏定义</td><td><code>:h i^x^d</code></td></tr><tr><td><code>&lt;c-x&gt;&lt;c-v&gt;</code></td><td>Vim 命令</td><td><code>:h i^x^v</code></td></tr><tr><td><code>&lt;c-x&gt;&lt;c-u&gt;</code></td><td>用户自定义补全（通过 <code>&#39;completefunc&#39;</code> 定义）</td><td><code>:h i^x^u</code></td></tr><tr><td><code>&lt;c-x&gt;&lt;c-o&gt;</code></td><td>Omni Completion（通过 <code>&#39;omnifunc&#39;</code> 定义）</td><td><code>:h i^x^o</code></td></tr><tr><td><code>&lt;c-x&gt;s</code></td><td>拼写建议</td><td><code>:h i^Xs</code></td></tr></tbody></table><p>尽管用户自定义补全与 Omni Completion 是不同的，但他们做的事情基本一致。共同点在于，他们都是一个监听当前光标位置的函数，返回值为一系列的补全建议。用户自定义补全是由用户定义的，基于用户的个人用途，因此你可以根据自己的喜好和需求随意定制。而 Omni Completion 是针对文件类型的补全，比如在 C 语言中补全一个结构体（struct）的成员（members），或者补全一个类的方法，因而它通常都是由文件类型插件设置和调用的。</p><p>如果你设置了 <code>&#39;complete&#39;</code> 选项，那么你就可以在一次操作中采用多种补全方案。这个选项默认包含了多种可能性，因此请按照自己的需求来配置。你可以通过 <code>&lt;c-n&gt;</code> 来调用下一个补全建议，或通过 <code>&lt;c-p&gt;</code> 来调用上一个补全建议。当然，这两个映射同样可以直接调用补全函数。请参阅 <code>:h i^n</code> 与 <code>:h &#39;complete&#39;</code> 来获得更多帮助。</p><p>如果你想配置弹出菜单的行为，请一定要看一看 <code>:h &#39;completeopt&#39;</code> 这篇帮助文档。默认的配置已经不错了，但我个人（原作者）更倾向于把 “noselect” 加上。</p><p>请参阅以下文档获取更多帮助：</p><figure class="highlight vim"><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><span class="line">:h ins-completion</span><br><span class="line">:h popupmenu-<span class="built_in">keys</span></span><br><span class="line">:h <span class="keyword">new</span>-omni-completion</span><br></pre></td></tr></table></figure><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="动作，操作符，文本对象"><a href="#动作，操作符，文本对象" class="headerlink" title="动作，操作符，文本对象"></a>动作，操作符，文本对象</h2><p><strong>动作</strong>也就是指移动光标的操作，你肯定很熟悉 <code>h</code>、<code>j</code>、<code>k</code> 和 <code>l</code>，以及 <code>w</code> 和 <code>b</code>。但其实，<code>/</code> 也是一个动作。他们都可以搭配数字使用，比如 <code>2?the&lt;cr&gt;</code> 可以将光标移动到倒数第二个 “the” 出现的位置。</p><p>以下会列出一些常用的动作。你也可以通过 <code>:h navigation</code> 来获取更多的帮助。</p><p><strong>操作符</strong>是对某个区域文本执行的操作。比如，<code>d</code>、<code>~</code>、<code>gU</code> 和 <code>&gt;</code> 都是操作符。这些操作符既可以在普通模式下使用，也可以在可视模式下使用。在普通模式中，顺序是先按操作符，再按动作指令，比如 <code>&gt;j</code>。在可视模式中，选中区域后直接按操作符就可以，比如 <code>Vjd</code>。</p><p>与动作一样，操作符也可以搭配数字使用，比如 <code>2gUw</code> 可以将当前单词以及下一个单词转成大写。由于动作和操作符都可以搭配数字使用，因此 <code>2gU2w</code> 与执行两次 <code>gU2w</code> 效果是相同的。</p><p>请参阅 <code>:h operator</code> 来查看所有的操作符。你也可以通过 <code>:set tildeop</code> 命令把 <code>~</code> 也变成一个操作符</p><p>值得注意的是，动作是单向的，而<strong>文本对象</strong>是双向的。文本对象不仅作用于符号（比如括号、中括号和大括号等）标记的范围内，也作用于整个单词、整个句子等其他情况。</p><p>文本对象不能用于普通模式中移动光标的操作，因为光标还没有智能到可以向两个方向同时跳转。但这个功能可以在可视模式中实现，因为在对象的一端选中的情况下，光标只需要跳转到另一端就可以了。</p><p>文本对象操作一般用 <code>i</code> 或 <code>a</code> 加上对象标识符操作，其中 <code>i</code> 表示在对象内（英文 inner）操作，<code>a</code> 表示对整个对象（英文 around）操作，这时开头和结尾的空格都会被考虑进来。举个例子，<code>diw</code> 可以删除当前单词，<code>ci(</code> 可以改变括号中的内容。</p><p>文本对象同样可以与数字搭配使用。比如，像 <code>((( )))</code> 这样的文本，假如光标位于最内层的括号上或最内层的括号内，那么 <code>d2a(</code> 将会删除从最内层开始的两对括号，以及他们之间的所有内容。其实，<code>d2a(</code> 这个操作等同于 <code>2da(</code>。在 Vim 的命令中，如果有两处都可以接收数字作为参数，那么最终结果就等同于两个数字相乘。在这里，<code>d</code> 与 <code>a(</code> 都是可以接收参数的，一个参数是 1，另一个是 2，我们可以把它们相乘然后放到最前面。</p><p>请参阅 <code>:h text-objects</code> 来获取更多关于文本对象的帮助。</p><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="自动命令"><a href="#自动命令" class="headerlink" title="自动命令"></a>自动命令</h2><p>在特定的情况下，Vim 会传出事件。如果你想针对这些事件执行回调方法，那么就需要用到自动命令这个功能。</p><p>如果没有了自动命令，那你基本上是用不了 Vim 的。自动命令一直都在执行，只是很多时候你没有注意到。不信的话，可以执行命令 <code>:au</code> ，不要被结果吓到，这些是当前有效的所有自动命令。</p><p>请使用 <code>:h {event}</code> 来查看 Vim 中所有事件的列表，你也可以参考 <code>:h autocmd-events-abc</code> 来获取关于事件的更多帮助。</p><p>一个很常用的例子，就是针对文件类型执行某些设置：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">autocmd</span> FileType <span class="keyword">ruby</span> <span class="keyword">setlocal</span> <span class="built_in">shiftwidth</span>=<span class="number">2</span> softtabstop=<span class="number">2</span> comments-=:#</span><br></pre></td></tr></table></figure><p>但是缓冲区是如何知道当前的文件中包含 Ruby 代码呢？这其实是另一个自动命令检测的到的，然后把文件类型设置成为 Ruby，这样就触发了上面的 <code>FileType</code> 事件。</p><p>在配置 vimrc 的时候，一般第一行加进去的就是 <code>filetype on</code>。这就意味着，Vim 启动时会读取 <code>filetype.vim</code> 文件，然后根据文件类型来触发相应的自动命令。</p><p>如果你勇于尝试，可以查看下 <code>:e $VIMRUNTIME/filetype.vim</code>，然后在输出中搜索 “Ruby”。这样，你就会发现其实 Vim 只是通过文件扩展名 <code>.rb</code> 判断某个文件是不是 Ruby 的。</p><p><strong>注意</strong>：对于相同事件，如果有多个自动命令，那么自动命令会按照定义时的顺序执行。通过 <code>:au</code> 就可以查看它们的执行顺序。</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">au</span> BufNewFile,BufRead *.rb,*.rbw <span class="keyword">setf</span> <span class="keyword">ruby</span></span><br></pre></td></tr></table></figure><p><code>BufNewFile</code> 与 <code>BufRead</code> 事件是被写在 Vim 源文件中的。因此，每当你通过 <code>:e</code> 或者类似的命令打开文件，这两个事件都会触发。然后，就是读取 <code>filetype.vim</code> 文件来判断打开的文件类型。</p><p>简单来说，事件和自动命令在 Vim 中的应用十分广泛。而且，Vim 为我们留出了一些易用的接口，方便用户配置适合自己的事件驱动回调。</p><p>请参阅 <code>:h autocommand</code> 来获取更多帮助</p><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="变更历史，跳转历史"><a href="#变更历史，跳转历史" class="headerlink" title="变更历史，跳转历史"></a>变更历史，跳转历史</h2><p>在 Vim 中，用户最近 100 次的文字改动都会被保存在<strong>变更历史</strong>中。如果在同一行有多个小改动，那么 Vim 会把它们合并成一个。尽管内容改动会合并，但作用的位置还是会只记录下最后一次改动的位置。</p><p>在你移动光标或跳转的时候，每一次的移动或跳转前的位置会被记录到<strong>跳转历史</strong>中。类似地，跳转历史也可以最多保存 100 条记录。对于每个窗口，跳转记录是独立的。但当你分离窗口时（比如使用 <code>:split</code> 命令），跳转历史会被复制过去。</p><p>Vim 中的跳转命令，包括 <code>&#39;</code>、<code>`</code>、<code>G</code>、<code>/</code>、<code>?</code>、<code>n</code>、<code>N</code>、<code>%</code>、<code>(</code>、<code>)</code>、<code>[[</code>、<code>]]</code>、<code>{</code>、<code>}</code>、<code>:s</code>、<code>:tag</code>、<code>L</code>、<code>M</code>、<code>H</code> 以及开始编辑一个新文件的命令。</p><table><thead><tr><th>列表</th><th>显示所有条目</th><th>跳转到上一个位置</th><th>跳转到下一个位置</th></tr></thead><tbody><tr><td>跳转历史</td><td><code>:jumps</code></td><td><code>[count]&lt;c-o&gt;</code></td><td><code>[count]&lt;c-i&gt;</code></td></tr><tr><td>变更历史</td><td><code>:changes</code></td><td><code>[count]g;</code></td><td><code>[count]g,</code></td></tr></tbody></table><p>如果你执行第二列的命令显示所有条目，这时 Vim 会用 <code>&gt;</code> 标记来为你指示当前位置。通常这个标记位于 1 的下方，也就代表最后一次的位置。</p><p>如果你希望关闭 Vim 之后还保留这些条目，请参阅 <code>:h viminfo-&#39;</code> 来获取更多帮助。</p><p><strong>注意</strong>：上面提到过，最后一次跳转前的位置也会记录在<a href="#%E6%A0%87%E6%B3%A8">标注</a>中，也可以通过连按 <kbd>``</kbd> 或 <kbd>&#39;&#39;</kbd> 跳转到那个位置</p><p>请参阅以下两个命令来获取更多帮助：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:h changelist</span><br><span class="line">:h jumplist</span><br></pre></td></tr></table></figure><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="内容变更历史记录"><a href="#内容变更历史记录" class="headerlink" title="内容变更历史记录"></a>内容变更历史记录</h2><p>Vim 会记录文本改变之前的状态。因此，你可以使用「撤销」操作 <kbd>u</kbd> 来取消更改，也可以通过「重做」操作 <kbd>Ctrl + r</kbd> 来恢复更改。</p><p>值得注意的是，Vim 采用 <a href="https://en.wikipedia.org/wiki/Tree_(data_structure)">tree</a> 数据结构来存储内容变更的历史记录，而不是采用 <a href="https://en.wikipedia.org/wiki/Queue_(abstract_data_type)">queue</a>。你的每次改动都会成为存储为树的节点。而且，除了第一次改动（根节点），之后的每次改动都可以找到一个对应的父节点。每一个节点都会记录改动的内容和时间。其中，「分支」代表从任一节点到根节点的路径。当你进行了撤销操作，然后又输入了新的内容，这时候就相当于创建了分支。这个原理和 git 中的 branch（分支）十分类似。</p><p>考虑以下这一系列按键操作：</p><figure class="highlight vim"><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><span class="line">ifoo<span class="symbol">&lt;esc&gt;</span></span><br><span class="line">obar<span class="symbol">&lt;esc&gt;</span></span><br><span class="line">obaz<span class="symbol">&lt;esc&gt;</span></span><br><span class="line"><span class="keyword">u</span></span><br><span class="line">oquux<span class="symbol">&lt;exc&gt;</span></span><br></pre></td></tr></table></figure><p>那么现在，Vim 中会显示三行文本，分别是 “foo”、”bar” 和 “quux”。这时候，存储的树形结构如下：</p><pre><code>     foo(1)       /    bar(2)   /      \baz(3)   quux(4)</code></pre><p>这个树形结构共包含四次改动，括号中的数字就代表时间顺序。</p><p>现在，我们有两种方式遍历这个树结构。一种叫「按分支遍历」，一种叫「按时间遍历」。</p><p>撤销 <kbd>u</kbd> 与重做 <kbd>Ctrl + r</kbd> 操作是按分支遍历。对于上面的例子，现在我们有三行字符。这时候按 <kbd>u</kbd> 会回退到 “bar” 节点，如果再按一次 <kbd>u</kbd> 则会回退到 “foo” 节点。这时，如果我们按下 <kbd>Ctrl + r</kbd> 就会前进至 “bar” 节点，再按一次就回前进至 “quux” 节点。在这种方式下，我们无法访问到兄弟节点（即 “baz” 节点）。</p><p>与之对应的是按时间遍历，对应的按键是 <code>g-</code> 和 <code>g+</code>。对于上面的例子，按下 <code>g-</code> 会首先回退到 “baz” 节点。再次按下 <code>g-</code> 会回退到 “bar” 节点。</p><table><thead><tr><th>命令&#x2F;按键</th><th>执行效果</th></tr></thead><tbody><tr><td><code>[count]u</code> 或 <code>:undo [count]</code></td><td>回退到 <code>[count]</code> 次改动之前</td></tr><tr><td><code>[count]&lt;c-r&gt;</code> 或 <code>:redo [count]</code></td><td>重做 <code>[count]</code> 次改动</td></tr><tr><td><code>U</code></td><td>回退至最新的改动</td></tr><tr><td><code>[count]g-</code> 或 <code>:earlier [count]?</code></td><td>根据时间回退到 <code>[count]</code> 次改动之前。”?” 为 “s”、”m”、”h”、”d” 或 “f”之一。例如，<code>:earlier 2d</code> 会回退到两天之前。<code>:earlier 1f</code> 则会回退到最近一次文件保存时的内容</td></tr><tr><td><code>[count]g+</code> 或 <code>:later [count]?</code></td><td>类似 <code>g-</code>，但方向相反</td></tr></tbody></table><p>内容变更记录会储存在内存中，当 Vim 退出时就会清空。如果需要持久化存储内容变更记录，请参阅<a href="#%E5%A4%87%E4%BB%BD%E6%96%87%E4%BB%B6%E4%BA%A4%E6%8D%A2%E6%96%87%E4%BB%B6%E6%92%A4%E9%94%80%E6%96%87%E4%BB%B6%E4%BB%A5%E5%8F%8Aviminfo%E6%96%87%E4%BB%B6%E7%9A%84%E5%A4%84%E7%90%86">备份文件，交换文件，撤销文件以及 viminfo 文件的处理</a>章节的内容。</p><p>如果你觉得这一部分的内容难以理解，请参阅 <a href="https://github.com/mbbill/undotree">undotree</a>，这是一个可视化管理内容变更历史记录的插件。类似的还有 <a href="https://github.com/simnalamburt/vim-mundo">vim-mundo</a>。</p><p>请参阅以下链接获取更多帮助：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:h <span class="keyword">undo</span>.txt</span><br><span class="line">:h usr_32</span><br></pre></td></tr></table></figure><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="全局位置信息表，局部位置信息表"><a href="#全局位置信息表，局部位置信息表" class="headerlink" title="全局位置信息表，局部位置信息表"></a>全局位置信息表，局部位置信息表</h2><p>在某一个动作返回一系列「位置」的时候，我们可以利用「全局位置信息表」和「局部位置信息表」来存储这些位置信息，方便以后跳转回对应的位置。每一个存储的位置包括文件名、行号和列号。</p><p>比如，编译代码是出现错误，这时候我们就可以把错误的位置直接显示在全局位置信息表，或者通过外部抓取工具使位置显示在局部位置信息表中。</p><p>尽管我们也可以把这些信息显示到一个空格缓冲区中，但用这两个信息表显示的好处在于接口调用很方便，而且也便于浏览输出。</p><p>Vim 中，全局位置信息表只能有一个，但每一个窗口都可以有自己的局部位置信息表。这两个信息表的外观看上去很类似，但在操作上会稍有不同。</p><p>以下为两者的操作比较：</p><table><thead><tr><th>动作</th><th>全局位置信息表</th><th>局部位置信息表</th></tr></thead><tbody><tr><td>打开窗口</td><td><code>:copen</code></td><td><code>:lopen</code></td></tr><tr><td>关闭窗口</td><td><code>:cclose</code></td><td><code>:lclose</code></td></tr><tr><td>下一个条目</td><td><code>:cnext</code></td><td><code>:lnext</code></td></tr><tr><td>上一个条目</td><td><code>:cprevious</code></td><td><code>:lprevious</code></td></tr><tr><td>第一个条目</td><td><code>:cfirst</code></td><td><code>:lfirst</code></td></tr><tr><td>最后一个条目</td><td><code>:clast</code></td><td><code>:llast</code></td></tr></tbody></table><p>请参阅 <code>:h :cc</code> 以及底下的内容，来获取更多命令的帮助。</p><p><strong>应用实例</strong>：<br>如果我们想用 <code>grep</code> 递归地在当前文件夹中寻找某个关键词，然后把输出结果放到全局位置信息表中，只需要这样：</p><figure class="highlight vim"><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><span class="line">:<span class="keyword">let</span> &amp;grepprg = <span class="string">&#x27;grep -Rn $* .&#x27;</span></span><br><span class="line">:grep! foo</span><br><span class="line">&lt;<span class="keyword">grep</span> output - hit enter&gt;</span><br><span class="line">:<span class="keyword">copen</span></span><br></pre></td></tr></table></figure><p>执行了上面的代码，你就能看到所有包含字符串 “foo” 的文件名以及匹配到的相关字段都会显示在全局位置信息表中。</p><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="宏"><a href="#宏" class="headerlink" title="宏"></a>宏</h2><p>你可以在 Vim 中录制一系列按键，并把他们存储到<a href="#%E5%AF%84%E5%AD%98%E5%99%A8">寄存器</a>中。对于一些需要临时使用多次的一系列操作，把它们作为宏保存起来会显著地提升效率。对于一些复杂的操作，建议使用 Vim 脚本来实现。</p><ul><li>首先，按下 <kbd>q</kbd>，然后按下你想要保存的寄存器，任何小写字母都可以。比如我们来把它保存到 <code>q</code> 这个寄存器中。按下 <code>qq</code>，你会发现命令行里已经显示了 “recording @q”。</li><li>如果你已经录制完成，那么只需要再按一次 <kbd>q</kbd> 就可以结束录制。</li><li>如果你想调用刚才录制的宏，只需要 <code>[count]@q</code></li><li>如果你想调用上一次使用的宏，只需要 <code>[count]@@</code></li></ul><p><strong>实例 1</strong>：</p><p>一个插入字符串 “abc” 后换行的宏，重复调用十次：</p><figure class="highlight vim"><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><span class="line">qq</span><br><span class="line"><span class="keyword">iabc</span><span class="symbol">&lt;cr&gt;</span><span class="symbol">&lt;esc&gt;</span></span><br><span class="line">q</span><br><span class="line"><span class="number">10</span>@q</span><br></pre></td></tr></table></figure><p>（对于上面这个功能，你同样可以通过如下的按键： <kbd>o</kbd><kbd>a</kbd><kbd>b</kbd><kbd>c</kbd> 然后 <kbd>ESC</kbd> 然后 <kbd>1</kbd><kbd>0</kbd><kbd>.</kbd> 来实现）。</p><p><strong>实例 2</strong>：</p><p>一个在每行前都加上行号的宏。从第一行开始，行号为 1，后面依次递增。我们可以通过 <kbd>Ctrl</kbd> + <kbd>a</kbd> 来实现递增的行号，在定义宏的时候，它会显示成 <code>^A</code>。</p><figure class="highlight vim"><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><span class="line">qq</span><br><span class="line"><span class="number">0</span>yf jP0^A</span><br><span class="line">q</span><br><span class="line"><span class="number">1000</span> @q</span><br></pre></td></tr></table></figure><p>这里能实现功能，是因为我们假定了文件最多只有 1000 行。但更好的方式是使用「递归」宏，它会一直执行，知道不能执行为止：</p><figure class="highlight vim"><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><span class="line">qq</span><br><span class="line"><span class="number">0</span>yf jP0^A@q</span><br><span class="line">q</span><br><span class="line">@q</span><br></pre></td></tr></table></figure><p>（对于上面这个插入行号的功能，如果你不愿意使用宏，同样可以通过这段按键操作来实现：<code>:%s/^/\=line(&#39;.&#39;) . &#39;. &#39;</code>）。</p><p>这里向大家展示了如何不用宏来达到相应的效果，但要注意，这些不用宏的实现方式只适用于这些简单的示例。对于一些比较复杂的自动化操作，你确实应该考虑使用宏。</p><p>请参阅以下文档获取更多帮助：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:h recording</span><br><span class="line">:h <span class="string">&#x27;lazyredraw&#x27;</span></span><br></pre></td></tr></table></figure><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="颜色主题"><a href="#颜色主题" class="headerlink" title="颜色主题"></a>颜色主题</h2><p>颜色主题可以把你的 Vim 变得更漂亮。Vim 是由多个组件构成的，我们可以给每一个组件都设置不同的文字颜色、背景颜色以及文字加粗等等。比如，我们可以通过这个命令来设置背景颜色：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">highlight</span> Normal ctermbg=<span class="number">1</span> guibg=<span class="keyword">red</span></span><br></pre></td></tr></table></figure><p>执行后你会发现，现在背景颜色变成红色了。请参阅 <code>:h :highlight</code> 来获取更多帮助。</p><p>其实，颜色主题就是一系列的 <code>:highlight</code> 命令的集合。</p><p>事实上，大部分颜色主题都包含两套配置。一套适用于例如 xterm 和 iTerm 这样的终端环境（使用前缀 <code>cterm</code>），另一套适用于例如 gvim 和 MacVim 的图形界面环境（使用前缀 <code>gui</code>）。对于上面的例子，<code>ctermbg</code> 就是针对终端环境的，而 <code>guibg</code> 就是针对图形界面环境的。</p><p>如果你下载了一个颜色主题，并且在终端环境中打开了 Vim，然后发现显示的颜色与主题截图中差别很大，那很可能是配置文件只设置了图形界面环境的颜色。反之同理，如果你使用的是图形界面环境，发现显示颜色有问题，那就很可能是配置文件只设置了终端环境的颜色。</p><p>第二种情况（图形界面环境的显示问题）其实不难解决。如果你使用的是 Neovim 或者 Vim 7.4.1830 的后续版本，可以通过打开<a href="https://zh.wikipedia.org/wiki/%E7%9C%9F%E5%BD%A9%E8%89%B2">真彩色</a>设置来解决显示问题。这就可以让终端环境的 Vim 使用 GUI 的颜色定义，但首先，你要确认一下你的终端环境和环境内的组件（比如 tmux）是否都支持真彩色。可以看一下<a href="https://gist.github.com/XVilka/8346728">这篇文档</a>，描述的十分详细。</p><p>请参阅以下文档或链接来获取更多帮助：</p><ul><li><code>:h &#39;termguicolors&#39;</code></li><li><a href="#%E4%B8%BB%E9%A2%98%E5%88%97%E8%A1%A8">主题列表</a></li><li><a href="#%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%BB%E9%A2%98%E4%B8%AD%E7%9A%84%E9%A2%9C%E8%89%B2">自定义主题中的颜色</a></li></ul><p>返回主目录 <a href="#%E5%9F%BA%E7%A1%80">:arrow_heading_up:</a></p><h2 id="折叠"><a href="#折叠" class="headerlink" title="折叠"></a>折叠</h2><p>每一部分文字（或者代码）都会有特定的结构。对于存在结构的文字和代码，也就意味着它们可以按照一定的逻辑分割成不同区域。Vim 中的折叠功能，就是按照特定的逻辑把文字和代码折叠成一行，并显示一些简短的描述。折叠功能涉及到很多操作，而且折叠功能可以嵌套使用。</p><p>在 Vim 中，有以下 6 中折叠类型：</p><table><thead><tr><th>折叠方式</th><th>概述</th></tr></thead><tbody><tr><td>diff</td><td>在「比较窗口」中折叠未改变的文本</td></tr><tr><td>expr</td><td>使用 <code>&#39;foldexpr&#39;</code> 来创建新的折叠逻辑</td></tr><tr><td>indent</td><td>基于缩进折叠</td></tr><tr><td>manual</td><td>使用 <code>zf</code>、<code>zF</code> 或 <code>:fold</code> 来自定义折叠</td></tr><tr><td>marker</td><td>根据特定的文本标记折叠（通常用于代码注释）</td></tr><tr><td>syntax</td><td>根据语法折叠，比如折叠 <code>if</code> 代码块</td></tr></tbody></table><p><strong>注意</strong>：折叠功能可能会显著地影响性能。如果你在使用折叠功能的时候出现了打字卡顿之类的问题，请考虑使用 <a href="https://github.com/Konfekt/FastFold">FastFold 插件</a>。这个插件可以让 Vim 按需更新折叠内容，而不是一直调用。</p><p>请参阅以下文档获取更多帮助：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:h usr_28</span><br><span class="line">:h folds</span><br></pre></td></tr></table></figure><h2 id="会话"><a href="#会话" class="headerlink" title="会话"></a>会话</h2><p>如果你保存了当前的「视图」（请参阅 <code>:h :mkview</code>），那么当前窗口、配置和按键映射都会被保存下来（请参阅 <code>:h :loadview</code>）。</p><p>「会话」就是存储所有窗口的相关设置，以及全局设置。简单来说，就是给当前的 Vim 运行实例拍个照，然后把相关信息存储到会话文件中。存储之后的改动就不会在会话文件中显示，你只需要在改动后更新一下会话文件就可以了。</p><p>你可以把当前工作的「项目」存储起来，然后可以在不同的「项目」之间切换。</p><p>现在就来试试吧。打开几个窗口和标签，然后执行 <code>:mksession Foo.vim</code>。如果你没有指定文件名，那就会默认保存为 <code>Session.vim</code>。这个文件会保存在当前的目录下，你可以通过 <code>:pwd</code> 来显示当前路径。重启 Vim 之后，你只需要执行 <code>:source Foo.vim</code>，就可以恢复刚才的会话了。所有的缓冲区、窗口布局、按键映射以及工作路径都会恢复到保存时的状态。</p><p>其实 Vim 的会话文件就只是 Vim 命令的集合。你可以通过命令 <code>:vs Foo.vim</code> 来看看会话文件中究竟有什么。</p><p>你可以决定 Vim 会话中究竟要保存哪些配置，只需要设置一下 <code>&#39;sessionoptions&#39;</code> 就可以了。</p><p>为了方便开发，Vim 把最后一次调用或写入的会话赋值给了一个内部变量 <code>v:this_session</code>。</p><p>请参阅以下文档来获取更多帮助：</p><figure class="highlight vim"><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><span class="line">:h Session</span><br><span class="line">:h <span class="string">&#x27;sessionoptions&#x27;</span></span><br><span class="line">:h <span class="variable">v:this_session</span></span><br></pre></td></tr></table></figure><h2 id="局部化"><a href="#局部化" class="headerlink" title="局部化"></a>局部化</h2><p>以上提到的很多概念，都有一个局部化（非全局）的版本：</p><table><thead><tr><th>全局</th><th>局部</th><th>作用域</th><th>帮助文档</th></tr></thead><tbody><tr><td><code>:set</code></td><td><code>:setlocal</code></td><td>缓冲区或窗口</td><td><code>:h local-options</code></td></tr><tr><td><code>:map</code></td><td><code>:map &lt;buffer&gt;</code></td><td>缓冲区</td><td><code>:h :map-local</code></td></tr><tr><td><code>:autocmd</code></td><td><code>:autocmd * &lt;buffer&gt;</code></td><td>缓冲区</td><td><code>:h autocmd-buflocal</code></td></tr><tr><td><code>:cd</code></td><td><code>:lcd</code></td><td>窗口</td><td><code>:h :lcd</code></td></tr><tr><td><code>:&lt;leader&gt;</code></td><td><code>:&lt;localleader&gt;</code></td><td>缓冲区</td><td><code>:h maploacalleader</code></td></tr></tbody></table><p>变量也有不同的作用域，详细内容请参考 <a href="http://vimdoc.sourceforge.net/htmldoc/usr_41.html">Vim scripting 的文档</a>。</p><h1 id="用法"><a href="#用法" class="headerlink" title="用法"></a>用法</h1><h2 id="获取离线帮助"><a href="#获取离线帮助" class="headerlink" title="获取离线帮助"></a>获取离线帮助</h2><p>Vim 自带了一套很完善的帮助文档，它们是一个个有固定排版格式的文本文件，通过标签可以访问这些文件的特定位置。</p><p>在开始之前先读一下这个章节：<code>:help :help</code>。执行这个命令以后会在新窗口打开 <code>$VIMRUNTIME/doc/helphelp.txt</code> 文件并跳转到这个文件中 <code>:help</code> 标签的位置。</p><p>一些关于帮助主题的简单规则：</p><ul><li>用单引号把文本包起来表示选项，如：<code>:h &#39;textwidth&#39;</code></li><li>以小括号结尾表示 VimL 函数，如：<code>:h reverse()</code></li><li>以英文冒号开头表示命令，如：<code>:h :echo</code></li></ul><p>使用快捷键 <code>&lt;c-d&gt;</code> （这是 <kbd>ctrl</kbd>+<kbd>d</kbd>）来列出所有包含你当前输入的内容的帮助主题。如：<code>:h tab&lt;c-d&gt;</code> 会列出所有包含 <code>tab</code> 主题，从 <code>softtabstop</code> 到 <code>setting-guitablabel</code> （译者注：根据安装的插件不同列出的选项也会不同）。</p><p>你想查看所有的 VimL 方法吗？很简单，只要输入：<code>:h ()&lt;c-d&gt;</code> 就可以了。你想查看所有与窗口相关的函数吗？输入 <code>:h win*()&lt;c-d&gt;</code>。</p><p>相信你很快就能掌握这些技巧，但是在刚开始的时候，你可能对于该通过什么进行查找一点线索都没有。这时你可以想象一些与要查找的内容相关的关键字，再让 <code>:helpgrep</code> 来帮忙。</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">helpgrep</span> backwards</span><br></pre></td></tr></table></figure><p>上面的命令会在所有的帮助文件中搜索“backwards”，然后跳转到第一个匹配的位置。所有的匹配位置都会被添加到全局位置信息表，用 <code>:cp / :cn</code> 可以在匹配位置之间进行切换。或者用 <code>:copen</code> 命令来打开全局位置信息表，将光标定位到你想要的位置，再按 回车就可以跳转到该匹配项。详细说明请参考 <code>:h quickfix</code>。</p><h2 id="获取离线帮助（补充）"><a href="#获取离线帮助（补充）" class="headerlink" title="获取离线帮助（补充）"></a>获取离线帮助（补充）</h2><p>这个列表最初发表在 <a href="https://groups.google.com/forum/#!forum/vim_dev">vim_dev</a>，由 @chrisbra 编辑的，他是 Vim 开发人员中最活跃的一个。</p><p>经过一些微小的改动后，重新发布到了这里。</p><hr><p>如果你知道你想要找什么，使用帮助系统的搜索会更简单一些，因为搜索出的主题都带有固定的格式。</p><p>而且帮助系统中的主题包含了你当前使用的 Vim 版本的所特有特性，而网上那些已经过时或者是早期发布的话题是不会包含这些的。</p><p>因此学习使用帮助系统以及它所用的语言是很有必要的。这里是一些例子（不一定全，我有可能忘了一些什么）。</p><p>（译者注：下面列表中提及的都是如何指定搜索主题以便快速准确的找到你想要的帮助）</p><ol><li><p>选项要用单引号引起来。用 <code>:h &#39;list&#39;</code> 来查看列表选项帮助。只有你明确的知道你要找这么一个选项的时候才可以这么做，不然的话你可以用 <code>:h options.txt</code> 来打开所有选项的帮助页面，再用正则表达式进行搜索，如：<code>/width</code>。某些选项有它们自己的命名空间，如：<code>:h cpo-a</code>，<code>:h cpo-A</code>， <code>:h cpo-b</code> 等等。</p></li><li><p>普通模式的命令不能用冒号作为前缀。使用 <code>:h gt</code> 来转到“gt”命令的帮助页面。</p></li><li><p>正则表达式以“&#x2F;”开头，所以 <code>:h /\+</code> 会带你到正则表达式中量词“+”的帮助页面。</p></li><li><p>组合键经常以一个字母开头表示它们可以在哪些模式中使用。如：<code>:h i_CTRL-X</code> 会带你到插入模式下的 CTRL-X 命令的用法帮助页面，这是一个自动完成类的组合键。需要注意的是某些键是有固定写法的，如 Control 键写成 CTRL。还有，查找普通模式下的组合键帮助时，可以省略开头的字母“n”，如：<code>:h CTRL-A</code>。而 <code>:h c_CTRL-A</code>（译者注：原文为 <code>:h c_CRTL-R</code>，感觉改为 A 更符合上下文语境）会解释 CTRL-A 在命令模式下输入命令时的作用；<code>:h v_CTRL-A</code> 说的是在可见模式下把光标所在处的数字加 1；<code>:h g_CTRL-A</code> 则说的是 g 命令（你需要先按 “g” 的命令）。这里的 “g” 代表一个普通的命令，这个命令总是与其它的按键组合使用才生效，与 “z” 开始的命令相似。</p></li><li><p>寄存器是以 “quote” 开头的。如：<code>:h quote:</code> （译者注：原文为<code>:h quote</code>，感觉作者想以”:”来举例）来查看关于”:”寄存器的说明。</p></li><li><p>关于 Vim 脚本（VimL）的帮助都在 <code>:h eval.txt</code> 里。而某些方面的语言可以使用 <code>:h expr-X</code> 获取帮助，其中的 ‘X’ 是一个特定的字符，如：<code>:h expr-!</code> 会跳转到描述 VimL 中’!’（非）的章节。另外一个重要提示，可以使用 <code>:h function-list</code> 来查看所有函数的简要描述，列表中包括函数名和一句话描述。</p></li><li><p>关于映射都可以在 <code>:h map.txt</code> 中找到。通过 <code>:h mapmode-i</code> 来查找 <code>:imap</code> 命令的相关信息；通过 <code>:h map-topic</code> 来查找专门针对映射的帮助（译者注：topic 为一个占位符，正如上面的字符 ‘X’ 一样，在实际使用中需要替换成相应的单词）（如：<code>:h :map-local</code> 查询本地 buffer 的映射，<code>:h map-bar</code> 查询如何在映射中处理’|’)。</p></li><li><p>命令定义用 “command-“ 开头，如用 <code>:h command-bar</code> 来查看自定义命令中’!’的作用。</p></li><li><p>窗口管理类的命令是以 “CTRL-W” 开头的，所以你可以用 <code>:h CTRL-W_*</code> 来查找相应的帮助（译者注：’*‘同样为占位符）（如：<code>:h CTRL-W_p</code> 查看切换到之前访问的窗口命令的解释）。如果你想找窗口处理的命令，还可以通过访问 <code>:h windows.txt</code> 并逐行向下浏览，所有窗口管理的命令都在这里了。</p></li><li><p>执行类的命令以”:”开头，即：<code>:h :s</code> 讲的是 “:s” 命令。</p></li><li><p>在输入某个话题时按 CTRL-D，让 Vim 列出所有的近似项辅助你输入。</p></li><li><p>用 <code>:helpgrep</code> 在所有的帮助页面（通常还包括了已安装的插件的帮助页面）中进行搜索。参考 <code>:h :helpgrep</code> 来了解如何使用。当你搜索了一个话题之后，所有的匹配结果都被保存到了全局位置信息表（或局部位置信息表）当中，可以通过 <code>:copen</code> 或 <code>:lopen</code> 打开。在打开的窗口中可能通过 <code>/</code> 对搜索结果进行进一步的过滤。</p></li><li><p><code>:h helphelp</code> 里介绍了如何使用帮助系统。</p></li><li><p>用户手册。它采用了一种对初学者更加友好的方式来展示帮助话题。用 <code>:h usr_toc.txt</code> 打开目录（你可能已经猜到这个命令的用处了）。浏览用户手册能帮助你找出某些你想了解的话题，如你可以在第 24 章看到关于“复合字符”以及“输入特殊字符”的讲解（用 <code>:h usr_24.txt</code> 可以快速打开相关章节）。</p></li><li><p>高亮分组的帮助以 <code>hl-</code> 开头。如：<code>:h hl-WarningMsg</code> 说的是警告信息分组的高亮。</p></li><li><p>语法高亮以<code>:syc-</code> 开头，如：<code>:h :syn-conceal</code> 讲的是 <code>:syn</code> 命令的对于隐藏字符是如何显示的。</p></li><li><p>快速修复命令以 <code>:c</code> 开头，而位置列表命令以 <code>:l</code> 开头。</p></li><li><p><code>:h BufWinLeave</code> 讲的是 BufWinLeave 自动命令。还有，<code>:h autocommand-events</code> （译者注：原文是 <code>:h autocommands-events</code>，但是没有该帮助）讲的是所有可用的事件。</p></li><li><p>启动参数都以“-”开头，如：<code>:h -f</code> 会告诉你 Vim 中 “-f” 参数的作用。</p></li><li><p>额外的特性都以“+”开头，如：<code>:h +conceal</code> 讲的是关于隐藏字符的支持。</p></li><li><p>错误代码可以在帮助系统中直接查到。<code>:h E297</code> 会带你到关于这一错误的详细解释。但是有时并没有转到错误描述，而是列出了经常导出这一错误的 Vim 命令，如 <code>:h E128</code> （译者注：原文为<code>:h hE128</code>，但是并没有该帮助）会直接跳转到 <code>:function</code> 命令。</p></li><li><p>关于包含的语法文件的文档的帮助话题格式是 <code>:h ft-*-syntax</code>。如：<code>:h ft-c-syntax</code> 说的就是 C 语言语法文件以及它所提供的选项。有的语法文件还会带有自动完成（<code>:h ft-php-omni</code>）或文件类型插件（<code>:h ft-tex-plugin</code>）相关的章节可以查看。</p></li></ol><p>另外在每个帮助页的顶端通常会包含一个用户文档链接（更多的从从用户的角度出发来主角命令的功能和用法，不涉及那么多细节）。如：<code>:h pattern.txt</code> 里包含了 <code>:h 03.9</code> 和 <code>:h usr_27</code> 两个章节的链接。</p><h2 id="获取在线帮助"><a href="#获取在线帮助" class="headerlink" title="获取在线帮助"></a>获取在线帮助</h2><p>如果你遇到了无法解决的问题，或者需要指引的话，可以参考 <a href="https://groups.google.com/forum/#!forum/vim_use">Vim 使用</a>邮件列表。 <a href="https://de.wikipedia.org/wiki/Internet_Relay_Chat">IRC</a> 也是一个很不错的资源。 <a href="https://freenode.net/">Freenode</a> 上的 <code>#vim</code> 频道很庞大，并且里面有许多乐于助人的人。</p><p>如果你想给 Vim 提交 Bug 的话，可以使用 <a href="https://groups.google.com/forum/#!forum/vim_dev">vim_dev</a> 邮件列表。</p><h2 id="执行自动命令"><a href="#执行自动命令" class="headerlink" title="执行自动命令"></a>执行自动命令</h2><p>你可以触发任何事件，如：<code>:doautocmd BufRead</code>。</p><h3 id="用户自定义事件"><a href="#用户自定义事件" class="headerlink" title="用户自定义事件"></a>用户自定义事件</h3><p>对于插件而言，创建你自己的自定义事件有时非常有用。</p><figure class="highlight vim"><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><span class="line"><span class="keyword">function!</span> <span class="title">Chibby</span><span class="params">()</span></span><br><span class="line">    <span class="comment">&quot; A lot of stuff is happening here.</span></span><br><span class="line">    <span class="comment">&quot; And at last..</span></span><br><span class="line">    <span class="keyword">doautocmd</span> User ChibbyExit</span><br><span class="line"><span class="keyword">endfunction</span></span><br></pre></td></tr></table></figure><p>现在你插件的用户可以在 Chibby 执行完成之后做任何他想做的事情：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">autocmd</span> User ChibbyExit <span class="keyword">call</span> ChibbyCleanup()</span><br></pre></td></tr></table></figure><p>顺便提一句，如果在使用 <code>:autocmd</code> 或 <code>:doautocmd</code> 时没有捕捉异常，那么会输出 “No matching autocommands” 信息。这也是为什么许多插件用 <code>silent doautocmd ...</code> 的原因。但是这也会有不足，那就是你不能再在 :autocmd 中使用 <code>echo &quot;foo&quot;</code> 了，取而代之的是你要使用 <code>unsilent echo &quot;foo&quot;</code> 来输出。</p><p>这就是为什么要在触发事件之前先判断事件是否存在的原因，</p><figure class="highlight vim"><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><span class="line"><span class="keyword">if</span> <span class="built_in">exists</span>(<span class="string">&#x27;#User#ChibbyExit&#x27;</span>)</span><br><span class="line">  <span class="keyword">doautocmd</span> User ChibbyExit</span><br><span class="line"><span class="keyword">endif</span></span><br></pre></td></tr></table></figure><p>帮助文档：<code>:h User</code></p><h3 id="事件嵌套"><a href="#事件嵌套" class="headerlink" title="事件嵌套"></a>事件嵌套</h3><p>默认情况下，自动命令不能嵌套！如果某个自动命令执行了一个命令，这个命令再依次触发其它的事件，这是不可能的。</p><p>例如你想在每次启动 Vim 的时候自动打开你的 vimrc 文件：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">autocmd</span> VimEnter * <span class="keyword">edit</span> $MYVIMRC</span><br></pre></td></tr></table></figure><p>当你启动 Vim 的时候，它会帮你打开你的 vimrc 文件，但是你很快会注意到这个文件没有任何的高亮，尽管平时它是正常可以高亮的。</p><p>问题在于你的非嵌套自动命令 <code>:edit</code> 不会触发“BufRead”事件，所以并不会把文件类型设置成“vim”，进而 <code>$VIMRUNTIME/syntax/vim.vim</code> 永远不会被引入。详细信息请参考：<code>:au BufRead *.vim</code>。要想完成上面所说的需求，使用下面这个命令：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">autocmd</span> VimEnter * nested <span class="keyword">edit</span> $MYVIMRC</span><br></pre></td></tr></table></figure><p>帮助文档：<code>:h autocmd-nested</code></p><h2 id="剪切板"><a href="#剪切板" class="headerlink" title="剪切板"></a>剪切板</h2><p>如果你想在没有 GUI 支持的 Unix 系统中使用 Vim 的 <code>&#39;clipboard&#39;</code> 选项，则需要 <code>+clipboard</code> 以及可选的 <code>+xterm_clipboard</code> 两个<a href="#what-kind-of-vim-am-i-running">特性</a>支持。</p><p>帮助文档：</p><figure class="highlight vim"><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><span class="line">:h <span class="string">&#x27;clipboard&#x27;</span></span><br><span class="line">:h <span class="keyword">gui</span>-clipboard</span><br><span class="line">:h <span class="keyword">gui</span>-selections</span><br></pre></td></tr></table></figure><p>另外请参考：<a href="#%E6%8C%81%E7%BB%AD%E7%B2%98%E8%B4%B4%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E6%AF%8F%E6%AC%A1%E9%83%BD%E8%A6%81%E8%AE%BE%E7%BD%AE-paste-%E6%A8%A1%E5%BC%8F">持续粘贴（为什么我每次都要设置 ‘paste’ 模式</a></p><h3 id="剪贴板的使用（Windows-OSX）"><a href="#剪贴板的使用（Windows-OSX）" class="headerlink" title="剪贴板的使用（Windows, OSX）"></a>剪贴板的使用（Windows, OSX）</h3><p>Windows 自带了<a href="https://msdn.microsoft.com/en-us/library/windows/desktop/ms649012(v=vs.85).aspx">剪贴板</a>，OSX 则带了一个<a href="https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/PasteboardGuide106/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008100-SW1">粘贴板</a></p><p>在这两个系统中都可以用大家习惯用的 <code>ctrl+c / cmd+c</code> 复制选择的文本，然后在另外一个应用中用 <code>ctrl+v / cmd+v</code> 进行粘贴。</p><p>需要注意的是复制的文本已经被发送到了剪贴板，所以你在粘贴复制的内容之前关闭这个应用是没有任何问题的。</p><p>每次复制的时候，都会向剪贴板寄存器 <code>*</code> 中写入数据。 而在 Vim 中分别使用 <code>&quot;*y</code> 和 <code>&quot;*p</code> 来进行复制（yank) 和 粘贴（paste)。</p><p>如果你不想每次操作都要指定 <code>*</code> 寄存器，可以在你的 vimrc 中添加如下配置：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">set</span> clipboard=unnamed</span><br></pre></td></tr></table></figure><p>通常情况下复制&#x2F;删除&#x2F;放入操作会往 <code>&quot;</code> 寄存器中写入数据，而加上了上面的配置之后 <code>*</code> 寄存器也会被写入同样数据，因此简单的使用 <code>y</code> 和 <code>p</code> 就可以复制粘贴了。</p><p>我再说一遍：使用上面的选项意味着每一次的复制&#x2F;粘贴，即使在同一个 Vim 窗口里，都会修改剪贴板的内容。你自己决定上面的选项是否适合。</p><p>如果你觉得输入 <code>y</code> 还是太麻烦的话，可以使用下面的设置把在可视模式下选择的内容发送到剪贴板：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">set</span> clipboard=unnamed,autoselect</span><br><span class="line"><span class="keyword">set</span> guioptions+=<span class="keyword">a</span></span><br></pre></td></tr></table></figure><p>帮助文档：</p><figure class="highlight vim"><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><span class="line">:h clipboard-unnamed</span><br><span class="line">:h autoselect</span><br><span class="line">:h <span class="string">&#x27;go_a&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="剪贴板的使用（Linux-BSD-…）"><a href="#剪贴板的使用（Linux-BSD-…）" class="headerlink" title="剪贴板的使用（Linux, BSD, …）"></a>剪贴板的使用（Linux, BSD, …）</h3><p>如果你的系统使用了 <a href="http://www.x.org/wiki">X 图形界面</a>，事情会变得有一点不同。X 图形界面实现了 <a href="http://www.x.org/releases/X11R7.7/doc/xproto/x11protocol.html">X 窗口系统协议</a>, 这个协议在 1987 年发布的主版本 11，因此 X 也通常被称为 X11。</p><p>在 X10 版本中，<a href="http://www.x.org/releases/X11R7.7/doc/xorg-docs/icccm/icccm.html#Peer_to_Peer_Communication_by_Means_of_Cut_Buffers">剪贴缓冲区</a>被用来实现像 <em>clipboard</em> 一样由 X 来复制文本，并且可以被所有的程序访问。现在这个机制在 X 中还存在，但是已经过时了，很多程序都不再使用这一机制。</p><p>近年来数据在程序之间是通过<a href="http://www.x.org/releases/X11R7.7/doc/xorg-docs/icccm/icccm.html#Peer_to_Peer_Communication_by_Means_of_Selections">选择</a>进行传递的。一共有三种选择，经常用到的有两种：PRIMARY 和 CLIPBOARD。</p><p>选择的工作工模大致是这样的：</p><pre><code>Program A：&lt;ctrl+c&gt;Program A：声称对 CLIPBOARD 的所有权Program B：&lt;ctrl+v&gt;Program B：发现CLIPBOARD的所有权被Program A持有Program B：从Program A请求数据Program A：响应这个请求并发送数据给Program BProgram B：从Program A接收数据并插入到窗口中</code></pre><table><thead><tr><th>选择</th><th>何时使用</th><th>如何粘贴</th><th>如何在 Vim 中访问</th></tr></thead><tbody><tr><td>PRIMARY</td><td>选择文本</td><td>鼠标中键, shift+insert</td><td><code>*</code> 寄存器</td></tr><tr><td>CLIPBOARD</td><td>选择文本并按 <code>ctrl+c</code></td><td><code>ctrl+v</code></td><td><code>+</code>寄存器</td></tr></tbody></table><p><strong>注意</strong>：X 服务器并不会保存选择（不仅仅是 CLIPBOARD 选择）！因此在关闭了相应的程序后，你用 <code>ctrl+c</code> 复制的内容将丢失。</p><p>使用 <code>&quot;*p</code> 来贴粘 PRIMARY 选择中的内容，或者使用 <code>&quot;+y1G</code> 来将整个文件的内容复制到 CLIPBOARD 选择。</p><p>如果你需要经常访问这两个寄存器，可以考虑使用如下配置：</p><figure class="highlight vim"><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><span class="line"><span class="keyword">set</span> clipboard^=unnamed          <span class="comment">&quot; * 寄存器</span></span><br><span class="line"><span class="comment">&quot; 或者</span></span><br><span class="line"><span class="keyword">set</span> clipboard^=unnamedplus      <span class="comment">&quot; + 寄存器</span></span><br></pre></td></tr></table></figure><p>（<code>^=</code> 用来将设置的值加到默认值之前，详见：<code>:h :set^=</code>）</p><p>这会使得所有复制&#x2F;删除&#x2F;放入操作使用 <code>*</code> 或 <code>+</code> 寄存器代替默认的未命令寄存器 <code>&quot;</code>。之后你就可以直接使用 <code>y</code> 或 <code>p</code> 访问你的 X 选择了。</p><p>帮助文档：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:h clipboard-unnamed</span><br><span class="line">:h clipboard-unnamedplus</span><br></pre></td></tr></table></figure><h2 id="打开文件时恢复光标位置"><a href="#打开文件时恢复光标位置" class="headerlink" title="打开文件时恢复光标位置"></a>打开文件时恢复光标位置</h2><p>如果没有这个设置，每次打开文件时光标都将定位在第一行。而加入了这个设置以后，你就可以恢复到上次关闭文件时光标所在的位置了。</p><p>将下面的配置添加到你的 vimrc 文件：</p><figure class="highlight vim"><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><span class="line"><span class="keyword">autocmd</span> BufReadPost *</span><br><span class="line">    \ <span class="keyword">if</span> <span class="built_in">line</span>(<span class="string">&quot;&#x27;\&quot;&quot;</span>) &gt; <span class="number">1</span> &amp;&amp; <span class="built_in">line</span>(<span class="string">&quot;&#x27;\&quot;&quot;</span>) &lt;= <span class="built_in">line</span>(<span class="string">&quot;$&quot;</span>) |</span><br><span class="line">    \   <span class="keyword">exe</span> <span class="string">&quot;normal! g`\&quot;&quot;</span> |</span><br><span class="line">    \ <span class="keyword">endif</span></span><br></pre></td></tr></table></figure><p>这是通过判断之前的光标位置是否存在（文件可能被其它程序修改而导致所记录的位置已经不存在了），如果存在的话就执行 <code>g`&quot;</code> （转到你离开时的光标位置但是不更改跳转列表）。</p><p>这需要使用 viminfo 文件：<code>:h viminfo-</code>。</p><h2 id="临时文件"><a href="#临时文件" class="headerlink" title="临时文件"></a>临时文件</h2><p>根据选项的不同， Vim 最多会创建 4 种工作文件。</p><h3 id="备份文件"><a href="#备份文件" class="headerlink" title="备份文件"></a>备份文件</h3><p>你可以让 Vim 在将修改写入到文件之前先备份原文件。默认情况下， Vim 会保存一个备份文件但是当修改成功写入后会立即删除它（<code>:set writebackup</code>）。如果你想一直保留这个备份文件的话，可以使用 <code>:set backup</code>。而如果你想禁用备份功能的话，可以使用 <code>:set nobackup nowritebackup</code>。</p><p>咱们来看一下上次我在 vimrc 中改了什么：</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></pre></td><td class="code"><pre><span class="line">$ diff ~/.vim/vimrc ~/.vim/files/backup/vimrc-vimbackup</span><br><span class="line">390d389</span><br><span class="line">&lt; <span class="built_in">command</span>! -bar -nargs=* -complete=<span class="built_in">help</span> H helpgrep &lt;args&gt;</span><br></pre></td></tr></table></figure><p>帮助文档：<code>:h backup</code></p><h3 id="交换文件"><a href="#交换文件" class="headerlink" title="交换文件"></a>交换文件</h3><p>假设你有一个非常棒的科幻小说的构思。在按照故事情节已经写了好几个小时几十万字的时候..忽然停电了！而那时你才想起来你上次保存 <code>~/来自外太空的邪恶入侵者.txt</code> 是在.. 好吧，你从来没有保存过。</p><p>但是并非没有希望了！在编辑某个文件的时候， Vim 会创建一个交换文件，里面保存的是对当前文件所有未保存的修改。自己试一下，打开任意的文件，并使用 <code>:swapname</code> 获得当前的交换文件的保存路径。你也可以将 <code>:set noswapfile</code> 加入到 vimrc 中来禁用交换文件。</p><p>默认情况下，交换文件会自动保存在被编辑文件所在的目录下，文件名以 <code>.file.swp</code> 后缀结尾，每当你修改了超过 200 个字符或是在之前 4 秒内没有任何动作时更新它的内容，在你不再编辑这个文件的时候会被删除。你可以自己修改这些数字，详见：<code>:h &#39;updatecount&#39;</code> 和 <code>:h &#39;updatetime&#39;</code>。</p><p>而在断电时，交换文件并不会被删除。当你再次打开 <code>vim ~/来自外太空的邪恶入侵者.txt</code> 时， Vim 会提示你恢复这个文件。</p><p>帮助文档：<code>:h swap-file</code> 和 <code>:h usr_11</code></p><h3 id="撤销文件"><a href="#撤销文件" class="headerlink" title="撤销文件"></a>撤销文件</h3><p><a href="#%E5%86%85%E5%AE%B9%E5%8F%98%E6%9B%B4%E5%8E%86%E5%8F%B2%E8%AE%B0%E5%BD%95">内容变更历史记录</a>是保存在内存中的，并且会在 Vim 退出时清空。如果你想让它持久化到磁盘中，可以设置 <code>:set undofile</code>。这会把文件 <code>~/foo.c</code> 的撤销文件保存在 <code>~/foo.c.un~</code>。</p><p>帮助文档：<code>:h &#39;undofile&#39;</code> 和 <code>:h undo-persistence</code></p><h3 id="viminfo-文件"><a href="#viminfo-文件" class="headerlink" title="viminfo 文件"></a>viminfo 文件</h3><p>备份文件、交换文件和撤销文件都是与文本状态相关的，而 viminfo 文件是用来保存在 Vim 退出时可能会丢失的其它的信息的。包括历史记录（命令历史、搜索历史、输入历史）、寄存器内容、标注、缓冲区列表、全局变量等等。</p><p>默认情况下，viminfo 被保存在 <code>~/.viminfo</code>。</p><p>帮助文档：<code>:h viminfo</code> 和 <code>:h &#39;viminfo&#39;</code></p><h3 id="临时文件管理设置示例"><a href="#临时文件管理设置示例" class="headerlink" title="临时文件管理设置示例"></a>临时文件管理设置示例</h3><p>如果你跟我一样，也喜欢把这些文件放到一个位置（如：<code>~/.vim/files</code>）的话，可以使用下面的配置：</p><figure class="highlight vim"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">&quot; 如果文件夹不存在，则新建文件夹</span></span><br><span class="line"><span class="keyword">if</span> !isdirectory($HOME.<span class="string">&#x27;/.vim/files&#x27;</span>) &amp;&amp; <span class="built_in">exists</span>(<span class="string">&#x27;*mkdir&#x27;</span>)</span><br><span class="line">  <span class="keyword">call</span> <span class="built_in">mkdir</span>($HOME.<span class="string">&#x27;/.vim/files&#x27;</span>)</span><br><span class="line"><span class="keyword">endif</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&quot; 备份文件</span></span><br><span class="line"><span class="keyword">set</span> backup</span><br><span class="line"><span class="keyword">set</span> backupdir   =$HOME/.<span class="keyword">vim</span>/<span class="keyword">files</span>/backup/</span><br><span class="line"><span class="keyword">set</span> backupext   =-vimbackup</span><br><span class="line"><span class="keyword">set</span> backupskip  =</span><br><span class="line"><span class="comment">&quot; 交换文件</span></span><br><span class="line"><span class="keyword">set</span> directory   =$HOME/.<span class="keyword">vim</span>/<span class="keyword">files</span>/swap//</span><br><span class="line"><span class="keyword">set</span> updatecount =<span class="number">100</span></span><br><span class="line"><span class="comment">&quot; 撤销文件</span></span><br><span class="line"><span class="keyword">set</span> <span class="built_in">undofile</span></span><br><span class="line"><span class="keyword">set</span> undodir     =$HOME/.<span class="keyword">vim</span>/<span class="keyword">files</span>/<span class="keyword">undo</span>/</span><br><span class="line"><span class="comment">&quot; viminfo 文件</span></span><br><span class="line"><span class="keyword">set</span> viminfo     =<span class="string">&#x27;100,n$HOME/.vim/files/info/viminfo</span></span><br></pre></td></tr></table></figure><p>注意：如果你在一个多用户系统中编辑某个文件时， Vim 提示你交换文件已经存在的话，可能是因为有其他的用户此时正在编辑这个文件。而如果将交换文件放到自己的 home 目录的话，这个功能就失效了。因此服务器非常不建议将这些文件修改到 HOME 目录，避免多人同时编辑一个文件，却没有任何警告。</p><h2 id="编辑远程文件"><a href="#编辑远程文件" class="headerlink" title="编辑远程文件"></a>编辑远程文件</h2><p>Vim 自带的 netrw 插件支持对远程文件的编辑。实际上它将远程的文件通过 scp 复制到本地的临时文件中，再用那个文件打开一个缓冲区，然后在保存时把文件再复制回远程位置。</p><p>下面的命令在你本地的 VIM 配置与 SSH 远程服务器上管理员想让你使用的配置有冲突时尤其有用：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">e</span> scp://bram@awesome.site.<span class="keyword">com</span>/.vimrc</span><br></pre></td></tr></table></figure><p>如果你已经设置了 <code>~/.ssh/config</code>，SSH 会自动读取这里的配置：</p><pre><code>Host awesome    HostName awesome.site.com    Port 1234    User bram</code></pre><p>如果你的 <code>~/.ssh/config</code> 中有以上的内容，那么下面的命令就可以正常执行了：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">e</span> scp://awesome/.vimrc</span><br></pre></td></tr></table></figure><p>可以用同样的方法编辑 <code>~/.netrc</code>, 详见：<code>:h netrc-netrc</code>。</p><p>确保你已经看过了 <code>:h netrw-ssh-hack</code> 和 <code>:h g:netrw_ssh_cmd</code>。</p><p>另外一种编辑远程文件的方法是使用 <a href="https://wiki.archlinux.org/index.php/Sshfs">sshfs</a>，它会用 <a href="https://en.wikipedia.org/wiki/Filesystem_in_Userspace">FUSE</a> 来挂载远程的文件系统到你本地的系统当中。</p><h2 id="插件管理"><a href="#插件管理" class="headerlink" title="插件管理"></a>插件管理</h2><p><a href="https://github.com/tpope/vim-pathogen">Pathogen</a>是第一个比较流行的插件管理工具。实际上它只是修改了 <em>runtimepath</em> （<code>:h &#39;rtp&#39;</code>） 来引入所有放到该目录下的文件。你需要自己克隆插件的代码仓库到那个目录。</p><p>真正的插件管理工具会在 Vim 中提供帮助你安装或更新插件的命令。以下是一些常用的插件管理工具：</p><ul><li><a href="https://github.com/Shougo/dein.vim">dein</a></li><li><a href="https://github.com/junegunn/vim-plug">plug</a></li><li><a href="https://github.com/MarcWeber/vim-addon-manager">vim-addon-manager</a></li><li><a href="https://github.com/VundleVim/Vundle.vim">vundle</a></li></ul><h2 id="多行编辑"><a href="#多行编辑" class="headerlink" title="多行编辑"></a>多行编辑</h2><p>这是一种可以同时输入多行连续文本的技术。参考这个<a href="https://raw.githubusercontent.com/mhinz/vim-galore/master/contents/images/content-block_insert.gif">示例</a>。</p><p>用 <code>&lt;c-v&gt;</code> 切换到可视块模式。然后向下选中几行，按 <code>I</code> 或 <code>A</code> （译者注：大写字母，即 shift+i 或 shift+a）然后开始输入你想要输入的文本。</p><p>在刚开始的时候可能会有些迷惑，因为文本只出现在了当前编辑的行，只有在当前的插入动作结束后，之前选中的其它行才会出现插入的文本。</p><p>举一个简单的例子：<code>&lt;c-v&gt;3jItext&lt;esc&gt;</code>。</p><p>如果你要编辑的行长度不同，但是你想在他们后面追加相同的内容的话，可以试一下这个：<code>&lt;c-v&gt;3j$Atext&lt;esc&gt;</code>。</p><p>有时你可能需要把光标放到当前行末尾之后，默认情况下你是不可能做到的，但是可能通过设置 <code>virtualedit</code> 选项达到目的：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">set</span> virtualedit=<span class="keyword">all</span></span><br></pre></td></tr></table></figure><p>设置之后 <code>$10l</code> 或 <code>90|</code> 都会生效，即使超过了行尾的长度。</p><p>详见 <code>:h blockwise-examples</code>。在开始的时候可能会觉得有些复杂，但是它很快就会成为你的第二天性的。</p><p>如果你想探索更有趣的事情，可以看看<a href="https://github.com/terryma/vim-multiple-cursors">多光标</a></p><h2 id="使用外部程序和过滤器"><a href="#使用外部程序和过滤器" class="headerlink" title="使用外部程序和过滤器"></a>使用外部程序和过滤器</h2><p>免责声明：Vim 是单线程的，因此在 Vim 中以前端进程执行其它的程序时会阻止其它的一切。当然你可以使用 Vim 程序接口，如 Lua，并且使用它的多线程支持，但是在那期间， Vim 的处理还是被阻止了。Neovim 添加了任务 API 解决了此问题。</p><p>（据说 Bram 正在考虑在 Vim 中也添加任务控制。如果你使用了较新版本的的 Vim ，可以看一下 <code>:helpgrep startjob</code>。）</p><p>使用 <code>:!</code> 启动一个新任务。如果你想列出当前工作目录下的所有文件，可以使用 <code>:!ls</code>。 用 <code>|</code> 来将结果通过管道重定向，如：<code>:!ls -l | sort | tail -n5</code>。</p><p>没有使用范围时（译者注：范围就是 <code>:</code> 和 <code>!</code> 之间的内容，<code>.</code> 表示当前行，<code>+4</code> 表示向下偏移 4 行，<code>$</code> 表示最末行等，多行时用 <code>,</code> 将它们分开，如 <code>.,$</code> 表示从当前行到末行），<code>:!</code> 会显示在一个可滚动的窗口中（译者注：在 GVim 和在终端里运行的结果稍有不同）。相反的，如果指定了范围，这些行会被<a href="https://en.wikipedia.org/wiki/Filter_(software)">过滤</a>。这意味着它们会通过管道被重定向到过滤程序的 <a href="https://en.wikipedia.org/wiki/Standard_streams#Standard_input_.28stdin.29">stdin</a>，在处理后再通过过滤程序的 <a href="https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29">stdout</a> 输出，用输出结果替换范围内的文本。例如：为接下来的 5 行文本添加行号，可以使用：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:.,+<span class="number">4</span>!nl -<span class="keyword">ba</span> -w1 -s<span class="string">&#x27; &#x27;</span></span><br></pre></td></tr></table></figure><p>由于手动添加范围很麻烦， Vim 提供了一些辅助方法以方便的添加范围。如果需要经常带着范围的话，你可以在可见模式中先选择，然后再按 <code>:</code> （译者注：选中后再按 <code>!</code> 更方便）。还可以使用 <code>!</code> 来取用一个 motion 的范围，如 <code>!ipsort</code> （译者注：原文为 <code>!ip!sort</code> ，但经过实验发现该命令执行报错，可能是因为 Vim 版本的原因造成的，新版本使用 <code>ip</code> 选择当前段落后自动在命令后添加了 <code>!</code> ，按照作者的写法来看，可能之前的版本没有自动添加 <code>!</code> ）可以将当前段落的所有行按字母表顺序进行排序。</p><p>一个使用过滤器比较好的案例是<a href="https://golang.org/">Go 语言</a>。它的缩进语法非常个性，甚至还专门提供了一个名为 <code>gofmt</code> 的过滤器来对 Go 语言的源文件进行正确的缩进。Go 语言的插件通常会提供一个名为 <code>:Fmt</code> 的函数，这个函数就是执行了 <code>:%!gofmt</code> 来对整个文件进行缩进。</p><p>人们常用 <code>:r !prog</code> 将 prog 程序的插入放到当前行的下面，这对于脚本来说是很不错的选择，但是在使用的过程中我发现 <code>!!ls</code> 更加方便，它会用输出结果替换当前行的内容。（译者注：前面命令中的 <code>prog</code> 只是个占位符，在实际使用中需要替换成其它的程序，如 <code>:r !ls</code>，这就与后面的 <code>!!ls</code> 相对应了，两者唯一的不同是第一个命令不会覆盖当前行内容，但是第二个命令会）</p><p>帮助文档：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:h <span class="built_in">filter</span></span><br><span class="line">:h :read!</span><br></pre></td></tr></table></figure><h2 id="Cscope"><a href="#Cscope" class="headerlink" title="Cscope"></a>Cscope</h2><p><a href="http://cscope.sourceforge.net/">Cscope</a> 的功能比 <a href="http://ctags.sourceforge.net/">ctags</a> 要完善，但是只支持 C（通过设置 cscope.files 后同样支持 C++以及 Java）。</p><p>鉴于 Tag 文件只是知道某个符号是在哪里定义的，cscope 的数据库里的数据信息就多的多了：</p><ul><li>符号是在哪里定义的？</li><li>符号是在哪里被使用的？</li><li>这个全局符号定义了什么？</li><li>这个变量是在哪里被赋值的？</li><li>这个函数在源文件的哪个位置？</li><li>哪些函数调用了这个函数？</li><li>这个函数调用了哪些函数？</li><li>“out of space”消息是从哪来的？</li><li>在目录结构中当前的源文件在哪个位置？</li><li>哪些文件引用了这个头文件？</li></ul><h3 id="1-构建数据库"><a href="#1-构建数据库" class="headerlink" title="1. 构建数据库"></a>1. 构建数据库</h3><p>在你项目的根目录执行下面的命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ cscope -bqR</span><br></pre></td></tr></table></figure><p>这条命令会在当前目录下创建三个文件：<code>cscope{,.in,.po}.out</code> 。把它们想象成你的数据库。</p><p>不幸的时 <code>cscope</code> 默认只分析 <code>*.[c|h|y|l]</code> 文件。如果你想在 Java 项目中使用 cscope ，需要这样做：</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><span class="line">$ find . -name <span class="string">&quot;*.java&quot;</span> &gt; cscope.files</span><br><span class="line">$ cscope -bq</span><br></pre></td></tr></table></figure><h3 id="2-添加数据库"><a href="#2-添加数据库" class="headerlink" title="2. 添加数据库"></a>2. 添加数据库</h3><p>打开你新创建的数据库连接：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">cs</span> <span class="built_in">add</span> <span class="keyword">cscope</span>.out</span><br></pre></td></tr></table></figure><p>检查连接已经创建成功：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">cs</span> show</span><br></pre></td></tr></table></figure><p>（当然你可以添加多个连接。）</p><h3 id="3-查询数据库"><a href="#3-查询数据库" class="headerlink" title="3. 查询数据库"></a>3. 查询数据库</h3><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">cs</span> <span class="keyword">find</span> <span class="symbol">&lt;kind&gt;</span> <span class="symbol">&lt;query&gt;</span></span><br></pre></td></tr></table></figure><p>如：<code>:cs find d foo</code> 会列出 <code>foo(...)</code> 调用的所有函数。</p><table><thead><tr><th>Kind</th><th>说明</th></tr></thead><tbody><tr><td>s</td><td><strong>s</strong>ymbol：查找使用该符号的引用</td></tr><tr><td>g</td><td><strong>g</strong>lobal：查找该全局符号的定义</td></tr><tr><td>c</td><td><strong>c</strong>alls：查找调用当前方法的位置</td></tr><tr><td>t</td><td><strong>t</strong>ext：查找出现该文本的位置</td></tr><tr><td>e</td><td><strong>e</strong>grep：使用 egrep 搜索当前单词</td></tr><tr><td>f</td><td><strong>f</strong>ile：打开文件名</td></tr><tr><td>i</td><td><strong>i</strong>ncludes：查询引入了当前文件的文件</td></tr><tr><td>d</td><td><strong>d</strong>epends：查找当前方法调用的方法</td></tr></tbody></table><p>推荐一些比较方便的映射，如：</p><figure class="highlight vim"><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><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;buffer&gt;</span> <span class="symbol">&lt;leader&gt;</span><span class="keyword">cs</span> :<span class="keyword">cscope</span> <span class="keyword">find</span> s  <span class="symbol">&lt;c-r&gt;</span>=<span class="built_in">expand</span>(<span class="string">&#x27;&lt;cword&gt;&#x27;</span>)<span class="symbol">&lt;cr&gt;</span><span class="symbol">&lt;cr&gt;</span></span><br><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;buffer&gt;</span> <span class="symbol">&lt;leader&gt;</span><span class="keyword">cg</span> :<span class="keyword">cscope</span> <span class="keyword">find</span> g  <span class="symbol">&lt;c-r&gt;</span>=<span class="built_in">expand</span>(<span class="string">&#x27;&lt;cword&gt;&#x27;</span>)<span class="symbol">&lt;cr&gt;</span><span class="symbol">&lt;cr&gt;</span></span><br><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;buffer&gt;</span> <span class="symbol">&lt;leader&gt;</span><span class="keyword">cc</span> :<span class="keyword">cscope</span> <span class="keyword">find</span> <span class="keyword">c</span>  <span class="symbol">&lt;c-r&gt;</span>=<span class="built_in">expand</span>(<span class="string">&#x27;&lt;cword&gt;&#x27;</span>)<span class="symbol">&lt;cr&gt;</span><span class="symbol">&lt;cr&gt;</span></span><br><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;buffer&gt;</span> <span class="symbol">&lt;leader&gt;</span>ct :<span class="keyword">cscope</span> <span class="keyword">find</span> t  <span class="symbol">&lt;c-r&gt;</span>=<span class="built_in">expand</span>(<span class="string">&#x27;&lt;cword&gt;&#x27;</span>)<span class="symbol">&lt;cr&gt;</span><span class="symbol">&lt;cr&gt;</span></span><br><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;buffer&gt;</span> <span class="symbol">&lt;leader&gt;</span><span class="keyword">ce</span> :<span class="keyword">cscope</span> <span class="keyword">find</span> <span class="keyword">e</span>  <span class="symbol">&lt;c-r&gt;</span>=<span class="built_in">expand</span>(<span class="string">&#x27;&lt;cword&gt;&#x27;</span>)<span class="symbol">&lt;cr&gt;</span><span class="symbol">&lt;cr&gt;</span></span><br><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;buffer&gt;</span> <span class="symbol">&lt;leader&gt;</span><span class="keyword">cf</span> :<span class="keyword">cscope</span> <span class="keyword">find</span> <span class="keyword">f</span>  <span class="symbol">&lt;c-r&gt;</span>=<span class="built_in">expand</span>(<span class="string">&#x27;&lt;cfile&gt;&#x27;</span>)<span class="symbol">&lt;cr&gt;</span><span class="symbol">&lt;cr&gt;</span></span><br><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;buffer&gt;</span> <span class="symbol">&lt;leader&gt;</span>ci :<span class="keyword">cscope</span> <span class="keyword">find</span> i ^<span class="symbol">&lt;c-r&gt;</span>=<span class="built_in">expand</span>(<span class="string">&#x27;&lt;cfile&gt;&#x27;</span>)<span class="symbol">&lt;cr&gt;</span>$<span class="symbol">&lt;cr&gt;</span></span><br><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;buffer&gt;</span> <span class="symbol">&lt;leader&gt;</span><span class="keyword">cd</span> :<span class="keyword">cscope</span> <span class="keyword">find</span> d  <span class="symbol">&lt;c-r&gt;</span>=<span class="built_in">expand</span>(<span class="string">&#x27;&lt;cword&gt;&#x27;</span>)<span class="symbol">&lt;cr&gt;</span><span class="symbol">&lt;cr&gt;</span></span><br></pre></td></tr></table></figure><p>所以 <code>:tag</code> （或 <code>&lt;c-]&gt;</code>）跳转到标签定义的文件，而 <code>:cstag</code> 可以达到同样的目的，同时还会打开 cscope 的数据库连接。<code>&#39;cscopetag&#39;</code> 选项使得 <code>:tag</code> 命令自动的像 <code>:cstag</code> 一样工作。这在你已经使用了基于标签的映射时会非常方便。</p><p>帮助文档：<code>:h cscope</code></p><h2 id="MatchIt"><a href="#MatchIt" class="headerlink" title="MatchIt"></a>MatchIt</h2><p>由于 Vim 是用 C 语言编写的，因此许多功能都假设使用类似 C 语言的语法。默认情况下，如果你的光标在 <code>{</code> 或 <code>#endif</code> , 就可以使用 <code>%</code> 跳转到与之匹配的 <code>}</code> 或 <code>#ifdef</code>。</p><p>Vim 自带了一个名为 matchit.vim 的插件，但是默认没有启用。启用后可以用 <code>%</code> 在 HTML 相匹配的标签或 VimL 的 if&#x2F;else&#x2F;endif 块之间进行跳转，它还带来了一些新的命令。</p><h3 id="在-Vim-8-中安装"><a href="#在-Vim-8-中安装" class="headerlink" title="在 Vim 8 中安装"></a>在 Vim 8 中安装</h3><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&quot; vimrc</span></span><br><span class="line">packadd! matchit</span><br></pre></td></tr></table></figure><h3 id="在-Vim-7-或者更早的版本中安装"><a href="#在-Vim-7-或者更早的版本中安装" class="headerlink" title="在 Vim 7 或者更早的版本中安装"></a>在 Vim 7 或者更早的版本中安装</h3><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&quot;vimrc</span></span><br><span class="line"><span class="keyword">runtime</span> macros/matchit.<span class="keyword">vim</span></span><br></pre></td></tr></table></figure><p>由于 matchit 的文档很全面，我建议安装以后执行一次下面的命令：</p><figure class="highlight vim"><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><span class="line">:!mkdir -<span class="keyword">p</span> ~/.<span class="keyword">vim</span>/doc</span><br><span class="line">:!cp $VIMRUNTIME/macros/matchit.<span class="keyword">vim</span> ~/.<span class="keyword">vim</span>/doc</span><br><span class="line">:<span class="keyword">helptags</span> ~/.<span class="keyword">vim</span>/doc</span><br></pre></td></tr></table></figure><h3 id="简短的介绍"><a href="#简短的介绍" class="headerlink" title="简短的介绍"></a>简短的介绍</h3><p>至此这个插件已经可以使用了。 参考 <code>:h matchit-intro</code> 来获得支持的命令以及 <code>:h matchit-languages</code> 来获得支持的语言。</p><p>你可以很方便的定义自己的匹配对，如：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">autocmd</span> FileType <span class="keyword">python</span> <span class="keyword">let</span> <span class="variable">b:match_words</span> = <span class="string">&#x27;\&lt;if\&gt;:\&lt;elif\&gt;:\&lt;else\&gt;&#x27;</span></span><br></pre></td></tr></table></figure><p>之后你就可以在任何的 Python 文件中使用 <code>%</code> （向前）或 <code>g%</code> （向后）在这三个片断之间跳转了。</p><p>帮助文档：</p><figure class="highlight vim"><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><span class="line">:h matchit-install</span><br><span class="line">:h matchit</span><br><span class="line">:h <span class="variable">b:match_words</span></span><br></pre></td></tr></table></figure><h1 id="技巧"><a href="#技巧" class="headerlink" title="技巧"></a>技巧</h1><h2 id="聪明地使用-n-和-N"><a href="#聪明地使用-n-和-N" class="headerlink" title="聪明地使用 n 和 N"></a>聪明地使用 n 和 N</h2><p><kbd>n</kbd> 与 <kbd>N</kbd> 的实际跳转方向取决于使用 <code>/</code> 还是 <code>?</code> 来执行搜索，其中 <code>/</code> 是向后搜索，<code>?</code> 是向前搜索。一开始我（原作者）觉得这里很难理解。</p><p>如果你希望 <kbd>n</kbd> 始终为向后搜索，<kbd>N</kbd> 始终为向前搜索，那么只需要这样设置：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;expr&gt;</span> n  <span class="string">&#x27;Nn&#x27;</span>[<span class="variable">v:searchforward</span>]</span><br><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;expr&gt;</span> <span class="keyword">N</span>  <span class="string">&#x27;nN&#x27;</span>[<span class="variable">v:searchforward</span>]</span><br></pre></td></tr></table></figure><h2 id="聪明地使用命令行历史"><a href="#聪明地使用命令行历史" class="headerlink" title="聪明地使用命令行历史"></a>聪明地使用命令行历史</h2><p>我（原作者）习惯用 <kbd>Ctrl</kbd> + <kbd>p</kbd> 和 <kbd>Ctrl</kbd> + <kbd>n</kbd> 来跳转到上一个&#x2F;下一个条目。其实这个操作也可以用在命令行中，快速调出之前执行过的命令。</p><p>不仅如此，你会发现 <kbd>上</kbd> 和 <kbd>下</kbd> 其实更智能。如果命令行中已经存在了一些文字，我们可以通过按方向键来匹配已经存在的内容。比如，命令行中现在是 <code>:echo</code>，这时候我们按 <kbd>上</kbd>，就会帮我们补全成 <code>:echo &quot;Vim rocks!&quot;</code>（前提是，之前输入过这段命令）。</p><p>当然，Vim 用户都不愿意去按方向键，事实上我们也不需要去按，只需要设置这样的映射：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">cnoremap</span> <span class="symbol">&lt;c-n&gt;</span> <span class="symbol">&lt;down&gt;</span></span><br><span class="line"><span class="keyword">cnoremap</span> <span class="symbol">&lt;c-p&gt;</span> <span class="symbol">&lt;up&gt;</span></span><br></pre></td></tr></table></figure><p>这个功能，我（原作者）每天都要用很多次。</p><h2 id="智能-Ctrl-l"><a href="#智能-Ctrl-l" class="headerlink" title="智能 Ctrl-l"></a>智能 Ctrl-l</h2><p><kbd>Ctrl</kbd> + <kbd>l</kbd> 的默认功能是清空并「重新绘制」当前的屏幕，就和 <code>:redraw!</code> 的功能一样。下面的这个映射就是执行重新绘制，并且取消通过 <code>/</code> 和 <code>?</code> 匹配字符的高亮，而且还可以修复代码高亮问题（有时候，由于多个代码高亮的脚本重叠，或者规则过于复杂，Vim 的代码高亮显示会出现问题）。不仅如此，还可以刷新「比较模式」（请参阅 <code>:help diff-mode</code>）的代码高亮：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;leader&gt;</span><span class="keyword">l</span> :<span class="keyword">nohlsearch</span><span class="symbol">&lt;cr&gt;</span>:<span class="keyword">diffupdate</span><span class="symbol">&lt;cr&gt;</span>:<span class="keyword">syntax</span> <span class="keyword">sync</span> fromstart<span class="symbol">&lt;cr&gt;</span><span class="symbol">&lt;c-l&gt;</span></span><br></pre></td></tr></table></figure><h2 id="禁用错误报警声音和图标"><a href="#禁用错误报警声音和图标" class="headerlink" title="禁用错误报警声音和图标"></a>禁用错误报警声音和图标</h2><figure class="highlight vim"><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><span class="line"><span class="keyword">set</span> noerrorbells</span><br><span class="line"><span class="keyword">set</span> novisualbell</span><br><span class="line"><span class="keyword">set</span> t_vb=</span><br></pre></td></tr></table></figure><p>请参阅 <a href="http://vim.wikia.com/wiki/Disable_beeping">Vim Wiki: Disable beeping</a>。</p><h2 id="快速移动当前行"><a href="#快速移动当前行" class="headerlink" title="快速移动当前行"></a>快速移动当前行</h2><p>有时，我（原作者）想要快速把当前行上移或下移一行，只需要这样设置映射：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">nnoremap</span> [<span class="keyword">e</span>  :<span class="symbol">&lt;c-u&gt;</span><span class="keyword">execute</span> <span class="string">&#x27;move -1-&#x27;</span>. <span class="variable">v:count1</span><span class="symbol">&lt;cr&gt;</span></span><br><span class="line"><span class="keyword">nnoremap</span> ]<span class="keyword">e</span>  :<span class="symbol">&lt;c-u&gt;</span><span class="keyword">execute</span> <span class="string">&#x27;move +&#x27;</span>. <span class="variable">v:count1</span><span class="symbol">&lt;cr&gt;</span></span><br></pre></td></tr></table></figure><p>这个映射，同样可以搭配数字使用，比如连续按下 <kbd>2</kbd> <kbd>]</kbd> <kbd>e</kbd> 就可以把当前行向下移动两行。</p><h2 id="快速添加空行"><a href="#快速添加空行" class="headerlink" title="快速添加空行"></a>快速添加空行</h2><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">nnoremap</span> [<span class="symbol">&lt;space&gt;</span>  :<span class="symbol">&lt;c-u&gt;</span>put! =<span class="built_in">repeat</span>(<span class="built_in">nr2char</span>(<span class="number">10</span>), <span class="variable">v:count1</span>)<span class="symbol">&lt;cr&gt;</span><span class="string">&#x27;[</span></span><br><span class="line"><span class="string">nnoremap ]&lt;space&gt;  :&lt;c-u&gt;put =repeat(nr2char(10), v:count1)&lt;cr&gt;</span></span><br></pre></td></tr></table></figure><p>设置之后，连续按下 <kbd>5</kbd> <kbd>[</kbd> <kbd>空格</kbd> 在当前行上方插入 5 个空行。</p><h3 id="运行时检测"><a href="#运行时检测" class="headerlink" title="运行时检测"></a>运行时检测</h3><p>需要的特性：+profile</p><p>Vim 提供了一个内置的运行时检查功能，能够找出运行慢的代码。</p><p><code>:profile</code> 命令后面跟着子命令来确定要查看什么。</p><p>如果你想查看所有的：</p><figure class="highlight vim"><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><span class="line">:<span class="keyword">profile</span> start /tmp/<span class="keyword">profile</span>.<span class="built_in">log</span></span><br><span class="line">:<span class="keyword">profile</span> <span class="keyword">file</span> *</span><br><span class="line">:<span class="keyword">profile</span> func *</span><br><span class="line">&lt;<span class="keyword">do</span> something in Vim&gt;</span><br><span class="line">&lt;<span class="keyword">quit</span> Vim&gt;</span><br></pre></td></tr></table></figure><p>Vim 不断地在内存中检查信息，只在退出的时候输出出来。（Neovim 已经解决了这个问题用 <code>:profile dump</code> 命令）</p><p>看一下 <code>/tmp/profile.log</code> 文件，检查时运行的所有代码都会被显示出来，包括每一行代码运行的频率和时间。</p><p>大多数代码都是用户不熟悉的插件代码，如果你是在解决一个确切的问题，<br>直接跳到这个日志文件的末尾，那里有 <code>FUNCTIONS SORTED ON TOTAL TIME</code> 和 <code>FUNCTIONS SORTED ON SELF TIME</code> 两个部分，如果某个 function 运行时间过长一眼就可以看到。</p><h3 id="查看启动时间"><a href="#查看启动时间" class="headerlink" title="查看启动时间"></a>查看启动时间</h3><p>感觉 Vim 启动的慢？到了研究几个数字的时候了：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">vim</span> --startuptime /tmp/startup.<span class="built_in">log</span> +q &amp;&amp; <span class="keyword">vim</span> /tmp/startup.<span class="built_in">log</span></span><br></pre></td></tr></table></figure><p>第一栏是最重要的因为它显示了<strong>绝对运行时间</strong>，如果在前后两行之间时间差有很大的跳跃，那么是第二个文件太大或者含有需要检查的错误的 VimL 代码。</p><h2 id="NUL-符用新行表示"><a href="#NUL-符用新行表示" class="headerlink" title="NUL 符用新行表示"></a>NUL 符用新行表示</h2><p>文件中的 NUL 符 （<code>\0</code>），在内存中被以新行（<code>\n</code>）保存，在缓存空间中显示为 <code>^@</code>。</p><p>更多信息请参看 <code>man 7 ascii</code> 和 <code>:h NL-used-for-Nul</code> 。</p><h2 id="快速编辑自定义宏"><a href="#快速编辑自定义宏" class="headerlink" title="快速编辑自定义宏"></a>快速编辑自定义宏</h2><p>这个功能真的很实用！下面的映射，就是在一个新的命令行窗口中读取某一个寄存器（默认为 <code>*</code>）。当你设置完成后，只需要按下 <kbd>回车</kbd> 即可让它生效。</p><p>在录制宏的时候，我经常用这个来更改拼写错误。</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">nnoremap</span> <span class="symbol">&lt;leader&gt;</span><span class="keyword">m</span>  :<span class="symbol">&lt;c-u&gt;</span><span class="symbol">&lt;c-r&gt;</span><span class="symbol">&lt;c-r&gt;</span>=<span class="string">&#x27;let @&#x27;</span>. <span class="variable">v:register</span> .<span class="string">&#x27; = &#x27;</span>. <span class="built_in">string</span>(<span class="built_in">getreg</span>(<span class="variable">v:register</span>))<span class="symbol">&lt;cr&gt;</span><span class="symbol">&lt;c-f&gt;</span><span class="symbol">&lt;left&gt;</span></span><br></pre></td></tr></table></figure><p>只需要连续按下 <kbd>leader</kbd> <kbd>m</kbd> 或者 <kbd>&quot;</kbd> <kbd>leader</kbd> <kbd>m</kbd> 就可以调用了。</p><p>请注意，这里之所以要写成 <code>&lt;c-r&gt;&lt;c-r&gt;</code> 是为了确保 <code>&lt;c-r&gt;</code> 执行了。请参阅 <code>:h c_^R^R</code></p><h2 id="快速跳转到源-头-文件"><a href="#快速跳转到源-头-文件" class="headerlink" title="快速跳转到源(头)文件"></a>快速跳转到源(头)文件</h2><p>这个技巧可以用在多种文件类型中。当你从源文件或者头文件中切换到其他文件的时候，这个技巧可以设置「文件标记」（请参阅 <code>:h marks</code>），然后你就可以通过连续按下 <kbd>&#39;</kbd> <kbd>C</kbd> 或者 <kbd>&#39;</kbd> <kbd>H</kbd> 快速跳转回去（请参阅 <code>:h &#39;A</code>）。</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">autocmd</span> BufLeave *.&#123;<span class="keyword">c</span>,cpp&#125; <span class="keyword">mark</span> C</span><br><span class="line"><span class="keyword">autocmd</span> BufLeave *.h       <span class="keyword">mark</span> H</span><br></pre></td></tr></table></figure><p><strong>注意</strong>：由于这个标记是设置在 viminfo 文件中，因此请先确认 <code>:set viminfo?</code> 中包含了 <code>:h viminfo-&#39;</code>。</p><h2 id="在-GUI-中快速改变字体大小"><a href="#在-GUI-中快速改变字体大小" class="headerlink" title="在 GUI 中快速改变字体大小"></a>在 GUI 中快速改变字体大小</h2><p>印象中，我（原作者）记得一下代码是来自 tpope’s 的配置文件：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">command! Bigger  :<span class="keyword">let</span> &amp;guifont = <span class="keyword">substitute</span>(&amp;guifont, <span class="string">&#x27;\d\+$&#x27;</span>, <span class="string">&#x27;\=submatch(0)+1&#x27;</span>, <span class="string">&#x27;&#x27;</span>)</span><br><span class="line">command! Smaller :<span class="keyword">let</span> &amp;guifont = <span class="keyword">substitute</span>(&amp;guifont, <span class="string">&#x27;\d\+$&#x27;</span>, <span class="string">&#x27;\=submatch(0)-1&#x27;</span>, <span class="string">&#x27;&#x27;</span>)</span><br></pre></td></tr></table></figure><h2 id="根据模式改变光标类型"><a href="#根据模式改变光标类型" class="headerlink" title="根据模式改变光标类型"></a>根据模式改变光标类型</h2><p>我（原作者）习惯在普通模式下用块状光标，在插入模式下用条状光标（形状类似英文 “I” 的样子），然后在替换模式中使用下划线形状的光标。</p><figure class="highlight vim"><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><span class="line"><span class="keyword">if</span> <span class="built_in">empty</span>($TMUX)</span><br><span class="line">  <span class="keyword">let</span> &amp;t_SI = <span class="string">&quot;\&lt;Esc&gt;]50;CursorShape=1\x7&quot;</span></span><br><span class="line">  <span class="keyword">let</span> &amp;t_EI = <span class="string">&quot;\&lt;Esc&gt;]50;CursorShape=0\x7&quot;</span></span><br><span class="line">  <span class="keyword">let</span> &amp;t_SR = <span class="string">&quot;\&lt;Esc&gt;]50;CursorShape=2\x7&quot;</span></span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">  <span class="keyword">let</span> &amp;t_SI = <span class="string">&quot;\&lt;Esc&gt;Ptmux;\&lt;Esc&gt;\&lt;Esc&gt;]50;CursorShape=1\x7\&lt;Esc&gt;\\&quot;</span></span><br><span class="line">  <span class="keyword">let</span> &amp;t_EI = <span class="string">&quot;\&lt;Esc&gt;Ptmux;\&lt;Esc&gt;\&lt;Esc&gt;]50;CursorShape=0\x7\&lt;Esc&gt;\\&quot;</span></span><br><span class="line">  <span class="keyword">let</span> &amp;t_SR = <span class="string">&quot;\&lt;Esc&gt;Ptmux;\&lt;Esc&gt;\&lt;Esc&gt;]50;CursorShape=2\x7\&lt;Esc&gt;\\&quot;</span></span><br><span class="line"><span class="keyword">endif</span></span><br></pre></td></tr></table></figure><p>原理很简单，就是让 Vim 在进入和离开插入模式的时候，输出一些序列，请参考 <a href="https://en.wikipedia.org/wiki/Escape_sequence">escape sequence</a>。Vim 与终端之间的中间层，比如 <a href="https://tmux.github.io/">tmux</a> 会处理并执行上面的代码。</p><p>但上面这个还是有一个缺点的。终端环境的内部原理不尽相同，对于序列的处理方式也稍有不同。因此，上面的代码可能无法在你的环境中运行。甚至，你的运行环境也有可能不支持其他光标形状，请参阅你的 Vim 运行环境的文档。</p><p>好消息是，上面这个代码，可以在 iTerm2 中完美运行。</p><h2 id="防止水平滑动的时候失去选择"><a href="#防止水平滑动的时候失去选择" class="headerlink" title="防止水平滑动的时候失去选择"></a>防止水平滑动的时候失去选择</h2><p>如果你选中了一行或多行，那么你可以用 <kbd>&lt;</kbd> 或 <kbd>&gt;</kbd> 来调整他们的缩进。但在调整之后就不会保持选中状态了。</p><p>你可以连续按下 <kbd>g</kbd> <kbd>v</kbd> 来重新选中他们，请参考 <code>:h gv</code>。因此，你可以这样来配置映射：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">xnoremap</span> &lt;  &lt;<span class="keyword">gv</span></span><br><span class="line"><span class="keyword">xnoremap</span> &gt;  &gt;<span class="keyword">gv</span></span><br></pre></td></tr></table></figure><p>设置好之后，在可视模式中使用 <code>&gt;&gt;&gt;&gt;&gt;</code> 就不会再出现上面提到的问题了。</p><h2 id="重新载入保存文件"><a href="#重新载入保存文件" class="headerlink" title="重新载入保存文件"></a>重新载入保存文件</h2><p>通过<a href="#%E8%87%AA%E5%8A%A8%E5%91%BD%E4%BB%A4">自动命令</a>，你可以在保存文件的同时触发一些其他功能。比如，如果这个文件是一个配置文件，那么就重新载入；或者你还可以对这个文件进行代码风格检查。</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">autocmd</span> BufWritePost $MYVIMRC <span class="keyword">source</span> $MYVIMRC</span><br><span class="line"><span class="keyword">autocmd</span> BufWritePost ~/.Xdefaults <span class="keyword">call</span> <span class="built_in">system</span>(<span class="string">&#x27;xrdb ~/.Xdefaults&#x27;</span>)</span><br></pre></td></tr></table></figure><h2 id="更加智能的当前行高亮"><a href="#更加智能的当前行高亮" class="headerlink" title="更加智能的当前行高亮"></a>更加智能的当前行高亮</h2><p>我（原作者）很喜欢「当前行高亮」（请参阅 <code>:h cursorline</code>）这个功能，但我只想让这个效果出现在当前窗口，而且在插入模式中关闭这个效果：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">autocmd</span> InsertLeave,WinEnter * <span class="keyword">set</span> cursorline</span><br><span class="line"><span class="keyword">autocmd</span> InsertEnter,WinLeave * <span class="keyword">set</span> nocursorline</span><br></pre></td></tr></table></figure><h2 id="更快的关键字补全"><a href="#更快的关键字补全" class="headerlink" title="更快的关键字补全"></a>更快的关键字补全</h2><p>关键字补全（<code>&lt;c-n&gt;</code> 或 <code>&lt;c-p&gt;</code>）功能的工作方式是，无论 <code>&#39;complete&#39;</code> 设置中有什么，它都会尝试着去补全。这样，一些我们用不到的标签也会出现在补全列表中。而且，它会扫描很多文件，有时候运行起来非常慢。如果你不需要这些，那么完全可以像这样把它们禁用掉：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">set</span> <span class="built_in">complete</span>-=i   <span class="comment">&quot; disable scanning included files</span></span><br><span class="line"><span class="keyword">set</span> <span class="built_in">complete</span>-=t   <span class="comment">&quot; disable searching tags</span></span><br></pre></td></tr></table></figure><h2 id="改变颜色主题的默认外观"><a href="#改变颜色主题的默认外观" class="headerlink" title="改变颜色主题的默认外观"></a>改变颜色主题的默认外观</h2><p>如果你想让状态栏在颜色主题更改后依然保持灰色，那么只需要这样设置：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">autocmd</span> ColorScheme * <span class="keyword">highlight</span> StatusLine ctermbg=darkgray cterm=NONE guibg=darkgray <span class="keyword">gui</span>=NONE</span><br></pre></td></tr></table></figure><p>同理，如果你想让某一个颜色主题（比如 “lucius”）的状态栏为灰色（请使用 <code>:echo color_name</code> 来查看当前可用的所有颜色主题）：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">autocmd</span> ColorScheme lucius <span class="keyword">highlight</span> StatusLine ctermbg=darkgray cterm=NONE guibg=darkgray <span class="keyword">gui</span>=NONE</span><br></pre></td></tr></table></figure><h2 id="命令"><a href="#命令" class="headerlink" title="命令"></a>命令</h2><p>下面的命令都比较有用，最好了解一下。用 <code>:h :&lt;command name&gt;</code> 来了解更多关于它们的信息，如：<code>:h :global</code>。</p><h3 id="global-和-vglobal-在所有匹配行执行命令"><a href="#global-和-vglobal-在所有匹配行执行命令" class="headerlink" title=":global 和 :vglobal - 在所有匹配行执行命令"></a>:global 和 :vglobal - 在所有匹配行执行命令</h3><p>在所有符合条件的行上执行某个命令。如： <code>:global /regexp/ print</code> 会在所有包含 “regexp” 的行上执行 <code>print</code> 命令（译者注：regexp 有正则表达式的意思，该命令同样支持正则表达式，在所有符合正则表达式的行上执行指定的命令）。</p><p>趣闻：你们可能都知道老牌的 grep 命令，一个由 Ken Thompson 编写的过滤程序。它是干什么用的呢？它会输出所有匹配指定正则表达式的行！现在猜一下 <code>:global /regexp/ print</code> 的简写形式是什么？没错！就是 <code>:g/re/p</code> 。 Ken Thompsom 在编写 grep 程序的时候是受了 vi <code>:global</code> 的启发。（译者注： <a href="https://robots.thoughtbot.com/how-grep-got-its-name%EF%BC%89">https://robots.thoughtbot.com/how-grep-got-its-name）</a></p><p>既然它的名字是 <code>:global</code>，理应仅作用在所有行上，但是它也是可以带范围限制的。假设你想使用 <code>:delete</code> 命令删除从当前行到下一个空行（由正则表达式 <code>^$</code> 匹配）范围内所有包含 “foo” 的行：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:,/^$/g/foo/d</span><br></pre></td></tr></table></figure><p>如果要在所有 <em>不</em> 匹配的行上执行命令的话，可以使用 <code>:global!</code> 或是它的别名 <code>:vglobal</code> （ V 代表的是 inVerse ）。</p><h3 id="normal-和-execute-脚本梦之队"><a href="#normal-和-execute-脚本梦之队" class="headerlink" title=":normal 和 :execute - 脚本梦之队"></a>:normal 和 :execute - 脚本梦之队</h3><p>这两个命令经常在 Vim 的脚本里使用。</p><p>借助于 <code>:normal</code> 可以在命令行里进行普通模式的映射。如：<code>:normal! 4j</code> 会令光标下移 4 行（由于加了”!”，所以不会使用自定义的映射 “j”）。</p><p>需要注意的是 <code>:normal</code> 同样可以使用范围数（译者注：参考 <code>:h range</code> 和 <code>:h :normal-range</code> 了解更多），故 <code>:%norm! Iabc</code> 会在所有行前加上 “abc”。</p><p>借助于 <code>:execute</code> 可以将命令和表达式混合在一起使用。假设你正在编辑一个 C 语言的文件，想切换到它的头文件：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">execute</span> <span class="string">&#x27;edit&#x27;</span> <span class="built_in">fnamemodify</span>(<span class="built_in">expand</span>(<span class="string">&#x27;%&#x27;</span>), <span class="string">&#x27;:r&#x27;</span>) . <span class="string">&#x27;.h&#x27;</span></span><br></pre></td></tr></table></figure><p>（译者注：头文件为与与源文件同名但是扩展名为 <code>.h</code> 的文件。上面的命令中 expand 获得当前文件的名称，fnamemodify 获取不带扩展名的文件名，再连上 ‘.h’ 就是头文件的文件名了，最后在使用 edit 命令打开这个头文件。）</p><p>这两个命令经常一起使用。假设你想让光标下移 n 行：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">let</span> n = <span class="number">4</span></span><br><span class="line">:<span class="keyword">execute</span> <span class="string">&#x27;normal!&#x27;</span> n . <span class="string">&#x27;j&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="重定向消息"><a href="#重定向消息" class="headerlink" title="重定向消息"></a>重定向消息</h3><p>许多命令都会输出消息，<code>:redir</code> 用来重定向这些消息。它可以将消息输出到文件、<a href="#%E5%AF%84%E5%AD%98%E5%99%A8">寄存器</a>或是某个变量中。</p><figure class="highlight vim"><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><span class="line"><span class="comment">&quot; 将消息重定向到变量 `neatvar` 中</span></span><br><span class="line">:<span class="keyword">redir</span> =&gt; neatvar</span><br><span class="line"><span class="comment">&quot; 打印所有寄存器的内容</span></span><br><span class="line">:<span class="keyword">reg</span></span><br><span class="line"><span class="comment">&quot; 结束重定向</span></span><br><span class="line">:<span class="keyword">redir</span> END</span><br><span class="line"><span class="comment">&quot; 输出变量</span></span><br><span class="line">:<span class="keyword">echo</span> neatvar</span><br><span class="line"><span class="comment">&quot; 恶搞一下，我们把它输出到当前缓冲区</span></span><br><span class="line">:<span class="keyword">put</span> =neatvar</span><br></pre></td></tr></table></figure><p>再 Vim 8 中，可以更简单的方式即位：</p><pre><code>:put =execute(&#39;reg&#39;)</code></pre><p>（译者注：原文最后一条命令是 <code>:put =nicevar</code> 但是实际会报变量未定义的错误）<br>（实测 neovim&#x2F;vim8 下没问题）</p><p>帮助文档：<code>:h :redir</code></p><h1 id="调试"><a href="#调试" class="headerlink" title="调试"></a>调试</h1><h2 id="常规建议"><a href="#常规建议" class="headerlink" title="常规建议"></a>常规建议</h2><p>如果你遇到了奇怪的行为，尝试用这个命令重现它：</p><pre><code>vim -u NONE -N</code></pre><p>这样会在不引用 vimrc（默认设置）的情况下重启 vim，并且在 <strong>nocompatible</strong> 模式下（使用 vim 默认设置而不是 vi 的）。（搜索 <code>:h --noplugin</code> 命令了解更多启动加载方式）</p><p>如果仍旧能够出现该错误，那么这极有可能是 vim 本身的 bug，请给 <a href="%22https://groups.google.com/forum/#!forum/vim_dev%22">vim_dev</a> 发送邮件反馈错误，多数情况下问题不会立刻解决，你还需要进一步研究</p><p>许多插件经常会提供新的（默认的&#x2F;自动的）操作。如果在保存的时候发生了，那么请用 <code>:verb au BufWritePost</code> 命令检查潜在的问题</p><p>如果你在使用一个插件管理工具，将插件行注释调，再进行调试。</p><p>问题还没有解决？如果不是插件的问题，那么肯定是你的自定义的设置的问题，可能是你的 options 或 autocmd 等等。</p><p>到了一行行代码检查的时候了，不断地排除缩小检查范围知道你找出错误，根据二分法的原理你不会花费太多时间的。</p><p>在实践过程中，可能就是这样，把 <code>:finish</code> 放在你的 <strong>vimrc</strong> 文件中间，Vim 会跳过它之后的设置。如果问题还在，那么问题就出在<code>:finish</code>之前的设置中，再把<code>:finish</code>放到前一部分设置的中间位置。否则问题就出现在它后面的半部分设置，那么就把<code>:finish</code>放到后半部分的中间位置。不断的重复即可找到。</p><h2 id="调整日志等级"><a href="#调整日志等级" class="headerlink" title="调整日志等级"></a>调整日志等级</h2><p>Vim 现在正在使用的另一个比较有用的方法是增加 debug 信息输出详细等级。现在 Vim 支持 9 个等级，可以用<code>:h &#39;verbose&#39;</code>命令查看。</p><figure class="highlight vim"><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><span class="line">:<span class="keyword">e</span> /tmp/foo</span><br><span class="line">:<span class="keyword">set</span> <span class="keyword">verbose</span>=<span class="number">2</span></span><br><span class="line">:<span class="keyword">w</span></span><br><span class="line">:<span class="keyword">set</span> <span class="keyword">verbose</span>=<span class="number">0</span></span><br></pre></td></tr></table></figure><p>这可以显示出所有引用的文件、没有变化的文件或者各种各样的作用于保存的插件。</p><p>如果你只是想用简单的命令来提高等级，也是用 <code>:verbose</code> ，放在其他命令之前，通过计数来指明等级，默认是 1.</p><figure class="highlight vim"><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><span class="line">:<span class="keyword">verb</span> <span class="keyword">set</span> <span class="keyword">verbose</span></span><br><span class="line"><span class="comment">&quot;  verbose=1</span></span><br><span class="line">:<span class="number">10</span><span class="keyword">verb</span> <span class="keyword">set</span> <span class="keyword">verbose</span></span><br><span class="line"><span class="comment">&quot;  verbose=10</span></span><br></pre></td></tr></table></figure><p>通常用等级 1 来显示上次从哪里设置的选项</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">verb</span> <span class="keyword">set</span> ai?</span><br><span class="line"><span class="comment">&quot;      Last set from ~/.vim/vimrc</span></span><br></pre></td></tr></table></figure><p>一般等级越高输出信息月详细。但是不要害怕，亦可以把输出导入到文件中：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">set</span> verbosefile=/tmp/foo | <span class="number">15</span><span class="keyword">verbose</span> <span class="keyword">echo</span> <span class="string">&quot;foo&quot;</span> | <span class="keyword">vsplit</span> /tmp/foo</span><br></pre></td></tr></table></figure><p>你可以一开始的时候就打开 verbosity，用 <code>-V</code> 选项，它默认设置调试等级为 10。 例如：<code>vim -V5</code></p><h2 id="查看启动日志"><a href="#查看启动日志" class="headerlink" title="查看启动日志"></a>查看启动日志</h2><h2 id="查看运行时日志"><a href="#查看运行时日志" class="headerlink" title="查看运行时日志"></a>查看运行时日志</h2><h2 id="Vim-脚本调试"><a href="#Vim-脚本调试" class="headerlink" title="Vim 脚本调试"></a>Vim 脚本调试</h2><p>如果你以前使用过命令行调试器的话，对于<code>:debug</code>命令你很快就会感到熟悉。</p><p>只需要在任何其他命令之前加上<code>:debug</code>就会让你进入调试模式。也就是，被调试的 Vim 脚本会在第一行停止运行，同时该行会被显示出来。</p><p>想了解可用的 6 个调试命令，可以查阅<code>:h &gt;cont</code>和阅读下面内容。需要指出的是，类似 gdb 和其他相似调试器，调试命令可以使用它们的简短形式：<code>c</code>、 <code>q</code>、<code>n</code>、<code>s</code>、 <code>i</code>和 <code>f</code>。</p><p>除了上面的之外，你还可以自由地使用任何 Vim 的命令。比如，<code>:echo myvar</code>，该命令会在当前的脚本代码位置和上下文上被执行。</p><p>只需要简单使用<code>:debug 1</code>，你就获得了<a href="https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop">REPL</a>调试特性。</p><p>当然，调试模式下是可以定义断点的，不然的话每一行都去单步调试就会十分痛苦。（断点之所以被叫做断点，是因为运行到它们的时候，运行就会停止下来。因此，你可以利用断点跳过自己不感兴趣的代码区域）。请查阅<code>:h :breakadd</code>、 <code>:h :breakdel</code>和 <code>:h :breaklist</code>获取更多细节。</p><p>假设你需要知道你每次在保存一个文件的时候有哪些代码在运行：</p><figure class="highlight vim"><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></pre></td><td class="code"><pre><span class="line">:<span class="keyword">au</span> BufWritePost</span><br><span class="line"><span class="comment">&quot; signify  BufWritePost</span></span><br><span class="line"><span class="comment">&quot;     *         call sy#start()</span></span><br><span class="line">:<span class="keyword">breakadd</span> func *start</span><br><span class="line">:<span class="keyword">w</span></span><br><span class="line"><span class="string">&quot; Breakpoint in &quot;</span>sy#start<span class="comment">&quot; line 1</span></span><br><span class="line"><span class="string">&quot; Entering Debug mode.  Type &quot;</span>cont<span class="comment">&quot; to continue.</span></span><br><span class="line"><span class="comment">&quot; function sy#start</span></span><br><span class="line"><span class="comment">&quot; line 1: if g:signify_locked</span></span><br><span class="line">&gt;s</span><br><span class="line"><span class="comment">&quot; function sy#start</span></span><br><span class="line"><span class="comment">&quot; line 3: endif</span></span><br><span class="line">&gt;</span><br><span class="line"><span class="comment">&quot; function sy#start</span></span><br><span class="line"><span class="comment">&quot; line 5: let sy_path = resolve(expand(&#x27;%:p&#x27;))</span></span><br><span class="line">&gt;q</span><br><span class="line">:<span class="keyword">breakdel</span> *</span><br></pre></td></tr></table></figure><p>正如你所见，使用<code>&lt;cr&gt;</code>命令会重复之前的调试命令，也就是在该例子中的<code>s</code>命令。</p><p><code>:debug</code>命令可以和<a href="#verbosity">verbose</a>选项一起使用。</p><h2 id="语法文件调试"><a href="#语法文件调试" class="headerlink" title="语法文件调试"></a>语法文件调试</h2><p>语法文件由于包含错误的或者复制的正则表达式，常常会使得 Vim 的运行较慢。如果 Vim 在编译的时候包含了<code>+profile</code> <a href="#what-kind-of-vim-am-i-running">feature</a>特性，就可以给用户提供一个超级好用的<code>:syntime</code>命令。</p><figure class="highlight vim"><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><span class="line">:<span class="keyword">syntime</span> <span class="keyword">on</span></span><br><span class="line"><span class="comment">&quot; 多次敲击&lt;c-l&gt;来重绘窗口，这样的话就会使得相应的语法规则被重新应用一次</span></span><br><span class="line">:<span class="keyword">syntime</span> off</span><br><span class="line">:<span class="keyword">syntime</span> report</span><br></pre></td></tr></table></figure><p>输出结果包含了很多的度量维度。比如，你可以通过结果知道哪些正则表达式耗时太久需要被优化；哪些正则表达式一直在别使用但重来没有一次成功匹配。</p><p>请查阅<code>:h :syntime</code>。</p><h1 id="杂项"><a href="#杂项" class="headerlink" title="杂项"></a>杂项</h1><h2 id="附加资源"><a href="#附加资源" class="headerlink" title="附加资源"></a>附加资源</h2><table><thead><tr><th>资源名称</th><th>简介</th></tr></thead><tbody><tr><td><a href="http://www.moolenaar.net/habits.html">七个高效的文本编辑习惯</a></td><td>作者：Bram Moolenaar（即 Vim 的作者）</td></tr><tr><td><a href="http://www.moolenaar.net/habits_2007.pdf">七个高效的文本编辑习惯 2.0（PDF 版）</a></td><td>同上</td></tr><tr><td><a href="http://www.ibm.com/developerworks/views/linux/libraryview.jsp?sort_order=asc&sort_by=Title&search_by=scripting+the+vim+editor">IBM DeveloperWorks: 使用脚本编写 Vim 编辑器</a></td><td>Vim 脚本编写五辑</td></tr><tr><td><a href="http://learnvimscriptthehardway.stevelosh.com/">《漫漫 Vim 路》</a></td><td>使用魔抓定制 Vim 插件</td></tr><tr><td><a href="http://www.amazon.com/Practical-Vim-Edit-Speed-Thought/dp/1680501275/">《 Vim 实践 (第 2 版)》</a></td><td>轻取 Vim 最佳书籍</td></tr><tr><td><a href="http://vimcasts.org/episodes/archive">Vimcasts.org</a></td><td>Vim 录屏演示</td></tr><tr><td><a href="http://www.viemu.com/a-why-vi-vim.html">为什么是个脚本都用 vi？</a></td><td>常见误区释疑</td></tr><tr><td><a href="http://stackoverflow.com/a/1220118">你不爱 vi，所以你不懂 Vim </a></td><td>简明,扼要,准确的干货</td></tr></tbody></table><h2 id="Vim-配置集合"><a href="#Vim-配置集合" class="headerlink" title="Vim 配置集合"></a>Vim 配置集合</h2><p>目前，网上有很多流行 Vim 配置集合，对于 Vim 配置集合，个人认为有利有弊。<br>对于维护的比较好的配置，比如 <a href="http://spacevim.org/cn/">SpaceVim</a> 还是值得尝试的，可以节省很多自行配置的时间。<br>当然，网上还有很多其他很流行的配置，比如：</p><ul><li><a href="https://github.com/wklken/k-vim">k-vim</a></li><li><a href="https://github.com/amix/vimrc">amix’s vimrc</a></li><li><a href="https://github.com/carlhuda/janus">janus</a></li></ul><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="编辑小文件时很慢"><a href="#编辑小文件时很慢" class="headerlink" title="编辑小文件时很慢"></a>编辑小文件时很慢</h3><p>有两个因素对性能影响非常大：</p><ol><li><p>过于复杂的 <strong>正则表达式</strong> 。尤其是 Ruby 的语法文件，以前会造成性能下降。（见<a href="#debugging-syntax-files">调试语法文件</a>）</p></li><li><p><strong>屏幕重绘</strong> 。有一些功能会强制重绘所有行。</p></li></ol><table><thead><tr><th>典型肇事者</th><th>原因</th><th>解决方案</th></tr></thead><tbody><tr><td><code>:set cursorline</code></td><td>会导致所有行重绘</td><td><code>:set nocursorline</code></td></tr><tr><td><code>:set cursorcolumn</code></td><td>会导致所有行重绘</td><td><code>:set nocursorcolumn</code></td></tr><tr><td><code>:set relativenumber</code></td><td>会导致所有行重绘</td><td><code>:set norelativenumber</code></td></tr><tr><td><code>:set foldmethod=syntax</code></td><td>如果语法文件已经很慢了，这只会变得更慢</td><td><code>:set foldmethod=manual</code>，<code>:set foldmethod=marker</code> 或者使用<a href="https://github.com/Konfekt/FastFold">快速折叠</a>插件</td></tr><tr><td><code>:set synmaxcol=3000</code></td><td>由于内部表示法，Vim 处理比较长的行时会有问题。让它高亮到 3000 列……</td><td><code>:set synmaxcol=200</code></td></tr><tr><td>matchparen.vim</td><td>Vim 默认加载的插件，用正则表达式查找配对的括号</td><td>禁用插件：<code>:h matchparen</code></td></tr></tbody></table><p><strong>注意</strong>：只有在你真正遇到性能问题的时候才需要做上面的调整。在大多数情况下使用上面提到的选项是完全没有问题的。</p><h3 id="编辑大文件的时候很慢"><a href="#编辑大文件的时候很慢" class="headerlink" title="编辑大文件的时候很慢"></a>编辑大文件的时候很慢</h3><p>Vim 处理大文件最大的问题就是它会一次性读取整个文件。这么做是由于缓冲区的内部机理导致的（在 <a href="https://groups.google.com/forum/#!topic/vim_dev/oY3i8rqYGD4/discussion">vim_dev</a> 中讨论）。</p><p>如果只是想查看的话，<code>tail hugefile | vim -</code> 是一个不错的选择。</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><span class="line">$ vim -u NONE -N</span><br></pre></td></tr></table></figure><p>这将会使得跳转变快很多，尤其是省去了基于很耗费资源的正则表达式的语法高亮。你还可以告诉 Vim 不要使用交换文件和 viminfo 文件，以避免由于写这些文件而造成的延时：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ vim -n -u NONE -i NONE -N</span><br></pre></td></tr></table></figure><p>简而言之，尽量避免使用 Vim 写过大的文件。</p><h3 id="持续粘贴（为什么我每次都要设置-‘paste’-模式）"><a href="#持续粘贴（为什么我每次都要设置-‘paste’-模式）" class="headerlink" title="持续粘贴（为什么我每次都要设置 ‘paste’ 模式）"></a>持续粘贴（为什么我每次都要设置 ‘paste’ 模式）</h3><p>持续粘贴模式让终端模拟器可以区分输入内容与粘贴内容。</p><p>你有没有遇到过往 Vim 里粘贴代码之后被搞的一团糟？</p><p>这在你使用 <code>cmd+v</code>、<code>shirt-insert</code>、<code>middle-click</code> 等进行粘贴的时候才会发生。<br>因为那样的话你只是向终端模拟器扔了一大堆的文本。<br>Vim 并不知道你刚刚是粘贴的文本，它以为你在飞速的输入。<br>于是它想缩进这些行但是失败了。</p><p>这明显不是个问题，如果你用 Vim 的寄存器粘贴，如：<code>&quot;+p</code> ，这时 Vim 就知道了你在粘贴，就不会导致格式错乱了。</p><p>使用 <code>:set paste</code> 就可以解决这个问题正常进行粘贴。见 <code>:h &#39;paste&#39;</code> 和 <code>:h &#39;pastetoggle&#39;</code> 获取更多信息。</p><p>如果你受够了每次都要设置 <code>&#39;paste&#39;</code> 的话，看看这个能帮你自动设置的插件：<a href="https://github.com/ConradIrwin/vim-bracketed-paste">bracketed-paste</a>。</p><p><a href="http://cirw.in/blog/bracketed-paste">点此</a>查看该作者对于这个插件的更多描述。</p><p>Neovim 尝试把这些变得更顺畅，如果终端支持的话，它会自动开启持续粘贴模式，无须再手动进行切换。</p><h3 id="在终端中按-ESC-后有延时"><a href="#在终端中按-ESC-后有延时" class="headerlink" title="在终端中按 ESC 后有延时"></a>在终端中按 ESC 后有延时</h3><p>如果你经常使用命令行，那么肯定要接触 <em>终端模拟器</em> ，如 xterm、gnome-terminal、iTerm2 等等（与实际的<a href="https://en.wikipedia.org/wiki/Computer_terminal">终端</a>不同）。</p><p>终端模拟器与他们的祖辈一样，使用 <a href="https://zh.wikipedia.org/wiki/%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97">转义序列</a> （也叫 <em>控制序列</em> ）来控制光标移动、改变文本颜色等。转义序列就是以转义字符开头的 ASCII 字符串（用<a href="https://zh.wikipedia.org/wiki/%E8%84%B1%E5%AD%97%E7%AC%A6%E8%A1%A8%E7%A4%BA%E6%B3%95">脱字符表示法</a>表示成 <code>^[</code> ）。当遇到这样的字符串后，终端模拟器会从<a href="https://en.wikipedia.org/wiki/Terminfo">终端信息</a>数据库中查找对应的动作。</p><p>为了使用问题更加清晰，我会先来解释一下什么是映射超时。在映射存在歧义的时候就会产生映射超时：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">nnoremap</span> ,<span class="keyword">a</span> :<span class="keyword">echo</span> <span class="string">&#x27;foo&#x27;</span><span class="symbol">&lt;cr&gt;</span></span><br><span class="line">:<span class="keyword">nnoremap</span> ,<span class="keyword">ab</span> :<span class="keyword">echo</span> <span class="string">&#x27;bar&#x27;</span><span class="symbol">&lt;cr&gt;</span></span><br></pre></td></tr></table></figure><p>上面的例子中两个映射都能正常工作，但是当输入 <code>,a</code> 之后，Vim 会延时 1 秒，因为它要确认用户是否还要输入那个 <code>b</code>。</p><p>转义序列会产生同样的问题：</p><ul><li><code>&lt;esc&gt;</code> 作为返回普通模式或取消某个动作的按键而被大量使用</li><li>光标键使用转义序列进行的编码</li><li>Vim 期望 <kbd>Alt</kbd> （也叫作 <em>Mate Key</em> ）会发送一个正确的 8-bit 编码的高位，但是许多终端模拟器并不支持这个（也可能默认没有启用），而只是发送一个转义序列作为代替。</li></ul><p>你可以这样测试上面所提到的事情： <code>vim -u NONE -N</code> 然后输入 <code>i&lt;c-v&gt;&lt;left&gt;</code> ，你会看到一个以 <code>^[</code> 开头的字符串，表明这是一个转义序列，<code>^[</code> 就是转义字符。</p><p>简而言之，Vim 在区分录入的 <code>&lt;esc&gt;</code> 和转义序列的时候需要一定的时间。</p><p>默认情况下，Vim 用 <code>:set timeout timeoutlen=1000</code>，就是说它会用 1 秒的时间来区分有歧义的映射 <em>以及</em> 按键编码。这对于映射来说是一个比较合理的值，但是你可以自行定义按键延时的长短，这是解决该问题最根本的办法：</p><figure class="highlight vim"><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><span class="line"><span class="keyword">set</span> timeout           <span class="comment">&quot; for mappings</span></span><br><span class="line"><span class="keyword">set</span> timeoutlen=<span class="number">1000</span>   <span class="comment">&quot; default value</span></span><br><span class="line"><span class="keyword">set</span> ttimeout          <span class="comment">&quot; for key codes</span></span><br><span class="line"><span class="keyword">set</span> ttimeoutlen=<span class="number">10</span>    <span class="comment">&quot; unnoticeable small value</span></span><br></pre></td></tr></table></figure><p>在 <code>:h ttimeout</code> 里你可以找到一个关于这些选项之间关系的小表格。</p><p>而如果你在 tmux 中使用 Vim 的话，别忘了把下面的配置加入到你的 <code>~/.tmux.conf</code>文件中：</p><pre><code>set -sg escape-time 0</code></pre><h3 id="无法重复函数中执行的搜索"><a href="#无法重复函数中执行的搜索" class="headerlink" title="无法重复函数中执行的搜索"></a>无法重复函数中执行的搜索</h3><ul><li>在命令中的搜索（<code>/</code>、<code>:substitute</code> 等）内容会改变“上次使用的搜索内容”。（它保存在<code>/</code>寄存器中，用 <code>:echo @/</code> 可以输出它里面的内容）</li><li>简单的文本变化可以通过 <code>.</code> 重做。（它保存在 <code>.</code> 寄存器，用 <code>:echo @.</code> 可以输出它的内容）</li></ul><p>而在你在函数中进行这些操作的时候，一切就会变得不同。因此你不能用 N&#x2F;n 查找某个函数刚刚查找的内容，也不能重做函数中对文本的修改。</p><p>帮助文档：<code>:h function-search-undo</code>。</p><h2 id="进阶阅读"><a href="#进阶阅读" class="headerlink" title="进阶阅读"></a>进阶阅读</h2><ul><li><a href="https://github.com/wsdjeg/vim-plugin-dev-guide">Vim 插件开发指南</a></li><li><a href="PLUGINS.md">常用插件列表</a></li></ul><h2 id="加入我们"><a href="#加入我们" class="headerlink" title="加入我们"></a>加入我们</h2><p>可以协助我们核对翻译，或者从<a href="CONTRIBUTING.md">章节列表</a>中认领章节进行翻译。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;Vim-从入门到精通&quot;&gt;&lt;a href=&quot;#Vim-从入门到精通&quot; class=&quot;headerlink&quot; title=&quot;Vim 从入门到精通&quot;&gt;&lt;/a&gt;Vim 从入门到精通&lt;/h1&gt;&lt;blockquote&gt;
&lt;p&gt;本文主要在翻译 &lt;a href=&quot;https://</summary>
      
    
    
    
    <category term="转载" scheme="https://blog.jugg.xyz/categories/%E8%BD%AC%E8%BD%BD/"/>
    
    
    <category term="Vim" scheme="https://blog.jugg.xyz/tags/Vim/"/>
    
  </entry>
  
</feed>
