<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Farmer</title>
  <icon>https://blog.farmer233.top/favicon.png</icon>
  <subtitle>-Farmer的自习室</subtitle>
  <link href="https://blog.farmer233.top/atom.xml" rel="self"/>
  
  <link href="https://blog.farmer233.top/"/>
  <updated>2026-04-22T08:30:03.572Z</updated>
  <id>https://blog.farmer233.top/</id>
  
  <author>
    <name>-Farmer</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>上海行 —— PyCon China 2025</title>
    <link href="https://blog.farmer233.top/2025/09/23/pycon-2025/"/>
    <id>https://blog.farmer233.top/2025/09/23/pycon-2025/</id>
    <published>2025-09-22T16:48:18.000Z</published>
    <updated>2026-04-22T08:30:03.572Z</updated>
    
    <content type="html"><![CDATA[<p>故事要从6年前说起，那是我第一次接触到 PyCon，还记得那年大会十分的盛大，在全国各地都设有分会场（很可惜广州没有，不愧是广寒宫 :p），甚至连 Luciano Ramalho 也来参加，但以学生微薄的收入显然不支持我到现场参加。</p><p>后面出来工作后的几次 PyCon 都有着或多或少的原因没有参加。今年孤身一人的我好像也没有什么不参加的理由了，我毅然的在论坛上点击了「参与」的投票</p><h2 id="扬帆起航"><a href="#扬帆起航" class="headerlink" title="扬帆起航"></a>扬帆起航</h2><p>很荣幸这次收到 Grey 的邀请在代码厨房的分会场上介绍一下 APIFlask，思前想后发觉自己已经3年没有参与这个项目了，让我来介绍它自觉难以胜任、有失偏颇。</p><p>我更想从轻松、有意思的角度来切入，讲讲我与 APIFlask 的相遇、相识、相知，再讲讲怎么在工作中的落地，从而引申出 APIFlask 的一些特性。</p><p>用一种聊天的方式而不是分享的方式来讲述，为此我最后将主题改为「我与 APIFlask 的故事」</p><p>由于身在职场中，不可避免的存在雇主与雇员的关系，尽管我以个人身份参加。为此我还需要在公司内部审批报备，我为了让这件事看上去比较“正规”，邮件的标题我还使用了《关于受邀在PyCon China 2025大会进行技术分享的参会报备申请​》 :p</p><p>所幸各级领导都挺支持与赞同我这次的行程，让我感受到公司还是很鼓励员工去参与各种大会、分享。</p><h2 id="大乡里进城"><a href="#大乡里进城" class="headerlink" title="大乡里进城"></a>大乡里进城</h2><p>本着来都来了的想法，我提前一天便早早到达上海。<del>可能是</del>（就是）不用上班的缘故，这天我居然没有赖床，开开心心的洗漱完出发去机场。还记得那天和朋友说，路上的空气都是甜的。</p><p>由于预算的问题我这次飞的是浦东机场，由于见识浅薄错过了磁悬浮，坐的市域铁进市区。后面听朋友说这是全球唯一的正在运营的磁悬浮，有点可惜但也无妨，又有去上海的理由了,不是吗？</p><p>晚上我和朋友约在了外滩的一家餐厅碰头，吃的是云南菜，让我想到了最近热议的云贵川 bistro 与上海主理人，我同事甚至打趣说我们是代码数据搬运主理人 :p</p><p>饭后我们坐观光车绕着外滩兜了一圈，我们发觉这个线路不太对劲，没什么东西好看的，好在我们快睡着的时候回到了起点。这天我打卡了上海保卫塔，和平饭店、永安百货等著名的地点。由于第二天的行程比较重要，我们早早的结束了这天的行程，回旅店休息了。</p><h2 id="PyCon-China-与美丽的邂逅"><a href="#PyCon-China-与美丽的邂逅" class="headerlink" title="PyCon China 与美丽的邂逅"></a>PyCon China 与美丽的邂逅</h2><p>或许是水土不服的缘故，早上起来肚子不舒服导致出门的时间耽误了，但所幸没有错过什么重要的环节。</p><p>最激动的终于能见到神秘的 grey 了，在我和 grey 碰头没多久，厨委会的其他人也到了，大型的网友面基现场。</p><p>苏雄和高佳在策划代码厨房第一期的播客，邀请的嘉宾如无意外就是 grey 了，看着他们神采奕奕的讨论着如何策划，刹那间我仿佛回到了大学时和几个朋友做公众号时的场景，真是令人怀念。猛然间发现自己这三年好像失去了什么东西，那颗初心仿佛蒙上了厚重的灰尘，他们的讨论犹如一把鸡毛掸子扫去那厚重的尘埃，在此由衷的感谢他们，让我找回了初心。</p><p>下午的分享如期而至，我排在了最后一个讲，前面几个项目的分享都十分的有趣，让我记忆深刻的是 yihong 老师做开源的收益是负200刀 XD，当然 yihong 的演讲非常棒，是我梦想中的演讲</p><p>第一次上台做分享，虽然做足了准备但还是免不了紧张，但后面回过头来看还是有不少东西没有讲到。不过人无完人，没什么事情是能尽善尽美的，也是一次不错的体验与经历。当然也十分开心的解锁了新成就：<strong>在 PyCon 大会做分享</strong></p><p>随着傍晚的到来，大会也落下帷幕，今天收获颇丰，除了干货满满的知识，还认识的了很多有意思的人。</p><!-- 原本是计划着参加讲师晚宴的，但考虑到上海还有很多地方没有去，以及一些不能明状的理由，我毅然的放弃了讲师晚宴去和刚认识的朋友一起游览上海的繁华。过去的我还未曾体验过邂逅是何种滋味，但是在今天我体验到了。在参加完 PyCon 大会后我们一起去吃了晚饭、一起去逛了上海著名的一些景点与夜景、最后去找了一家别具风味的路边酒摊小酌一杯。在寻找酒馆的时候，不知道是他记错了还是故意的，我们走错了三个地方，一路上有说不完的话，甚至忘记了时间的流逝，不知不觉间逛了3个多小时，最后我看他嗓子都讲冒烟了，不由得一笑，便沿途找了家便利店先买瓶水解渴。当然也加快了寻找酒馆的步伐，最后我们在酒摊又畅聊了到一点，打起了哈欠才散场离去。诚然，我确实有好感，我在去年分手后到现在从未有过如此强烈的心动，或许是我的性格使然，也或许是胆怯、自卑、羞耻，最后还是没有将自己的好感表达出来。当然我也很清楚的明白两个人之间有着天然的物理距离，此去一别可能再无见面的可能，会十分的可惜与遗憾，因此在分道扬镳各自上出租车后，我还是鼓起勇气和他约了第二天的饭，想着大家都是差不都的时间从虹桥出发离开上海，很快他便欣然的答应了我的约饭，我打心底的感到开心。## 最后的挣扎与胆小的懦夫最后一天起床明显感觉到了疲惫，感叹自己不再年轻之余也还是收拾出发了。因为我答应了我朋友要帮他去看看**武康大楼**，还要买点手信给我的朋友们。当然更重要的是还要赶着时间去赴约，毕竟迟到是不好的。来到武康大楼发现人头涌动，好不容易挤到了标志性的打卡位，草草拍了几张照片便去寻找「大隐书局」（一家文创店）购买手信了，一进门便看到了我朋友心心念的冰箱贴，本想拿了就结帐离开，但余光撇见一个**宛平南路600号**的冰箱贴十分有意思，不自觉的停下了脚步仔细挑选了起来，因为我知道我那个读心理的朋友对此会十分感兴趣。结束了武康路的行程，我便飞快的奔赴下一 part我们相约在一家虹桥附近的商场吃午饭，由于上一行程的耽误使我预感要迟到，正当我要拿起手机向他致歉时，发现他也正好和我说也要晚一些才能到，真是凑巧。午饭的聊天中我发现他有着丰富的阅历与对事物有着自己的见解，我觉得在同质化严重的当下这种独立的思考尤为难得。快乐的时间总是飞逝的，不知不觉间又到了要分道扬镳的时间，我们提前了十来分钟到打车的地方准备买杯喝的等待，本来我想着请他喝一杯当作这两天的感谢，毕竟我提出想请他吃饭却被他义正严辞的拒绝了，那请他喝杯东西总不为过吧？我如是想到。无奈天公不允，我们点餐后发现居然要等50分钟才能取餐，无奈之下只能作罢。最后的最后，我鼓起勇气问了他一个问题但他才说开头，车便到了，没能听到他的答案很是遗憾，我最后没能将我的好感说出口也是。或许此去我们再无缘相见，连好感都不表达，那未免也太遗憾了。有时候做一个理智清醒冷静的成年人，好像也未必是件好事。回到广州朋友接机，我激动的和他们分享了这趟美好的路程。我朋友说我这是一场艳遇，但我自觉连好感的表达都没有又怎配的上艳遇这个词，只作是一场美丽的邂逅吧。当然，也或许是艳火吧。 -->]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;故事要从6年前说起，那是我第一次接触到 PyCon，还记得那年大会十分的盛大，在全国各地都设有分会场（很可惜广州没有，不愧是广寒宫 :p），甚至连 Luciano Ramalho 也来参加，但以学生微薄的收入显然不支持我到现场参加。&lt;/p&gt;
&lt;p&gt;后面出来工作后的几次 Py</summary>
      
    
    
    
    <category term="胡思乱语" scheme="https://blog.farmer233.top/categories/%E8%83%A1%E6%80%9D%E4%B9%B1%E8%AF%AD/"/>
    
    
    <category term="胡言乱语" scheme="https://blog.farmer233.top/tags/%E8%83%A1%E8%A8%80%E4%B9%B1%E8%AF%AD/"/>
    
  </entry>
  
  <entry>
    <title>九月</title>
    <link href="https://blog.farmer233.top/2024/09/01/%E4%B9%9D%E6%9C%88/"/>
    <id>https://blog.farmer233.top/2024/09/01/%E4%B9%9D%E6%9C%88/</id>
    <published>2024-09-01T09:40:53.000Z</published>
    <updated>2026-04-22T08:30:03.574Z</updated>
    
    <content type="html"><![CDATA[<p>今天是星期天，是九月的第一天，亦是秋天的第一天。</p><p>秋天的到来，总是伴随着一种特殊的情感，那种既温柔又略带忧伤的感觉，在每一个秋日的清晨和黄昏里悄然蔓延。秋天是一个让人沉思的季节，树叶渐渐变黄，微风轻拂，带来丝丝凉意，仿佛在提醒我们时间的流逝和生命的无常。</p><span id="more"></span><p>秋天是一年中我最喜欢的季节，清爽、静逸是我对它的标签。我很喜欢走在铺满落叶的小路上，脚下发出轻微的沙沙声让人感觉到很舒服。</p><p>秋意浓时，总是容易引发人们对过去的追忆和对未来的思考，也忽然记起两年前写过的<a href="https://blog.farmer233.top/2022/03/15/%E7%94%9F%EF%BC%8C%E6%B4%BB/">生, 活</a>，在人生的每个十字路上貌似有很多选择但好像又没有选择。</p><p>秋意浓时同样也是寒冬的前兆，过去的三年，我总不是孤身一人，总有那份温情伴我左右，陪我走过那些寒冷的夜晚。然而，今年的秋天，似乎格外的萧瑟。那曾在身旁的身影，如今都成了回忆，渐行渐远，日渐模糊。</p><p>听闻，怀念很疼不忍触碰，于是，我佯装落笔轻松，想写一写黄昏的风，再写一写入夜的梦，怎奈，文字的轻柔却又格外沉重，这风一吹，总有当初的心动，而梦里，也总有回不去的曾经。</p><p>但我仍必须疾步前行，逝去的，莫再想，路正崎岖更漫长。宛如悉达多离开乔达摩，乔达文皈依佛门。正如悉达多所感：世界隐匿于周围，如同天际孤星，苏醒的最后颤栗，分娩的最后痉挛。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;今天是星期天，是九月的第一天，亦是秋天的第一天。&lt;/p&gt;
&lt;p&gt;秋天的到来，总是伴随着一种特殊的情感，那种既温柔又略带忧伤的感觉，在每一个秋日的清晨和黄昏里悄然蔓延。秋天是一个让人沉思的季节，树叶渐渐变黄，微风轻拂，带来丝丝凉意，仿佛在提醒我们时间的流逝和生命的无常。&lt;/p&gt;</summary>
    
    
    
    <category term="胡思乱语" scheme="https://blog.farmer233.top/categories/%E8%83%A1%E6%80%9D%E4%B9%B1%E8%AF%AD/"/>
    
    
    <category term="胡言乱语" scheme="https://blog.farmer233.top/tags/%E8%83%A1%E8%A8%80%E4%B9%B1%E8%AF%AD/"/>
    
    <category term="emotion" scheme="https://blog.farmer233.top/tags/emotion/"/>
    
  </entry>
  
  <entry>
    <title>《OSTEP》学习笔记（十）常见并发问题</title>
    <link href="https://blog.farmer233.top/2024/01/01/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%8D%81%EF%BC%89%E5%B8%B8%E8%A7%81%E5%B9%B6%E5%8F%91%E9%97%AE%E9%A2%98/"/>
    <id>https://blog.farmer233.top/2024/01/01/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%8D%81%EF%BC%89%E5%B8%B8%E8%A7%81%E5%B9%B6%E5%8F%91%E9%97%AE%E9%A2%98/</id>
    <published>2024-01-01T03:00:13.000Z</published>
    <updated>2026-04-22T08:30:03.573Z</updated>
    
    <content type="html"><![CDATA[<p>从下面的统计中我们可以看出，常见的并发问题可以分为：「非死锁的缺陷」与「死锁缺陷」</p><img src="/2024/01/01/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%8D%81%EF%BC%89%E5%B8%B8%E8%A7%81%E5%B9%B6%E5%8F%91%E9%97%AE%E9%A2%98/info.png" class="" title="info"><span id="more"></span><h2 id="非死锁缺陷"><a href="#非死锁缺陷" class="headerlink" title="非死锁缺陷"></a>非死锁缺陷</h2><p>非死锁缺陷主要可以概括为以下几个：</p><ul><li>违反原子性</li></ul><p>「违反原子性」是指违反了多次内存访问中预期的<strong>可串行性</strong>，也就是说临界区代码没有被锁保护起来（即代码段本意是原子的，但在执行中并没有强制实现原子性）。如下面的代码，解决方法也就是加锁:</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></pre></td><td class="code"><pre><span class="line">Thread <span class="number">1</span>::</span><br><span class="line">    <span class="comment">// 判断和赋值应该是原子性的</span></span><br><span class="line">    <span class="keyword">if</span> (thd-&gt;proc_info)</span><br><span class="line">    &#123;</span><br><span class="line">        ...</span><br><span class="line">        <span class="built_in">fputs</span>(thd-&gt;proc_info, ...);</span><br><span class="line">        ...</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">Thread <span class="number">2</span>::</span><br><span class="line">    thd-&gt;proc_info = <span class="literal">NULL</span>;</span><br></pre></td></tr></table></figure><ul><li>违反顺序性</li></ul><p>两个内存访问的<strong>预期顺序</strong>被打破了（即 A 应该在 B 之前执行，但是实际运行中却不是这个顺序）比如下面的这段代码中，如果 mState &#x3D; mThread-&gt;State 语句先执行，则 mThread 为空。可以通过加<strong>条件变量</strong>(或信号量)解决：</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></pre></td><td class="code"><pre><span class="line">Thread <span class="number">1</span>::</span><br><span class="line">    <span class="type">void</span> <span class="title function_">init</span><span class="params">()</span></span><br><span class="line">    &#123;</span><br><span class="line">        ...</span><br><span class="line">        mThread = PR_CreateThread(mMain, ...);</span><br><span class="line">        ...</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">Thread <span class="number">2</span>::</span><br><span class="line">    <span class="type">void</span> <span class="title function_">mMain</span><span class="params">(...)</span></span><br><span class="line">    &#123;</span><br><span class="line">        ...</span><br><span class="line">        mState = mThread-&gt;State;</span><br><span class="line">        ...</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h2 id="死锁缺陷"><a href="#死锁缺陷" class="headerlink" title="死锁缺陷"></a>死锁缺陷</h2><p>除了上面的缺陷外，死锁（deadlock）死一种并发系统中经典的问题。比如下面的代码就可能会出现死锁：</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">Thread <span class="number">1</span>:</span><br><span class="line">lock(L1);</span><br><span class="line">lock(L2);</span><br><span class="line"></span><br><span class="line">Thread <span class="number">2</span>:</span><br><span class="line">lock(L2);</span><br><span class="line">lock(L1);</span><br></pre></td></tr></table></figure><h3 id="为什么发生死锁"><a href="#为什么发生死锁" class="headerlink" title="为什么发生死锁"></a>为什么发生死锁</h3><ul><li>复杂的依赖：以操作系统为例。虚拟内存系统依赖文件系统才能从磁盘读到内存页；<code>文件系统</code>依赖<code>虚拟内存系统</code>申请一页内存，以便存放读到的块。</li><li>封装：软件开发者一直倾向于隐藏实现细节，以模块化的方式让软件开发更容易。然而，模块化和锁不是很契合，以 Java 的 Vector 类和 AddAll()方法为例，我们这样调用这个方法：</li></ul><figure class="highlight java"><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">Vector v1, v2;</span><br><span class="line">v1.AddAll(v2);</span><br></pre></td></tr></table></figure><p>在内部，这个方法需要多线程安全，因此针对被添加向量（v1）和参数（v2）的锁都需要获取。假设这个方法，先给 v1 加锁，然后再给 v2 加锁。如果另外某个线程几乎同时在调用 v2.AddAll(v1)，就可能遇到死锁。</p><h3 id="产生死锁的条件"><a href="#产生死锁的条件" class="headerlink" title="产生死锁的条件"></a>产生死锁的条件</h3><ul><li>互斥：线程对于需要的资源进行互斥的访问（例如一个线程抢到锁）</li><li>持有并等待：线程持有了资源（例如已将持有的锁），同时又在等待其他资源（例如，需要获得的锁）</li><li>非抢占：线程获得的资源（例如锁），不能被抢占</li><li>循环等待：线程之间存在一个环路，环路上每个线程都额外持有一个资源，而这个资源又是下一个线程要申请的</li></ul><h3 id="预防死锁"><a href="#预防死锁" class="headerlink" title="预防死锁"></a>预防死锁</h3><ul><li>循环等待：这个方法是最实用和常用的方法，就是让代码不产生循环等待的情况。最直接的方法就是获取锁的时候提供一个<strong>全序</strong>（total ordering），通过严格的加锁顺序避免了循环等待。</li><li>持有并等待：这个方法通过原子地抢锁来避免死锁问题，也就是说在用一把大锁把几个锁保护起来，但这么做可能会影响性能。</li><li>非抢占：解决非抢占就是要让线程能占用其他线程没在用的锁，不能占着茅坑不拉屎。具体来说<code>trylock()</code> 函数会尝试获取锁，当锁被占有时则返回-1。</li><li>互斥：如果不存在锁那么便不存在死锁。通过强大的硬件指令，我们可以构造出<strong>不需要锁</strong>的数据结构，这个强大的硬件指令就是「比较并交换（compare-and-swap）」。</li></ul><p>下面是一个「比较并交换」的例子，在 <code>*address</code> 的值等于 <code>expected</code> 值时，将其赋值为 new：</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></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">CompareAndSwap</span><span class="params">(<span class="type">int</span> *address, <span class="type">int</span> expected, <span class="type">int</span> new)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (*address == expected)</span><br><span class="line">    &#123;</span><br><span class="line">        *address = new;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>; <span class="comment">// success</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>; <span class="comment">// failure</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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="type">void</span> <span class="title function_">AtomicIncrement</span><span class="params">(<span class="type">int</span> *value, <span class="type">int</span> amount)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">do</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="type">int</span> old = *value;</span><br><span class="line">    &#125; <span class="keyword">while</span> (CompareAndSwap(value, old, old + amount) == <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/** 无同步操作</span></span><br><span class="line"><span class="comment">        int old = *value;</span></span><br><span class="line"><span class="comment">        *value = old + amount;</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>一个更复杂的例子：<strong>链表插入</strong>。这是在链表头部插入元素的代码：</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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">insert</span><span class="params">(<span class="type">int</span> value)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">node_t</span> *n = <span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(<span class="type">node_t</span>));</span><br><span class="line">    assert(n != <span class="literal">NULL</span>);</span><br><span class="line">    n-&gt;value = value;</span><br><span class="line">    n-&gt;next = head;</span><br><span class="line">    head = n;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过<strong>加锁</strong>解决同步：</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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">insert</span><span class="params">(<span class="type">int</span> value)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">node_t</span> *n = <span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(<span class="type">node_t</span>));</span><br><span class="line">    assert(n != <span class="literal">NULL</span>);</span><br><span class="line">    n-&gt;value = value;</span><br><span class="line">    lock(listlock); <span class="comment">// begin critical section</span></span><br><span class="line">    n-&gt;next = head;</span><br><span class="line">    head = n;</span><br><span class="line">    unlock(listlock); <span class="comment">// end of critical section</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用<strong>比较并交换指令</strong>（compare-and-swap)来实现插入操作:</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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">insert</span><span class="params">(<span class="type">int</span> value)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">node_t</span> *n = <span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(<span class="type">node_t</span>));</span><br><span class="line">    assert(n != <span class="literal">NULL</span>);</span><br><span class="line">    n-&gt;value = value;</span><br><span class="line">    <span class="keyword">do</span></span><br><span class="line">    &#123;</span><br><span class="line">        n-&gt;next = head;</span><br><span class="line">    &#125; <span class="keyword">while</span> (CompareAndSwap(&amp;head, n-&gt;next, n) == <span class="number">0</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码，首先把 next 指针指向当前的链表头（head），然后试着把新结点交换到链表头。但是，如果此时其他的线程成功地修改了 head 的值，这里的交换就会失败，导致这个线程根据新的 head 值重试。</p><h3 id="通过调度避免死锁"><a href="#通过调度避免死锁" class="headerlink" title="通过调度避免死锁"></a>通过调度避免死锁</h3><p>除了死锁预防外，有的场景死锁避免（avoidance）可能会更加合适，通过合理的调度避免产生死锁。比如下面的这个例子中，只要 T1 和 T2 (需要同一个锁的线程)不同时运行，就不会产生死锁。但这样就失去了并发性</p><img src="/2024/01/01/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%8D%81%EF%BC%89%E5%B8%B8%E8%A7%81%E5%B9%B6%E5%8F%91%E9%97%AE%E9%A2%98/sort.jpg" class="" title="sort"><h3 id="检查和恢复"><a href="#检查和恢复" class="headerlink" title="检查和恢复"></a>检查和恢复</h3><p>最后一种常用的策略就是允许死锁偶尔发生，检查到死锁时再采取行动。</p><blockquote><p>提示：不要总是完美（TOM WEST 定律）</p></blockquote><p>Tom West 是经典的计算机行业小说《Soul of a New Machine》的主人公，有一句很棒的工程格言：“不是所有值得做的事情都值得做好”。<code>如果坏事很少发生，并且造成的影响很小，那么我们不应该去花费大量的精力去预防它</code>。当然，如果你在制造航天飞机，事故会导致航天飞机爆炸，那么你应该忽略这个建议。</p><p>很多数据库系统使用了<strong>死锁检测和恢复技术</strong>。死锁检测器会定期运行，通过构建资源图来检查循环。当循环（死锁）发生时，系统需要重启。如果还需要更复杂的数据结构相关的修复，那么需要人工参与。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>本章中学习了并发编程中出现的缺陷的类型。同时也简要的讨论了死锁的发生和处理。死锁和并发编程可谓是想生相伴，在计算机发展的历史中，在一些通用库和系统中（比如Linux），都已经有了一些无等待的实现，但这仍然不够通用，并且设计一个新的无等待数据结构极为复杂，以至于不实用。也许，我们可以换个角度来看，一种新的无需任何锁的并发编程模型或许才是最好的解决方案。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;从下面的统计中我们可以看出，常见的并发问题可以分为：「非死锁的缺陷」与「死锁缺陷」&lt;/p&gt;
&lt;img src=&quot;/2024/01/01/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%8D%81%EF%BC%89%E5%B8%B8%E8%A7%81%E5%B9%B6%E5%8F%91%E9%97%AE%E9%A2%98/info.png&quot; class=&quot;&quot; title=&quot;info&quot;&gt;</summary>
    
    
    
    <category term="操作系统" scheme="https://blog.farmer233.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="C语言" scheme="https://blog.farmer233.top/tags/C%E8%AF%AD%E8%A8%80/"/>
    
    <category term="Operating Systems" scheme="https://blog.farmer233.top/tags/Operating-Systems/"/>
    
  </entry>
  
  <entry>
    <title>《OSTEP》学习笔记（九）信号量</title>
    <link href="https://blog.farmer233.top/2023/11/25/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B9%9D%EF%BC%89%E4%BF%A1%E5%8F%B7%E9%87%8F/"/>
    <id>https://blog.farmer233.top/2023/11/25/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B9%9D%EF%BC%89%E4%BF%A1%E5%8F%B7%E9%87%8F/</id>
    <published>2023-11-25T09:57:43.000Z</published>
    <updated>2026-04-22T08:30:03.572Z</updated>
    
    <content type="html"><![CDATA[<p>从前两篇文章可知<strong>条件变量</strong>必须和<strong>锁</strong>配合使用，那为什么不直接封装在一起呢？于是就有个信号量。</p><p><strong>信号量</strong>只是将锁和单值条件的条件变量<strong>封装</strong>在一起，所以它不是一个全新的概念，它能实现的事锁加条件变量都能实现。对于比较复杂情况下的条件判断无法使用信号量解决，因为其只内置了一个简单的整型的 value 条件。</p><span id="more"></span><h2 id="信号量的定义"><a href="#信号量的定义" class="headerlink" title="信号量的定义"></a>信号量的定义</h2><p>信号量是有一个整数值的对象，可以用两个函数来操作它。在 POSIX 标准中，是<code>sem_wait()</code>和 <code>sem_post()</code>。</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;semaphore.h&gt;</span></span></span><br><span class="line"><span class="type">sem_t</span> s;</span><br><span class="line">sem_init(&amp;s, <span class="number">0</span>, <span class="number">1</span>);</span><br></pre></td></tr></table></figure><p>sem_init 用于初始化信号量，其中通过第三个参数将它的值初始化为1；第二个参数表示信号量是在同一个进程的多个线程共享的，我们可以看到在所有的例子中都设置为0，这个参数的值涉及到信号量的其他用法（比如跨进程的同步访问）。信号量初始化后，可以调用<code>sem_wait()</code>和 <code>sem_post()</code>来使用：</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></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">sem_wait</span><span class="params">(<span class="type">sem_t</span> *s)</span> &#123;</span><br><span class="line">    decrement the value of semaphore s by one</span><br><span class="line">    wait <span class="keyword">if</span> value of semaphore s is negative</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">sem_post</span><span class="params">(<span class="type">sem_t</span> *s)</span> &#123;</span><br><span class="line">    increment the value of semaphore s by one</span><br><span class="line">    <span class="keyword">if</span> there are one or more threads waiting, wake one</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>sem_wait()要么<code>立刻返回</code>（调用 sem_wait()时，信号量的值<code>大于等于 1</code>），同时<code>信号量减1</code>，要么会让调用线程<code>挂起</code>，直到之后的一个 post 操作。</li><li>sem_post()并没有等待某些条件满足。它直接<code>增加信号量</code>的值，如果有等待线程，<code>唤醒</code>其中一个。</li><li>当信号量的值为<code>负数</code>时，这个值就是<code>等待线程</code>的个数</li></ul><h2 id="二值信号量（锁）"><a href="#二值信号量（锁）" class="headerlink" title="二值信号量（锁）"></a>二值信号量（锁）</h2><p>首先我们看看信号量的第一种用法：用信号量作为锁。下面的代码中我们直接在临界区中用一对 <code>sem_wait()/sem_post()</code> 来包裹。但为了使这段代码正常工作，信号量的初始化是至关重要的。因为信号量是一个有整数值的对象，因此初始值应该是 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></pre></td><td class="code"><pre><span class="line"><span class="type">sem_t</span> m;</span><br><span class="line"><span class="comment">// 初值要为1</span></span><br><span class="line">sem_init(&amp;m, <span class="number">0</span>, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">sem_wait(&amp;m);</span><br><span class="line"><span class="comment">// critical section here</span></span><br><span class="line">sem_post(&amp;m);</span><br></pre></td></tr></table></figure><h2 id="信号量用作条件变量"><a href="#信号量用作条件变量" class="headerlink" title="信号量用作条件变量"></a>信号量用作条件变量</h2><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><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="type">sem_t</span> s;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> *</span><br><span class="line"><span class="title function_">child</span><span class="params">(<span class="type">void</span> *arg)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;child\n&quot;</span>);</span><br><span class="line">    sem_post(&amp;s); <span class="comment">// signal here: child is done</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</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 class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// X初始值应是0</span></span><br><span class="line">    sem_init(&amp;s, <span class="number">0</span>, X); <span class="comment">// what should X be?</span></span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;parent: begin\n&quot;</span>);</span><br><span class="line">    <span class="type">pthread_t</span> c;</span><br><span class="line">    Pthread_create(c, <span class="literal">NULL</span>, child, <span class="literal">NULL</span>);</span><br><span class="line">    sem_wait(&amp;s); <span class="comment">// wait here for child</span></span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;parent: end\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><h2 id="生产者-消费者问题"><a href="#生产者-消费者问题" class="headerlink" title="生产者&#x2F;消费者问题"></a>生产者&#x2F;消费者问题</h2><p>我们再尝试通过信号量来解决上一章提到的「生产者&#x2F;消费者」问题，下面的代码通过三个信号量的组合使用解决了互斥与死锁两个问题，代码如下：</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><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></pre></td><td class="code"><pre><span class="line"><span class="type">sem_t</span> empty;</span><br><span class="line"><span class="type">sem_t</span> full;</span><br><span class="line"><span class="type">sem_t</span> mutex;</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 class="type">void</span> *arg)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; loops; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// empty/full是一个条件变量，用于唤醒和等待</span></span><br><span class="line">        <span class="comment">// 不过可以自动形成等待队列，比条件变量方便</span></span><br><span class="line">        sem_wait(&amp;empty); <span class="comment">// line p1</span></span><br><span class="line">        <span class="comment">// mutex是一个锁，用于商品队列的操作的保护</span></span><br><span class="line">        <span class="comment">// 锁的作用域很重要，尽可能缩小临界区来避免死锁和提高性能</span></span><br><span class="line">        sem_wait(&amp;mutex); <span class="comment">// line p1.5 (MOVED MUTEX HERE...)</span></span><br><span class="line">        put(i);           <span class="comment">// line p2</span></span><br><span class="line">        sem_post(&amp;mutex); <span class="comment">// line p2.5 (... AND HERE)</span></span><br><span class="line">        sem_post(&amp;full);  <span class="comment">// line p3</span></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 class="type">void</span> *arg)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; loops; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        sem_wait(&amp;full);  <span class="comment">// line c1</span></span><br><span class="line">        sem_wait(&amp;mutex); <span class="comment">// line c1.5 (MOVED MUTEX HERE...)</span></span><br><span class="line">        <span class="type">int</span> tmp = get();  <span class="comment">// line c2</span></span><br><span class="line">        sem_post(&amp;mutex); <span class="comment">// line c2.5 (... AND HERE)</span></span><br><span class="line">        sem_post(&amp;empty); <span class="comment">// line c3</span></span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;%d\n&quot;</span>, tmp);</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">int</span> <span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    sem_init(&amp;empty, <span class="number">0</span>, MAX); <span class="comment">// MAX buffers are empty to begin with...</span></span><br><span class="line">    sem_init(&amp;full, <span class="number">0</span>, <span class="number">0</span>);    <span class="comment">// ... and 0 are full</span></span><br><span class="line">    sem_init(&amp;mutex, <span class="number">0</span>, <span class="number">1</span>);   <span class="comment">// mutex=1 because it is a lock</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="读写锁"><a href="#读写锁" class="headerlink" title="读写锁"></a>读写锁</h2><p>并发编程中另一个经典的问题是源于对「更加灵活的锁定原语」的渴望，也就是说对更细粒度的锁的渴望。比如一个并发链表有很多插入和查找操作。插入会修改链表的状态，而查找只是读取。也就是说其实只要没有插入操作，我们可以并发的执行多个查找操作。</p><blockquote><p>笔者认为所谓「更加灵活的锁定原语」或「如何提高锁的性能」等问题，其实都可以说是减少<strong>临界区代码</strong>，因为临界区代码其实只是并发编程中的一段「临时串行」的代码。而提高锁的性能、或者更加灵活的锁定原语，其实就是通过技巧来减少「临界区代码」，从而提高锁的效率。</p><p>比如：并发队列中将一把大锁拆分成入队&#x2F;出队两把锁，从而提高了并发效率。这其实就是常说的减少锁的粒度</p></blockquote><h2 id="哲学家就餐问题"><a href="#哲学家就餐问题" class="headerlink" title="哲学家就餐问题"></a>哲学家就餐问题</h2><p>假定有 5 位“哲学家”围着一个圆桌。每两位哲学家之间有一把餐叉（一共 5 把）。哲学家有时要<code>思考</code>一会，不需要餐叉；有时又要<code>就餐</code>。而一位哲学家只有同时拿到了左手边和右手边的<code>两把餐叉</code>，才能吃到东西。如下图：</p><img src="/2023/11/25/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B9%9D%EF%BC%89%E4%BF%A1%E5%8F%B7%E9%87%8F/dining_philosopher.png" class="" title="dining_philosopher"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">    think();</span><br><span class="line">    getforks();</span><br><span class="line">    eat();</span><br><span class="line">    putforks();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因此我们可以看到，关键的挑战就是如何实现 <code>getforks()</code>和 <code>putforks()</code>函数，保证没有死锁，没有哲学家饿死，并且并发度更高（尽可能让更多哲学家同时吃东西）。我们首先定义一下这两个函数：</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="type">int</span> <span class="title function_">left</span><span class="params">(<span class="type">int</span> p)</span> &#123; <span class="keyword">return</span> p; &#125;</span><br><span class="line"><span class="type">int</span> <span class="title function_">right</span><span class="params">(<span class="type">int</span> p)</span> &#123; <span class="keyword">return</span> (p + <span class="number">1</span>) % <span class="number">5</span>; &#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">getforks</span><span class="params">()</span> &#123;</span><br><span class="line">    sem_wait(forks[left(p)]);</span><br><span class="line">    sem_wait(forks[right(p)]);</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_">putforks</span><span class="params">()</span> &#123;</span><br><span class="line">    sem_post(forks[left(p)]);</span><br><span class="line">    sem_post(forks[right(p)]);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">getforks</span><span class="params">()</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (p == <span class="number">4</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        sem_wait(forks[right(p)]);</span><br><span class="line">        sem_wait(forks[left(p)]);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">    &#123;</span><br><span class="line">        sem_wait(forks[left(p)]);</span><br><span class="line">        sem_wait(forks[right(p)]);</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> _<span class="title">Zem_t</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="type">int</span> value;</span><br><span class="line">    <span class="type">pthread_cond_t</span> cond;</span><br><span class="line">    <span class="comment">// 对value的修改锁</span></span><br><span class="line">    <span class="type">pthread_mutex_t</span> lock;</span><br><span class="line">&#125; Zem_t;</span><br><span class="line"></span><br><span class="line"><span class="comment">// only one thread can call this</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">Zem_init</span><span class="params">(Zem_t *s, <span class="type">int</span> value)</span></span><br><span class="line">&#123;</span><br><span class="line">    s-&gt;value = value;</span><br><span class="line">    Cond_init(&amp;s-&gt;cond);</span><br><span class="line">    Mutex_init(&amp;s-&gt;lock);</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_">Zem_wait</span><span class="params">(Zem_t *s)</span></span><br><span class="line">&#123;</span><br><span class="line">    Mutex_lock(&amp;s-&gt;lock);</span><br><span class="line">    <span class="keyword">while</span> (s-&gt;value &lt;= <span class="number">0</span>)</span><br><span class="line">        Cond_wait(&amp;s-&gt;cond, &amp;s-&gt;lock);</span><br><span class="line">    <span class="comment">// 在这里递减value，value值就不会小于0了。</span></span><br><span class="line">    s-&gt;value--;</span><br><span class="line">    Mutex_unlock(&amp;s-&gt;lock);</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_">Zem_post</span><span class="params">(Zem_t *s)</span></span><br><span class="line">&#123;</span><br><span class="line">    Mutex_lock(&amp;s-&gt;lock);</span><br><span class="line">    s-&gt;value++;</span><br><span class="line">    Cond_signal(&amp;s-&gt;cond);</span><br><span class="line">    Mutex_unlock(&amp;s-&gt;lock);</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>作为条件变量时，免去了 while 判断是否需要等待的操作，因为内部包含了一个 value 值用于判断</li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;从前两篇文章可知&lt;strong&gt;条件变量&lt;/strong&gt;必须和&lt;strong&gt;锁&lt;/strong&gt;配合使用，那为什么不直接封装在一起呢？于是就有个信号量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;信号量&lt;/strong&gt;只是将锁和单值条件的条件变量&lt;strong&gt;封装&lt;/strong&gt;在一起，所以它不是一个全新的概念，它能实现的事锁加条件变量都能实现。对于比较复杂情况下的条件判断无法使用信号量解决，因为其只内置了一个简单的整型的 value 条件。&lt;/p&gt;</summary>
    
    
    
    <category term="操作系统" scheme="https://blog.farmer233.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="C语言" scheme="https://blog.farmer233.top/tags/C%E8%AF%AD%E8%A8%80/"/>
    
    <category term="Operating Systems" scheme="https://blog.farmer233.top/tags/Operating-Systems/"/>
    
  </entry>
  
  <entry>
    <title>《OSTEP》学习笔记（八）条件变量</title>
    <link href="https://blog.farmer233.top/2023/11/24/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AB%EF%BC%89%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F/"/>
    <id>https://blog.farmer233.top/2023/11/24/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AB%EF%BC%89%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F/</id>
    <published>2023-11-24T09:27:54.000Z</published>
    <updated>2026-04-22T08:30:03.573Z</updated>
    
    <content type="html"><![CDATA[<p>目前为止，我们已经形成了锁的概念，并且看到了硬件和操作系统是如何正确组合来实现锁的。但锁并不是并发程序设计所需要的唯一原语。</p><span id="more"></span><p>比如在很多情况下，线程需要检查某一条件（condition）满意之后，才会继续运行。比如父线程等待子线程的<code>join</code> 函数。下面是一段自旋等待的代码：</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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> *<span class="title function_">child</span><span class="params">(<span class="type">void</span> *arg)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;child\n&quot;</span>);</span><br><span class="line">    <span class="comment">// do someting...</span></span><br><span class="line">    <span class="comment">// 修改判断条件</span></span><br><span class="line">    done = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 要父线程等待子线程的话，需要加个自旋锁，并加上判断条件</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;parent: begin\n&quot;</span>);</span><br><span class="line">    <span class="type">pthread_t</span> c;</span><br><span class="line">    Pthread_create(&amp;c, <span class="literal">NULL</span>, child, <span class="literal">NULL</span>); <span class="comment">// create child</span></span><br><span class="line">    <span class="comment">// XXX how to wait for child?</span></span><br><span class="line">    <span class="comment">// 等待判断条件done为1(子线程执行完成)，（一直占着CPU，很浪费）</span></span><br><span class="line">    <span class="keyword">while</span> (done == <span class="number">0</span>)</span><br><span class="line">        ; <span class="comment">// 自旋等待</span></span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;parent: end\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>这个方案无疑是能解决问题的，但效率比较低，因为主线程会自旋检查，浪费 CPU 时间。那么如果有一种方法可以让父线程休眠，直到等待的条件满足才唤醒执行，无疑会更加高效。</p><h2 id="定义和程序"><a href="#定义和程序" class="headerlink" title="定义和程序"></a>定义和程序</h2><p>而这样的一种锁和休眠等待的组合，已经为我们封装好了。线程可以使用<strong>条件变量（condition variable）</strong>，来等待一个条件变成真。</p><p>条件变量是一个<strong>显式队列</strong>，当某些执行状态（即条件，condition）不满足时，线程可以把自己加入队列，等待（waiting）该条件。另外某个线程，当它改变了上述状态时，就可以唤醒一个或者多个等待线程（通过在该条件上发信号），让它们继续执行。具体用法如下：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 声明</span></span><br><span class="line"><span class="type">pthread_cond_t</span> c</span><br><span class="line"><span class="comment">// 相关操作</span></span><br><span class="line">wait()</span><br><span class="line">signal()</span><br></pre></td></tr></table></figure><p>wait()调用有一个参数，它是互斥量mutex。</p><p>条件变量一般要与锁一起使用，<code>wait()</code> 调用时，这个互斥量必须是已上锁状态（处于临界区内）。wait() 的职责是释放锁，并让调用线程休眠（原子地）。当线程被唤醒时（在另外某个线程发信号给它后），它必须重新获取锁(重新进入临界区)，再返回调用者。</p><p>实际应用时由<strong>条件和锁</strong>共同决定临界区，如果获得锁就进入临界区，但如果条件未满足，则暂时<strong>放弃锁</strong>退出临界区，直到被唤醒（且条件满足）时<strong>重新上锁</strong>进入临界区。典型的条件变量用法（伪代码）:</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></pre></td><td class="code"><pre><span class="line">lock(&amp;mutex);<span class="comment">//进入临界区，保护done及其他临界资源</span></span><br><span class="line"><span class="keyword">while</span> (done == <span class="number">0</span>) <span class="comment">// 判断条件，判断时必须保证在临界区内</span></span><br><span class="line">    wait(&amp;cond, &amp;mutex);<span class="comment">// 条件不满足时放弃锁，暂时退出临界区</span></span><br><span class="line"><span class="comment">// 条件满足时，进入真正的临界区</span></span><br><span class="line"><span class="comment">/* do somting... */</span></span><br><span class="line">unlock(&amp;mutex);<span class="comment">// 退出临界区</span></span><br></pre></td></tr></table></figure><p>需要注意：**发信号时总是持有锁，**尽管并不是所有情况下都严格需要，但有效且简单的做法，还是在使用条件变量发送信号时持有锁。虽然上面的例子是必须加锁的情况，但也有一些情况可以不加锁，而这可能是你应该避免的。因此，为了简单，请在<code>调用 signal</code> 时<code>持有锁</code>（hold the lock when calling signal）。</p><p>这个提示的反面，即调用 wait 时持有锁，不只是建议，而是 wait 的语义强制要求的。因为 wait 调用总是假设你调用它时已经持有锁、调用者睡眠之前会释放锁以及返回前重新持有锁。因此，这个提示的一般化形式是正确的：调用 signal 和 wait 时要持有锁（hold the lock when calling signal or wait），你会保持身心健康的。</p><h2 id="生产者-消费者（有界缓冲区）问题"><a href="#生产者-消费者（有界缓冲区）问题" class="headerlink" title="生产者&#x2F;消费者（有界缓冲区）问题"></a>生产者&#x2F;消费者（有界缓冲区）问题</h2><p>本章要面对的下一个问题，就是生产者&#x2F;消费者问题（producer&#x2F;consumer），有的也叫「有界缓冲区（bounded buffer）」问题。</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><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></pre></td><td class="code"><pre><span class="line"><span class="type">cond_t</span> cond;</span><br><span class="line"><span class="type">mutex_t</span> mutex;</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 class="type">void</span> *arg)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; loops; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        Pthread_mutex_lock(&amp;mutex);           <span class="comment">// p1</span></span><br><span class="line">        <span class="comment">// 名义上的临界区，在lock之间</span></span><br><span class="line">        <span class="keyword">if</span> (count == <span class="number">1</span>)                       <span class="comment">// p2</span></span><br><span class="line">            Pthread_cond_wait(&amp;cond, &amp;mutex); <span class="comment">// p3</span></span><br><span class="line">        <span class="comment">// 实际上的临界区，由于wait会释放锁退出临界区(被唤醒后重新上锁)，</span></span><br><span class="line">        <span class="comment">// 所以从这里开始才是真正的临界区</span></span><br><span class="line">        put(i);                               <span class="comment">// p4</span></span><br><span class="line">        Pthread_cond_signal(&amp;cond);           <span class="comment">// p5</span></span><br><span class="line">        Pthread_mutex_unlock(&amp;mutex);         <span class="comment">// p6</span></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 class="type">void</span> *arg)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; loops; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        Pthread_mutex_lock(&amp;mutex);           <span class="comment">// c1</span></span><br><span class="line">        <span class="comment">// 1. 此处存在问题，当有两个消费者（Cus1,Cus2）时，其中Cus1进入休眠，</span></span><br><span class="line">        <span class="comment">// Cus2正好在生产者生产后执行get把count消费掉，此时</span></span><br><span class="line">        <span class="comment">// 生产者又调用signal唤醒Cus1休眠的，直接执行了get发现</span></span><br><span class="line">        <span class="comment">// 没有count了,所以要把if改成while，wait出来还要再</span></span><br><span class="line">        <span class="comment">// 判断一次count</span></span><br><span class="line">        <span class="comment">// 2. 使用while带来另一个问题，Cus1消费后本该唤醒生产者，</span></span><br><span class="line">        <span class="comment">// 但如果唤醒了Cus2线程，因为没东西消费，Cus2也会等待，</span></span><br><span class="line">        <span class="comment">// 就会导致三个线程都在等待中。解决方法是设置两个条件变量，</span></span><br><span class="line">        <span class="comment">// 保证消费者唤醒生产者，生产者唤醒消费者</span></span><br><span class="line">        <span class="keyword">if</span> (count == <span class="number">0</span>)                       <span class="comment">// c2</span></span><br><span class="line">            Pthread_cond_wait(&amp;cond, &amp;mutex); <span class="comment">// c3</span></span><br><span class="line">        <span class="type">int</span> tmp = get();                      <span class="comment">// c4</span></span><br><span class="line">        Pthread_cond_signal(&amp;cond);           <span class="comment">// c5</span></span><br><span class="line">        Pthread_mutex_unlock(&amp;mutex);         <span class="comment">// c6</span></span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;%d\n&quot;</span>, tmp);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到上面的代码中使用 <code>if</code> 作为判断显然是不合理的，具体原因我写在注释中了。为此我们可以总结出一个关于条件变量使用的简单规则：「总是<code>使用 while 循环</code>（always use while loop）」。</p><p>其次便是<strong>通知是需要指向性</strong>的，消费者不应该唤醒消费者，而应该只唤醒生产者，反之亦然。</p><p>下面我们使用 <code>while</code> 来修改代码：</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><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></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> buffer[MAX];</span><br><span class="line"><span class="type">int</span> fill = <span class="number">0</span>;</span><br><span class="line"><span class="type">int</span> use = <span class="number">0</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_">put</span><span class="params">(<span class="type">int</span> value)</span></span><br><span class="line">&#123;</span><br><span class="line">    buffer[fill] = value;</span><br><span class="line">    fill = (fill + <span class="number">1</span>) % MAX;</span><br><span class="line">    count++;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">get</span><span class="params">()</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> tmp = buffer[use];</span><br><span class="line">    use = (use + <span class="number">1</span>) % MAX;</span><br><span class="line">    count--;</span><br><span class="line">    <span class="keyword">return</span> tmp;</span><br><span class="line">&#125;</span><br><span class="line"><span class="type">cond_t</span> empty, fill;</span><br><span class="line"><span class="type">mutex_t</span> mutex;</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 class="type">void</span> *arg)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; loops; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        Pthread_mutex_lock(&amp;mutex);            <span class="comment">// p1</span></span><br><span class="line">        <span class="keyword">while</span> (count == MAX)                   <span class="comment">// p2</span></span><br><span class="line">            Pthread_cond_wait(&amp;empty, &amp;mutex); <span class="comment">// p3</span></span><br><span class="line">        put(i);                                <span class="comment">// p4</span></span><br><span class="line">        Pthread_cond_signal(&amp;fill);            <span class="comment">// p5</span></span><br><span class="line">        Pthread_mutex_unlock(&amp;mutex);          <span class="comment">// p6</span></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 class="type">void</span> *arg)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; loops; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        Pthread_mutex_lock(&amp;mutex);           <span class="comment">// c1</span></span><br><span class="line">        <span class="keyword">while</span> (count == <span class="number">0</span>)                    <span class="comment">// c2</span></span><br><span class="line">            Pthread_cond_wait(&amp;fill, &amp;mutex); <span class="comment">// c3</span></span><br><span class="line">        <span class="type">int</span> tmp = get();                      <span class="comment">// c4</span></span><br><span class="line">        Pthread_cond_signal(&amp;empty);          <span class="comment">// c5</span></span><br><span class="line">        Pthread_mutex_unlock(&amp;mutex);         <span class="comment">// c6</span></span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;%d\n&quot;</span>, tmp);</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>下面来看一个例子，这是一个简单的多线程内存分配库，以下代码用于内存分配管理，free 后会唤醒 allocate 时因空间不够而等待的线程：</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="comment">// how many bytes of the heap are free?</span></span><br><span class="line"><span class="type">int</span> bytesLeft = MAX_HEAP_SIZE;</span><br><span class="line"></span><br><span class="line"><span class="comment">// need lock and condition too</span></span><br><span class="line"><span class="type">cond_t</span> c;</span><br><span class="line"><span class="type">mutex_t</span> m;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> *</span><br><span class="line"><span class="title function_">allocate</span><span class="params">(<span class="type">int</span> size)</span></span><br><span class="line">&#123;</span><br><span class="line">    Pthread_mutex_lock(&amp;m);</span><br><span class="line">    <span class="keyword">while</span> (bytesLeft &lt; size)</span><br><span class="line">        Pthread_cond_wait(&amp;c, &amp;m);</span><br><span class="line">    <span class="type">void</span> *ptr = ...; <span class="comment">// get mem from heap</span></span><br><span class="line">    bytesLeft -= size;</span><br><span class="line">    Pthread_mutex_unlock(&amp;m);</span><br><span class="line">    <span class="keyword">return</span> ptr;</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_">free</span><span class="params">(<span class="type">void</span> *ptr, <span class="type">int</span> size)</span></span><br><span class="line">&#123;</span><br><span class="line">    Pthread_mutex_lock(&amp;m);</span><br><span class="line">    bytesLeft += size;</span><br><span class="line">    Pthread_cond_signal(&amp;c); <span class="comment">// whom to signal??</span></span><br><span class="line">    Pthread_mutex_unlock(&amp;m);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>考虑以下场景：假设目前没有空闲内存，线程 Ta 调用 <code>allocate(100)</code> ，接着线程 Tb 请求较少的内存，调用 <code>allocate(10)</code> 。Ta 和 Tb 都等待在条件上并睡眠，没有足够的空闲内存来满足它们的请求。</p><p>假定第三个线程 Tc 调用了 <code>free(50)</code>，当他发出信号唤醒等待线程时，因为不知道唤醒哪个线程，可能不会唤醒申请 10 字节的 Tb 线程，而是唤醒 Ta 线程，但该线程由于内存不够仍然等待。因此图中的代码无法正常的工作。</p><p>解决的方案也很直接：用<code>使用 pthread_cond_broadcast()</code>唤醒所有等待的线程。这样做，确保了所有应该唤醒的线程都被唤醒。当然，不利的一面是可能会影响性能。Lampson 和 Redell 把这种<strong>条件变量</strong>叫作<strong>覆盖条件</strong>（covering condition），因为它能覆盖所有需要唤醒线程的场景（保守策略）</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>这一章引入了锁之外的另一个重要同步原语：<strong>条件变量</strong>。当某些程序状态不符合要求时，让线程进入休眠状态，避免不必要的空转（spin）</p><blockquote><p>需要注意，条件变量并不是不需要锁。</p></blockquote>]]></content>
    
    
    <summary type="html">&lt;p&gt;目前为止，我们已经形成了锁的概念，并且看到了硬件和操作系统是如何正确组合来实现锁的。但锁并不是并发程序设计所需要的唯一原语。&lt;/p&gt;</summary>
    
    
    
    <category term="操作系统" scheme="https://blog.farmer233.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="C语言" scheme="https://blog.farmer233.top/tags/C%E8%AF%AD%E8%A8%80/"/>
    
    <category term="Operating Systems" scheme="https://blog.farmer233.top/tags/Operating-Systems/"/>
    
  </entry>
  
  <entry>
    <title>《OSTEP》学习笔记（七）基于锁的并发数据结构</title>
    <link href="https://blog.farmer233.top/2023/11/18/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%83%EF%BC%89%E5%9F%BA%E4%BA%8E%E9%94%81%E7%9A%84%E5%B9%B6%E5%8F%91%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    <id>https://blog.farmer233.top/2023/11/18/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%83%EF%BC%89%E5%9F%BA%E4%BA%8E%E9%94%81%E7%9A%84%E5%B9%B6%E5%8F%91%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/</id>
    <published>2023-11-18T08:47:06.000Z</published>
    <updated>2026-04-22T08:30:03.572Z</updated>
    
    <content type="html"><![CDATA[<p>通过锁可以使数据结构线程安全（thread safe），但具体如何加锁则决定了该数据结构的效率。本章将探讨怎么给数据结构加锁，才能让该结构功能正确的同时保证高性能。</p><span id="more"></span><h2 id="并发计数器"><a href="#并发计数器" class="headerlink" title="并发计数器"></a>并发计数器</h2><p>下面先看一个简单的<strong>非并发的</strong>计数器，然后一步步的在此基础上构建一个并发安全且高性能的计数器：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">counter_t</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="type">int</span> value;</span><br><span class="line">&#125; <span class="type">counter_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">init</span><span class="params">(<span class="type">counter_t</span> *c)</span></span><br><span class="line">&#123;</span><br><span class="line">    c-&gt;value = <span class="number">0</span>;</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_">increment</span><span class="params">(<span class="type">counter_t</span> *c)</span></span><br><span class="line">&#123;</span><br><span class="line">    c-&gt;value++;</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_">decrement</span><span class="params">(<span class="type">counter_t</span> *c)</span></span><br><span class="line">&#123;</span><br><span class="line">    c-&gt;value--;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">get</span><span class="params">(<span class="type">counter_t</span> *c)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> c-&gt;value;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">counter_t</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="type">int</span> value;</span><br><span class="line">    <span class="type">pthread_mutex_t</span> lock;</span><br><span class="line">&#125; <span class="type">counter_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">init</span><span class="params">(<span class="type">counter_t</span> *c)</span></span><br><span class="line">&#123;</span><br><span class="line">    c-&gt;value = <span class="number">0</span>;</span><br><span class="line">    Pthread_mutex_init(&amp;c-&gt;lock, <span class="literal">NULL</span>);</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_">increment</span><span class="params">(<span class="type">counter_t</span> *c)</span></span><br><span class="line">&#123;</span><br><span class="line">    Pthread_mutex_lock(&amp;c-&gt;lock);</span><br><span class="line">    c-&gt;value++;</span><br><span class="line">    Pthread_mutex_unlock(&amp;c-&gt;lock);</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_">decrement</span><span class="params">(<span class="type">counter_t</span> *c)</span></span><br><span class="line">&#123;</span><br><span class="line">    Pthread_mutex_lock(&amp;c-&gt;lock);</span><br><span class="line">    c-&gt;value--;</span><br><span class="line">    Pthread_mutex_unlock(&amp;c-&gt;lock);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">get</span><span class="params">(<span class="type">counter_t</span> *c)</span></span><br><span class="line">&#123;</span><br><span class="line">    Pthread_mutex_lock(&amp;c-&gt;lock);</span><br><span class="line">    <span class="type">int</span> rc = c-&gt;value;</span><br><span class="line">    Pthread_mutex_unlock(&amp;c-&gt;lock);</span><br><span class="line">    <span class="keyword">return</span> rc;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样做在多CPU环境下性能很差，因为这种锁导致了多 CPU 情况下也只允许一个线程在运行，其他都在自旋等待，没发挥出多 CPU 的优势。相当于退化成串行，并且还增加了锁的开销。<strong>理想情况下，虽然工作量增多，但并行执行后，完成任务的时间并没有增加。</strong></p><h3 id="可扩展的计数器（扩展的意思是支持多-CPU）"><a href="#可扩展的计数器（扩展的意思是支持多-CPU）" class="headerlink" title="可扩展的计数器（扩展的意思是支持多 CPU）"></a>可扩展的计数器（扩展的意思是支持多 CPU）</h3><p>为了解决上面的问题，有一种解决方法称为：懒惰计数器（sloppy counter）</p><p>懒惰计数器通过多个<strong>局部计数器</strong>和一个<strong>全局计数器</strong>来实现一个逻辑计数器，其中每个 CPU 核心有一个局部计数器。具体来说，在 4 个 CPU 的机器上，有 4 个局部计数器和 1 个全局计数器。除了这些计数器，还有锁：每个局部计数器有一个锁，全局计数器有一个。</p><p>懒惰计数器的基本思想是这样的。如果一个核心上的线程想增加计数器，那就增加它的局部计数器，访问这个局部计数器是通过对应的<strong>局部锁</strong>同步的。因为每个 CPU 有自己的局部计数器，不同 CPU 上的线程不会竞争，所以计数器的更新操作可扩展性好。</p><p>为了保持全局计数器更新（以防某个线程要读取该值），<strong>局部值会定期转移给全局计数器</strong>，方法是获取<strong>全局锁</strong>，让全局计数器加上局部计数器的值，然后将局部计数器置零</p><p><strong>局部转全局的频度，取决于一个阈值</strong>，这里称为 S（表示 sloppiness）。S 越小，懒惰计数器则越趋近于非扩展的计数器。S 越大，扩展性越强，但是全局计数器与实际计数的偏差越大。</p><p>在这个例子中，阈值 S 设置为 5，4 个 CPU 上分别有一个线程更新局部计数器 L1,…, L4。随着时间增加，全局计数器 G 的值也会记录下来。每一段时间，局部计数器可能会增加。如果局部计数值增加到阈值 S，就把局部值转移到全局计数器，局部计数器清零。</p><img src="/2023/11/18/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%83%EF%BC%89%E5%9F%BA%E4%BA%8E%E9%94%81%E7%9A%84%E5%B9%B6%E5%8F%91%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/counter.png" class="" title="counter"><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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">counter_t</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="type">int</span> global;                     <span class="comment">// global count</span></span><br><span class="line">    <span class="type">pthread_mutex_t</span> glock;          <span class="comment">// global lock</span></span><br><span class="line">    <span class="type">int</span> local[NUMCPUS];             <span class="comment">// local count (per cpu)</span></span><br><span class="line">    <span class="type">pthread_mutex_t</span> llock[NUMCPUS]; <span class="comment">// ... and locks</span></span><br><span class="line">    <span class="type">int</span> threshold;                  <span class="comment">// update frequency</span></span><br><span class="line">&#125; <span class="type">counter_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// init: record threshold, init locks, init values</span></span><br><span class="line"><span class="comment">// of all local counts and global count</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">init</span><span class="params">(<span class="type">counter_t</span> *c, <span class="type">int</span> threshold)</span></span><br><span class="line">&#123;</span><br><span class="line">    c-&gt;threshold = threshold;</span><br><span class="line"></span><br><span class="line">    c-&gt;global = <span class="number">0</span>;</span><br><span class="line">    pthread_mutex_init(&amp;c-&gt;glock, <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; NUMCPUS; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        c-&gt;local[i] = <span class="number">0</span>;</span><br><span class="line">        pthread_mutex_init(&amp;c-&gt;llock[i], <span class="literal">NULL</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// update: usually, just grab local lock and update local amount</span></span><br><span class="line"><span class="comment">// once local count has risen by &#x27;threshold&#x27;, grab global</span></span><br><span class="line"><span class="comment">// lock and transfer local values to it</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">update</span><span class="params">(<span class="type">counter_t</span> *c, <span class="type">int</span> threadID, <span class="type">int</span> amt)</span></span><br><span class="line">&#123;</span><br><span class="line">    pthread_mutex_lock(&amp;c-&gt;llock[threadID]);</span><br><span class="line">    c-&gt;local[threadID] += amt; <span class="comment">// assumes amt &gt; 0</span></span><br><span class="line">    <span class="keyword">if</span> (c-&gt;local[threadID] &gt;= c-&gt;threshold)</span><br><span class="line">    &#123; <span class="comment">// transfer to global</span></span><br><span class="line">        pthread_mutex_lock(&amp;c-&gt;glock);</span><br><span class="line">        c-&gt;global += c-&gt;local[threadID];</span><br><span class="line">        pthread_mutex_unlock(&amp;c-&gt;glock);</span><br><span class="line">        c-&gt;local[threadID] = <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    pthread_mutex_unlock(&amp;c-&gt;llock[threadID]);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// get: just return global amount (which may not be perfect)</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">get</span><span class="params">(<span class="type">counter_t</span> *c)</span></span><br><span class="line">&#123;</span><br><span class="line">    pthread_mutex_lock(&amp;c-&gt;glock);</span><br><span class="line">    <span class="type">int</span> val = c-&gt;global;</span><br><span class="line">    pthread_mutex_unlock(&amp;c-&gt;glock);</span><br><span class="line">    <span class="keyword">return</span> val; <span class="comment">// only approximate!</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="并发链表"><a href="#并发链表" class="headerlink" title="并发链表"></a>并发链表</h2><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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// basic node structure</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">node_t</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="type">int</span> key;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">node_t</span> *<span class="title">next</span>;</span></span><br><span class="line">&#125; <span class="type">node_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// basic list structure (one used per list)</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">list_t</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="type">node_t</span> *head;</span><br><span class="line">    <span class="type">pthread_mutex_t</span> lock;</span><br><span class="line">&#125; <span class="type">list_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">List_Init</span><span class="params">(<span class="type">list_t</span> *L)</span></span><br><span class="line">&#123;</span><br><span class="line">    L-&gt;head = <span class="literal">NULL</span>;</span><br><span class="line">    pthread_mutex_init(&amp;L-&gt;lock, <span class="literal">NULL</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">List_Insert</span><span class="params">(<span class="type">list_t</span> *L, <span class="type">int</span> key)</span></span><br><span class="line">&#123;</span><br><span class="line">    pthread_mutex_lock(&amp;L-&gt;lock);</span><br><span class="line">    <span class="type">node_t</span> *new = <span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(<span class="type">node_t</span>));</span><br><span class="line">    <span class="keyword">if</span> (new == <span class="literal">NULL</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        perror(<span class="string">&quot;malloc&quot;</span>);</span><br><span class="line">        pthread_mutex_unlock(&amp;L-&gt;lock);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">-1</span>; <span class="comment">// fail</span></span><br><span class="line">    &#125;</span><br><span class="line">    new-&gt;key = key;</span><br><span class="line">    new-&gt;next = L-&gt;head;</span><br><span class="line">    L-&gt;head = new;</span><br><span class="line">    pthread_mutex_unlock(&amp;L-&gt;lock);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>; <span class="comment">// success</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">List_Lookup</span><span class="params">(<span class="type">list_t</span> *L, <span class="type">int</span> key)</span></span><br><span class="line">&#123;</span><br><span class="line">    pthread_mutex_lock(&amp;L-&gt;lock);</span><br><span class="line">    <span class="type">node_t</span> *curr = L-&gt;head;</span><br><span class="line">    <span class="keyword">while</span> (curr)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (curr-&gt;key == key)</span><br><span class="line">        &#123;</span><br><span class="line">            pthread_mutex_unlock(&amp;L-&gt;lock);</span><br><span class="line">            <span class="keyword">return</span> <span class="number">0</span>; <span class="comment">// success</span></span><br><span class="line">        &#125;</span><br><span class="line">        curr = curr-&gt;next;</span><br><span class="line">    &#125;</span><br><span class="line">    pthread_mutex_unlock(&amp;L-&gt;lock);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">-1</span>; <span class="comment">// failure</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从代码中可以看出，<strong>插入函数</strong>入口处获取锁，结束时释放锁。如果 malloc 失败（在极少的时候），会有一点小问题，在这种情况下，代码在插入失败之前，必须释放锁。</p><p>我们调整代码，让获取锁和释放锁只环绕插入代码的真正临界区(缩小临界区)。前面的方法有效是因为部分工作实际上不需要锁，假定 malloc()是线程安全的，每个线程都可以调用它，不需要担心竞争条件和其他并发缺陷。只有在更新共享列表时需要持有锁。下面展示了这些修改的细节。</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><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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">List_Init</span><span class="params">(<span class="type">list_t</span> *L)</span></span><br><span class="line">&#123;</span><br><span class="line">    L-&gt;head = <span class="literal">NULL</span>;</span><br><span class="line">    pthread_mutex_init(&amp;L-&gt;lock, <span class="literal">NULL</span>);</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_">List_Insert</span><span class="params">(<span class="type">list_t</span> *L, <span class="type">int</span> key)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// synchronization not needed</span></span><br><span class="line">    <span class="type">node_t</span> *new = <span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(<span class="type">node_t</span>));</span><br><span class="line">    <span class="keyword">if</span> (new == <span class="literal">NULL</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        perror(<span class="string">&quot;malloc&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    new-&gt;key = key;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// just lock critical section</span></span><br><span class="line">    pthread_mutex_lock(&amp;L-&gt;lock);</span><br><span class="line">    new-&gt;next = L-&gt;head;</span><br><span class="line">    L-&gt;head = new;</span><br><span class="line">    pthread_mutex_unlock(&amp;L-&gt;lock);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">List_Lookup</span><span class="params">(<span class="type">list_t</span> *L, <span class="type">int</span> key)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> rv = <span class="number">-1</span>;</span><br><span class="line">    pthread_mutex_lock(&amp;L-&gt;lock);</span><br><span class="line">    <span class="type">node_t</span> *curr = L-&gt;head;</span><br><span class="line">    <span class="keyword">while</span> (curr)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (curr-&gt;key == key)</span><br><span class="line">        &#123;</span><br><span class="line">            rv = <span class="number">0</span>;</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        curr = curr-&gt;next;</span><br><span class="line">    &#125;</span><br><span class="line">    pthread_mutex_unlock(&amp;L-&gt;lock);</span><br><span class="line">    <span class="keyword">return</span> rv; <span class="comment">// now both success and failure</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在已经能保证这个链表的并发安全了，想要解决性能问题，可以使用一种叫过手锁（hand-over-hand locking，也叫锁耦合，lock coupling），其原理十分简单，就是每个节点都有一个锁，替代之前整个链表一个锁。遍历链表的时候，首先抢占下一个节点的锁，然后释放当前节点的锁。</p><p>从理论上来说确实有点合理，但实际上在遍历的时候，每个节点都要加锁、解锁，而这个开销也是十分巨大的，很难说比单锁的方法快。即使有大量的线程和很大的链表，也不一定就比单锁快，它只适用于十分单一且特殊的场景，或者夹杂在某些方案里面。</p><h2 id="并发队列"><a href="#并发队列" class="headerlink" title="并发队列"></a>并发队列</h2><p>现在我们知道可以用标准的方法来创建一个并发安全的数据结构：添加一把大锁。现在队列这种数据结构其实和链表很像，只是队列的操作有限制，因此<strong>大锁这种方案我们跳过</strong>。我们来看看 Michael 和 Scott 设计的更并发的队列：</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> __<span class="title">node_t</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="type">int</span> value;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> __<span class="title">node_t</span> *<span class="title">next</span>;</span></span><br><span class="line">&#125; <span class="type">node_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">queue_t</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="type">node_t</span> *head;</span><br><span class="line">    <span class="type">node_t</span> *tail;</span><br><span class="line">    <span class="type">pthread_mutex_t</span> headLock;</span><br><span class="line">    <span class="type">pthread_mutex_t</span> tailLock;</span><br><span class="line">&#125; <span class="type">queue_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">Queue_Init</span><span class="params">(<span class="type">queue_t</span> *q)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// tmp是假结点</span></span><br><span class="line">    <span class="type">node_t</span> *tmp = <span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(<span class="type">node_t</span>));</span><br><span class="line">    tmp-&gt;next = <span class="literal">NULL</span>;</span><br><span class="line">    q-&gt;head = q-&gt;tail = tmp;</span><br><span class="line">    pthread_mutex_init(&amp;q-&gt;headLock, <span class="literal">NULL</span>);</span><br><span class="line">    pthread_mutex_init(&amp;q-&gt;tailLock, <span class="literal">NULL</span>);</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_">Queue_Enqueue</span><span class="params">(<span class="type">queue_t</span> *q, <span class="type">int</span> value)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">node_t</span> *tmp = <span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(<span class="type">node_t</span>));</span><br><span class="line">    assert(tmp != <span class="literal">NULL</span>);</span><br><span class="line">    tmp-&gt;value = value;</span><br><span class="line">    tmp-&gt;next = <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    pthread_mutex_lock(&amp;q-&gt;tailLock);</span><br><span class="line">    q-&gt;tail-&gt;next = tmp;</span><br><span class="line">    q-&gt;tail = tmp;</span><br><span class="line">    pthread_mutex_unlock(&amp;q-&gt;tailLock);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">Queue_Dequeue</span><span class="params">(<span class="type">queue_t</span> *q, <span class="type">int</span> *value)</span></span><br><span class="line">&#123;</span><br><span class="line">    pthread_mutex_lock(&amp;q-&gt;headLock);</span><br><span class="line">    <span class="type">node_t</span> *tmp = q-&gt;head;</span><br><span class="line">    <span class="type">node_t</span> *newHead = tmp-&gt;next;</span><br><span class="line">    <span class="comment">// 如果是假结点，则视为空</span></span><br><span class="line">    <span class="keyword">if</span> (newHead == <span class="literal">NULL</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        pthread_mutex_unlock(&amp;q-&gt;headLock);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">-1</span>; <span class="comment">// queue was empty</span></span><br><span class="line">    &#125;</span><br><span class="line">    *value = newHead-&gt;value;</span><br><span class="line">    q-&gt;head = newHead;</span><br><span class="line">    pthread_mutex_unlock(&amp;q-&gt;headLock);</span><br><span class="line">    <span class="built_in">free</span>(tmp);</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>这段代码十分巧妙，用到了两个锁和一个哨兵节点。一个锁负责队头，另一个负责队尾，使得入队出队操作可以并发执行。而哨兵节点是在初始化的时候分配的，利用它分开了头和尾的操作。</p><h2 id="并发散列表"><a href="#并发散列表" class="headerlink" title="并发散列表"></a>并发散列表</h2><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><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> BUCKETS (101)</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">hash_t</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="type">list_t</span> lists[BUCKETS];</span><br><span class="line">&#125; <span class="type">hash_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">Hash_Init</span><span class="params">(<span class="type">hash_t</span> *H)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; BUCKETS; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        List_Init(&amp;H-&gt;lists[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">int</span> <span class="title function_">Hash_Insert</span><span class="params">(<span class="type">hash_t</span> *H, <span class="type">int</span> key)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> bucket = key % BUCKETS;</span><br><span class="line">    <span class="keyword">return</span> List_Insert(&amp;H-&gt;lists[bucket], key);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">Hash_Lookup</span><span class="params">(<span class="type">hash_t</span> *H, <span class="type">int</span> key)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> bucket = key % BUCKETS;</span><br><span class="line">    <span class="keyword">return</span> List_Lookup(&amp;H-&gt;lists[bucket], key);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个散列表使用我们上面实现的<strong>并发链表</strong>，性能特别好。每个散列桶（每个桶都是一个链表）都有一个锁，而不是整个散列表只有一个锁，从而支持许多并发操作，其实就是<strong>分段加锁</strong>了。</p><p>这个简单的并发散列表扩展性（性能）极好，相较于单纯的链表。原因就是缩小了临界区，减少了不同线程间操作的冲突。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>并发其实并不意味着高性能，在实现并发数据结构时，先从最简单的开始（也就是加一把大锁），有性能问题的时候再做优化。关于最后一点，避免<strong>不成熟的优化</strong>（premature optimization），对于所有关心性能的开发者都有用。<strong>我们让整个应用的某一小部分变快，却没有提高整体性能，其实没有价值</strong></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;通过锁可以使数据结构线程安全（thread safe），但具体如何加锁则决定了该数据结构的效率。本章将探讨怎么给数据结构加锁，才能让该结构功能正确的同时保证高性能。&lt;/p&gt;</summary>
    
    
    
    <category term="操作系统" scheme="https://blog.farmer233.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="C语言" scheme="https://blog.farmer233.top/tags/C%E8%AF%AD%E8%A8%80/"/>
    
    <category term="Operating Systems" scheme="https://blog.farmer233.top/tags/Operating-Systems/"/>
    
  </entry>
  
  <entry>
    <title>《OSTEP》学习笔记（六）锁</title>
    <link href="https://blog.farmer233.top/2023/11/17/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AD%EF%BC%89%E9%94%81/"/>
    <id>https://blog.farmer233.top/2023/11/17/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AD%EF%BC%89%E9%94%81/</id>
    <published>2023-11-17T08:49:13.000Z</published>
    <updated>2026-04-22T08:30:03.573Z</updated>
    
    <content type="html"><![CDATA[<p>并发编程要解决的最基本的问题：<strong>我们希望以原子方式执行一系列的指令</strong>，但由于中断的存在我们做不到这点。因此本章介绍了锁（lock）来解决这一问题。程序员在代码中加锁，放在临界区周围，保证临界区能够像单条原子指令一样执行。</p><span id="more"></span><h2 id="锁的基本思想"><a href="#锁的基本思想" class="headerlink" title="锁的基本思想"></a>锁的基本思想</h2><p>lock()和 unlock()函数的语义很简单。调用 <strong>lock()</strong> 尝试获取锁，如果没有其他线程持有锁（即它是<strong>可用</strong><code>的</code>），该线程会<strong>获得锁</strong>，进入<strong>临界区</strong>。当持有锁的线程在临界区时，其他线程就无法进入临界区。伪代码如下：</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">lock_t mutex;</span><br><span class="line">...</span><br><span class="line">lock(&amp;mutex);</span><br><span class="line">// 临界区代码(可能存在并发问题的代码）</span><br><span class="line">// 比如全局变量的操作: balance = balance + 1</span><br><span class="line">... </span><br><span class="line">unlock(&amp;mutex);</span><br></pre></td></tr></table></figure><h3 id="Pthread-锁"><a href="#Pthread-锁" class="headerlink" title="Pthread 锁"></a>Pthread 锁</h3><p>POSIX 库将锁称为互斥量（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></pre></td><td class="code"><pre><span class="line"><span class="type">pthread_mutex_t</span> lock = PTHREAD_MUTEX_INITIALIZER;</span><br><span class="line">pthread_mutex_lock(&amp;lock); <span class="comment">// wrapper for pthread_mutex_lock()</span></span><br><span class="line">balance = balance + <span class="number">1</span>;</span><br><span class="line">pthread_mutex_unlock(&amp;lock);</span><br></pre></td></tr></table></figure><h2 id="评价锁的几个指标"><a href="#评价锁的几个指标" class="headerlink" title="评价锁的几个指标"></a>评价锁的几个指标</h2><ul><li>能否完成它的基本任务——互斥（mutual exclusion）</li><li>公平性（fairness）——当锁可用时，是否每个竞争的线程都有公平的机会抢到锁？</li><li>性能（performance）——锁竞争带来的时间开销，比如单 CPU 上竞争的开销的和多 CPU 上多个线程竞争时的开销，通过比较不同的场景，我们嗯更好的理解不同锁技术对性能的影响。</li></ul><h2 id="控制中断"><a href="#控制中断" class="headerlink" title="控制中断"></a>控制中断</h2><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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">lock</span><span class="params">()</span> &#123;</span><br><span class="line">    DisableInterrupts();</span><br><span class="line">&#125;</span><br><span class="line"><span class="type">void</span> <span class="title function_">unlock</span><span class="params">()</span> &#123;</span><br><span class="line">    EnableInterrupts();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样的方案的优点是简单、清晰易懂。但缺点却很多：</p><ol><li>要<strong>允许所有调用线程执行特权操作</strong>（打开&#x2F;关闭中断），信任这种机制不会被滥用。但这是理想的情况，因为我们无法得知用户允许的程序是否可信任，因此该方案不适合作为通用方案。糟糕的情况比如有以下几个：<ol><li>一个贪婪的程序可以在开始时就关闭中断，从而独占处理器。</li><li>更糟糕的情况是被恶意程序利用，调用 lock 后一直死循环，系统无法重新获得控制权，只能重启系统。</li></ol></li><li><strong>不支持多处理器</strong>，如果多个线程允许在不同的 CPU 上，每个线程都试图进入同一个临界区，那么即便关中断也没用。当下多处理器已经很普遍了，因此需要方案更加通用。</li><li><strong>关中断导致中断丢失</strong>，这可能会导致严重的系统问题。比如磁盘完成了读取请求，正常应该会给个中断，但 CPU 因为关中断导致错失了这一中断，那么，操作系统如何知道去唤醒等待读取的进程？</li><li><strong>效率低</strong>，与正常的执行指令相比，现代 CPU 对于关闭&#x2F;打开中断的代码执行的比较慢。</li></ol><p>因此基于上述的情况，只在少数的情况下用开&#x2F;关中断来实现互斥原语，比如操作系统本身会采用屏蔽中断的方式，保证访问自己数据结构的原子性或者避免复杂的中断处理情况，因为在操作系统内部它总是可信的，不存在信任问题。</p><blockquote><p>「原语」是什么？个人简单的理解，原语是指<strong>操作系统层面</strong>提供的指令，一个原语中可能有很多个底层的指令，但操作系统帮我们确保了这些指令以原子的方式执行。</p></blockquote><h2 id="「硬件支持」测试并设置指令（原子交换）"><a href="#「硬件支持」测试并设置指令（原子交换）" class="headerlink" title="「硬件支持」测试并设置指令（原子交换）"></a>「硬件支持」测试并设置指令（原子交换）</h2><p>锁的实现需要硬件支持，最简单的<strong>硬件支持</strong>是<strong>测试并设置指令</strong>（test-and-set instruction，TAS），也叫作<strong>原子交换</strong>（atomic exchange）。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">lock_t</span> &#123;</span> <span class="type">int</span> flag; &#125; <span class="type">lock_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">init</span><span class="params">(<span class="type">lock_t</span> *mutex)</span> &#123;</span><br><span class="line">    <span class="comment">// 0 -&gt; lock is available, 1 -&gt; held</span></span><br><span class="line">    mutex-&gt;flag = <span class="number">0</span>;</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_">lock</span><span class="params">(<span class="type">lock_t</span> *mutex)</span> &#123;</span><br><span class="line">    <span class="keyword">while</span> (mutex-&gt;flag == <span class="number">1</span>) <span class="comment">// TEST the flag</span></span><br><span class="line">        ; <span class="comment">// spin-wait (do nothing)</span></span><br><span class="line">    mutex-&gt;flag = <span class="number">1</span>; <span class="comment">// now SET it!</span></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_">unlock</span><span class="params">(<span class="type">lock_t</span> *mutex)</span> &#123;</span><br><span class="line">    mutex-&gt;flag = <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上述代码中，每次 lock 会判断 flag 的值，也就是**测试并设置指令（test-and-set instruction）**中的test，然后判断成功才 <strong>set</strong></p><p>但如果没有硬件辅助，也就是让<strong>测试并设置</strong>作为一个原子操作，会导致两个线程有可能<strong>同时进入</strong>临界区。</p><blockquote><p>注意自旋等待spin-wait会影响性能</p></blockquote><h2 id="实现可用的自旋锁"><a href="#实现可用的自旋锁" class="headerlink" title="实现可用的自旋锁"></a>实现可用的自旋锁</h2><p>自旋锁（spin lock）其实就是一直自旋（判断某个条件成立），直到锁可用。</p><p>在 SPARC 上，需要的指令叫 ldstub（load&#x2F;store unsigned byte，加载&#x2F;保存无符号字节）；在 x86 上，是 xchg（atomic exchange，原子交换）指令。但它们基本上在不同的平台上做同样的事，通常称为<strong>测试并设置指令</strong>（test-and-set）。</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></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">TestAndSet</span><span class="params">(<span class="type">int</span> *old_ptr, <span class="type">int</span> new)</span> &#123;</span><br><span class="line">    <span class="type">int</span> old = *old_ptr; <span class="comment">// fetch old value at old_ptr</span></span><br><span class="line">    *old_ptr = new; <span class="comment">// store &#x27;new&#x27; into old_ptr</span></span><br><span class="line">    <span class="keyword">return</span> old; <span class="comment">// return the old value</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>将<strong>测试并设置</strong>作为<strong>原子操作</strong>：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">lock_t</span> &#123;</span></span><br><span class="line">    <span class="type">int</span> flag;</span><br><span class="line">&#125; <span class="type">lock_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">init</span><span class="params">(<span class="type">lock_t</span> *lock)</span> &#123;</span><br><span class="line">    <span class="comment">// 0 indicates that lock is available, 1 that it is ld</span></span><br><span class="line">    lock-&gt;flag = <span class="number">0</span>;</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_">lock</span><span class="params">(<span class="type">lock_t</span> *lock)</span> &#123;</span><br><span class="line">    <span class="keyword">while</span> (TestAndSet(&amp;lock-&gt;flag, <span class="number">1</span>) == <span class="number">1</span>)</span><br><span class="line">        ; <span class="comment">// spin-wait (do nothing)</span></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_">unlock</span><span class="params">(<span class="type">lock_t</span> *lock)</span> &#123;</span><br><span class="line">    lock-&gt;flag = <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="评价自旋锁"><a href="#评价自旋锁" class="headerlink" title="评价自旋锁"></a>评价自旋锁</h3><p>现在我们来用之前提到的标准来评价基本的自旋锁：</p><ul><li>锁最重要的一点是正确性（correctness），也就是能够互斥吗？答案是肯定的，因此自旋锁是一把正确的锁。</li><li>公平性（fairness）：显然自旋锁不提供任何的公平性保证，因此可能会导致饿死。</li><li>性能（performan）：单处理器上，性能开销相当大。因为在上锁后最少会自旋一个时间片，浪费 CPU 周期。在多处理器上，自旋锁性能不错。因为临界区一般很短，因此锁很快就可用了（自旋的时候），并没有浪费很多 CPU 周期，因此效果还不错。</li></ul><h2 id="硬件支持：比较并交换指令（CAS）"><a href="#硬件支持：比较并交换指令（CAS）" class="headerlink" title="硬件支持：比较并交换指令（CAS）"></a>硬件支持：比较并交换指令（CAS）</h2><p>某些系统提供了另一个硬件原语——比较交换指令（SPARC 中是 compare-and-swap，x86 中是 compare-and-exchange）。下面是伪代码：</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></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">CompareAndSwap</span><span class="params">(<span class="type">int</span> *ptr, <span class="type">int</span> expected, <span class="type">int</span> new)</span> &#123;</span><br><span class="line">    <span class="type">int</span> actual = *ptr;</span><br><span class="line">    <span class="keyword">if</span> (actual == expected)</span><br><span class="line">        *ptr = new;</span><br><span class="line">    <span class="keyword">return</span> actual;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到和<strong>测试并设置指令</strong>的工作方式类似，但其实它十分强大，这在后面讨论「无等待同步（wait-free synchronization）时，会用到这条指令的强大之处。但如果只是用它实现一个简单的自旋锁，那么它无疑等价于上面分析的自旋锁。</p><h2 id="链接的加载和条件式存储指令"><a href="#链接的加载和条件式存储指令" class="headerlink" title="链接的加载和条件式存储指令"></a>链接的加载和条件式存储指令</h2><p>一些平台提供了实现临界区的一对指令：链接的加载（load-lionked）和条件式存储（store-conditional）可以用来配合使用，实现其他并发结构。</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="type">int</span> <span class="title function_">LoadLinked</span><span class="params">(<span class="type">int</span> *ptr)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> *ptr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">StoreConditional</span><span class="params">(<span class="type">int</span> *ptr, <span class="type">int</span> value)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (no one has updated *ptr since the LoadLinked to this address) &#123;</span><br><span class="line">        *ptr = value;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>; <span class="comment">// success!</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>; <span class="comment">// failed to update</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>条件式存储</strong>（store-conditional）指令，只有上一次执行<strong>LoadLinked</strong>的地址在期间<strong>都没有更新</strong>时， 才会成功，同时更新了该地址的值</p><p>先通过 <strong>LoadLinked</strong> 尝试获取锁值，如果判断到锁被释放了，就执行<strong>StoreConditional</strong>判断在「执行完」<strong>LoadLinked</strong>到<strong>StoreConditional</strong>「执行前」ptr 有没有被<strong>更新</strong>，没有被更新则说明<strong>没有</strong>其他线程来抢，可以进临界区，有更新则说明已经被其他线程抢走了，继续重复本段落所述内容循环：</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="type">void</span> <span class="title function_">lock</span><span class="params">(<span class="type">lock_t</span> *lock)</span> &#123;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">        <span class="keyword">while</span> (LoadLinked(&amp;lock-&gt;flag) == <span class="number">1</span>)</span><br><span class="line">            ; <span class="comment">// spin until it&#x27;s zero</span></span><br><span class="line">        <span class="keyword">if</span> (StoreConditional(&amp;lock-&gt;flag, <span class="number">1</span>) == <span class="number">1</span>)</span><br><span class="line">            <span class="keyword">return</span>; <span class="comment">// if set-it-to-1 was a success: all done</span></span><br><span class="line">                <span class="comment">// otherwise: try it all over again</span></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_">unlock</span><span class="params">(<span class="type">lock_t</span> *lock)</span> &#123;</span><br><span class="line">    lock-&gt;flag = <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="硬件支持：获取并增加"><a href="#硬件支持：获取并增加" class="headerlink" title="硬件支持：获取并增加"></a>硬件支持：获取并增加</h2><p><strong>获取并增加</strong>（fetch-and-add）指令能<strong>原子地返回特定地址的旧值</strong>，并且让该值<strong>自增一</strong>。</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="type">int</span> <span class="title function_">FetchAndAdd</span><span class="params">(<span class="type">int</span> *ptr)</span> &#123;</span><br><span class="line">    <span class="type">int</span> old = *ptr;</span><br><span class="line">    *ptr = old + <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">return</span> old;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">lock_t</span> &#123;</span></span><br><span class="line">    <span class="type">int</span> ticket;</span><br><span class="line">    <span class="type">int</span> turn;</span><br><span class="line">&#125; <span class="type">lock_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">lock_init</span><span class="params">(<span class="type">lock_t</span> *lock)</span> &#123;</span><br><span class="line">    lock-&gt;ticket = <span class="number">0</span>;</span><br><span class="line">    lock-&gt;turn = <span class="number">0</span>;</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_">lock</span><span class="params">(<span class="type">lock_t</span> *lock)</span> &#123;</span><br><span class="line">    <span class="comment">// 开始挂号，获取当前号码。然后号码加1，保证每个人号码都不同</span></span><br><span class="line">    <span class="type">int</span> myturn = FetchAndAdd(&amp;lock-&gt;ticket);</span><br><span class="line">    <span class="comment">// 等待被叫号</span></span><br><span class="line">    <span class="keyword">while</span> (lock-&gt;turn != myturn)</span><br><span class="line">        ; <span class="comment">// spin</span></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_">unlock</span><span class="params">(<span class="type">lock_t</span> *lock)</span> &#123;</span><br><span class="line">    <span class="comment">// 只需要加1功能，不用返回旧值</span></span><br><span class="line">    FetchAndAdd(&amp;lock-&gt;turn);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个方案使用了两个变量来构建锁（ticket和turn），基本操作也很简单：每次进入 lock，就获取当前ticket<code>值</code>，相当于<strong>挂号</strong>，然后全局 ticket 本身会<strong>自增一</strong>，因此后续线程都会获得属于自己的唯一 ticket值，<strong>lock-&gt;turn</strong>表示当前<strong>叫号值</strong>，叫到号的运行。unlock 时递增lock-&gt;turn更新<strong>叫号值</strong>就行（也就是叫下一个号）。这种返回式保证了公平性，相当于每个线程排队运行（FIFO）。</p><h2 id="自旋过多：怎么办"><a href="#自旋过多：怎么办" class="headerlink" title="自旋过多：怎么办"></a>自旋过多：怎么办</h2><p>一个线程会一直自旋检查一个<strong>不会改变</strong>的值，浪费掉整个时间片！如果有 N 个线程去竞争一个锁，情况会更糟糕。同样的场景下，会浪费 <strong>N−1 个时间片</strong>，只是自旋并等待一个线程释放该锁。</p><p>如何让锁不会不必要地自旋，浪费 CPU 时间？要解决这个问题，只有硬件支持是不够的，我们还需要操作系统的支持！</p><h2 id="使用对列：休眠替代自旋"><a href="#使用对列：休眠替代自旋" class="headerlink" title="使用对列：休眠替代自旋"></a>使用对列：休眠替代自旋</h2><p>需要一个队列来保存等待锁的线程，上锁时发现锁已被持有，则入队并让调用线程休眠，解锁时从队列中取出一个线程唤醒。Solaris 中 <code>park()</code>能够让调用线程休眠，<code>unpark(threadID)</code>则会唤醒 threadID 标识的线程。</p><h2 id="两阶段锁（two-phase-lock）"><a href="#两阶段锁（two-phase-lock）" class="headerlink" title="两阶段锁（two-phase lock）"></a>两阶段锁（two-phase lock）</h2><p>两阶段锁中如果第一个自旋阶段没有获得锁，第二阶段调用者会睡眠，直到锁可用。Linux 中的 futex 就是这种锁，不过只自旋一次；更常见的方式是在循环中自旋固定的次数(希望这段时间内能获取到锁)，然后使用 futex 睡眠。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;并发编程要解决的最基本的问题：&lt;strong&gt;我们希望以原子方式执行一系列的指令&lt;/strong&gt;，但由于中断的存在我们做不到这点。因此本章介绍了锁（lock）来解决这一问题。程序员在代码中加锁，放在临界区周围，保证临界区能够像单条原子指令一样执行。&lt;/p&gt;</summary>
    
    
    
    <category term="操作系统" scheme="https://blog.farmer233.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="C语言" scheme="https://blog.farmer233.top/tags/C%E8%AF%AD%E8%A8%80/"/>
    
    <category term="Operating Systems" scheme="https://blog.farmer233.top/tags/Operating-Systems/"/>
    
  </entry>
  
  <entry>
    <title>《OSTEP》学习笔记（五）插叙：线程-API</title>
    <link href="https://blog.farmer233.top/2023/11/17/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%94%EF%BC%89%E6%8F%92%E5%8F%99%EF%BC%9A%E7%BA%BF%E7%A8%8B-API/"/>
    <id>https://blog.farmer233.top/2023/11/17/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%94%EF%BC%89%E6%8F%92%E5%8F%99%EF%BC%9A%E7%BA%BF%E7%A8%8B-API/</id>
    <published>2023-11-17T03:50:18.000Z</published>
    <updated>2026-04-22T08:30:03.573Z</updated>
    
    <content type="html"><![CDATA[<p>本章主要是对 pthread 库进行粗略的介绍，更多 API 的细节可以查看文档和参考其他书籍。随后的章节会慢慢介绍锁和条件变量的概念，因此本章节可以作为参考。</p><blockquote><p>本章解答的关键问题：如何创建和控制线程？<br>操作系统应该提供那些创建和控制线程的接口？这些接口如何设计得好用又实用？</p></blockquote><span id="more"></span><h2 id="线程创建"><a href="#线程创建" class="headerlink" title="线程创建"></a>线程创建</h2><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">#include &lt;pthread.h&gt;</span></span><br><span class="line">int</span><br><span class="line">pthread_create(</span><br><span class="line">    pthread_t * thread,</span><br><span class="line">    const pthread_attr_t * attr,</span><br><span class="line">    void * (*start_routine)(void*),</span><br><span class="line">    void * arg</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>上面的代码是 POSIX 中 创建线程的接口定义，该函数有 4 个参数，具体描述如下：</p><ol><li><strong>thread</strong> 是指向 pthread_t 结构体类型的指针，将利用这个结构来与该线程进行交互，因此需要将其传入 <code>pthread_create()</code> 以便初始化，相当于线程的唯一标识（身份证）</li><li><strong>attr</strong> 是用于指定该线程可能具有的属性。比如栈的大小，或者线程调度优先级等信息。</li><li><strong>*start_routine</strong> 是一个函数指针，指向要运行的函数</li><li><strong>arg</strong> 要运行的函数的参数</li></ol><h2 id="线程完成"><a href="#线程完成" class="headerlink" title="线程完成"></a>线程完成</h2><p>通过<code>pthread_join</code>阻塞等待线程完成</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建进程</span></span><br><span class="line"><span class="type">pthread_t</span> p;</span><br><span class="line">pthread_create(&amp;p, <span class="literal">NULL</span>, mythread, (<span class="type">void</span> *) <span class="number">100</span>);、</span><br><span class="line"><span class="comment">// 等待进程完成</span></span><br><span class="line">pthread_join(p, (<span class="type">void</span> **) &amp;m);</span><br></pre></td></tr></table></figure><ol><li>第一个参数是 pthread_t 类型，用于指定要等待的线程</li><li>第二个参数是一个指针，用于获取返回值。因为函数可以返回任何东西，所以它被定义为返回一个指向 void 的指针。</li></ol><h2 id="锁"><a href="#锁" class="headerlink" title="锁"></a>锁</h2><p>除了创建和等待线程之外，POSIX 线程库提供的最有用的函数集，可能就是通过锁（lock）来提供互斥的那些函数了。最基本的一对函数是：</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="type">int</span> <span class="title function_">pthread_mutex_lock</span><span class="params">(<span class="type">pthread_mutex_t</span> *mutex)</span>;</span><br><span class="line"><span class="type">int</span> <span class="title function_">pthread_mutex_unlock</span><span class="params">(<span class="type">pthread_mutex_t</span> *mutex)</span>;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="type">pthread_mutex_t</span> lock = PTHREAD_MUTEX_INITIALIZER;</span><br><span class="line"><span class="comment">// 或是int rc = pthread_mutex_init(&amp;lock, NULL);</span></span><br><span class="line">pthread_mutex_lock(&amp;lock);</span><br><span class="line">x = x + <span class="number">1</span>; <span class="comment">// or whatever your critical section is</span></span><br><span class="line">pthread_mutex_unlock(&amp;lock);</span><br></pre></td></tr></table></figure><blockquote><p>注意锁必须正确初始化，上面的代码展示了两种上锁的方法（使用 PTHREAD_MUTEX_INITIALIZER 常量或者使用 pthread_mutex_init() 函数，一般情况下更推荐用第二种。</p></blockquote><h2 id="条件变量（condition-variable）"><a href="#条件变量（condition-variable）" class="headerlink" title="条件变量（condition variable）"></a>条件变量（condition variable）</h2><p>所有线程库还有一个主要组件，就是<strong>条件变量</strong>。当线程之间必须发生某种信号时，条件变量就很有用。比如一个线程在<strong>等待</strong>另一个线程继续<strong>执行</strong>某些操作。</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="type">int</span> <span class="title function_">pthread_cond_wait</span><span class="params">(<span class="type">pthread_cond_t</span> *cond, <span class="type">pthread_mutex_t</span> *mutex)</span>;</span><br><span class="line"><span class="type">int</span> <span class="title function_">pthread_cond_signal</span><span class="params">(<span class="type">pthread_cond_t</span> *cond)</span>;</span><br></pre></td></tr></table></figure><p>要使用条件变量，必须另外有一个与此条件<strong>相关的锁</strong>。在调用上述任何一个函数时，应该持有这个锁。</p><p>第一个函数<code>pthread_cond_wait()</code>使调用线程进入<code>休眠</code>状态，因此等待其他线程发出信号，通常当程序中的某些内容发生变化时，现在正在休眠的线程可能会关心它。典型的用法如下所示：</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></pre></td><td class="code"><pre><span class="line"><span class="type">pthread_mutex_t</span> lock = PTHREAD_MUTEX_INITIALIZER;</span><br><span class="line"><span class="type">pthread_cond_t</span> cond = PTHREAD_COND_INITIALIZER;</span><br><span class="line">pthread_mutex_lock(&amp;lock);</span><br><span class="line"><span class="keyword">while</span> (ready == <span class="number">0</span>)</span><br><span class="line">    pthread_cond_wait(&amp;cond, &amp;lock);</span><br><span class="line">pthread_mutex_unlock(&amp;lock);</span><br></pre></td></tr></table></figure><p><code>唤醒线程</code>的代码运行在另外某个线程中，调用<code>pthread_cond_signal</code>时也需要持有<code>对应锁</code>。像下面这样：</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></pre></td><td class="code"><pre><span class="line">pthread_mutex_lock(&amp;lock);</span><br><span class="line">ready = <span class="number">1</span>;</span><br><span class="line">pthread_cond_signal(&amp;cond);</span><br><span class="line">pthread_mutex_unlock(&amp;lock);</span><br></pre></td></tr></table></figure><p><code>pthread_cond_wait</code>有第二个参数，因为它会<code>隐式</code>地<code>释放锁</code>，以便在其线程休眠后唤醒线程可以获取锁，之后又会<strong>重新获得锁</strong>。<br>本例通过 while 判断 ready 的值的变更，而不是通过条件变量唤醒判断 ready 已变更。将唤醒视为某种事物可能已经发生变化的暗示，而不是绝对的事实，这样更安全</p><h2 id="编译和运行"><a href="#编译和运行" class="headerlink" title="编译和运行"></a>编译和运行</h2><p>代码需要包括头文件 pthread.h 才能编译。链接时需要 pthread 库，增加 <code>-pthread</code> 标记。</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">prompt&gt; gcc -o main main.c -Wall -pthread</span><br></pre></td></tr></table></figure><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>本章介绍了基本的 pthread 库，包括<strong>线程创建</strong>、<strong>锁实现互斥执行、通过条件变量的信号和等待</strong>。如需更多信息，请在 Linux 系统上输入 <code>man -k pthread</code> 来获取。</p><p>当你使用 POSIX 线程库（或者实际上，任何线程库）来构建多线程程序时，需要记住一些小而重 要的事情：</p><ul><li><strong>保持简洁</strong>。最重要的一点，线程之间的锁和信号的代码应该尽可能简洁。复杂的线程交互容易产生缺陷。</li><li><strong>让线程交互减到最少</strong>。尽量减少线程之间的交互。每次交互都应该想清楚，并用验证过的、正确的方法来实现（很多方法会在后续章节中学习）。</li><li><strong>初始化锁和条件变量</strong>。未初始化的代码有时工作正常，有时失败，会产生奇怪的结果。</li><li><strong>检查返回值</strong>。当然，任何 C 和 UNIX 的程序，都应该检查返回值，这里也是一样。否则会导致古怪而难以理解的行为，让你尖叫，或者痛苦地揪自己的头发。</li><li><strong>注意传给线程的参数和返回值</strong>。具体来说，如果传递在栈上分配的变量的引用，可能就是在犯错误。</li><li><strong>每个线程都有自己的栈</strong>。类似于上一条，记住每一个线程都有自己的栈。因此，线程局部变量应该是线程私有的，其他线程不应该访问。线程之间共享数据，值要在堆（heap）或者其他全局可访问的位置。</li><li><strong>线程之间总是通过条件变量发送信号</strong>。切记不要用标记变量来同步。</li><li><strong>多查手册</strong>。尤其是 Linux 的 pthread 手册，有更多的细节、更丰富的内容。请仔细阅读！</li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;本章主要是对 pthread 库进行粗略的介绍，更多 API 的细节可以查看文档和参考其他书籍。随后的章节会慢慢介绍锁和条件变量的概念，因此本章节可以作为参考。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本章解答的关键问题：如何创建和控制线程？&lt;br&gt;操作系统应该提供那些创建和控制线程的接口？这些接口如何设计得好用又实用？&lt;/p&gt;
&lt;/blockquote&gt;</summary>
    
    
    
    <category term="操作系统" scheme="https://blog.farmer233.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="C语言" scheme="https://blog.farmer233.top/tags/C%E8%AF%AD%E8%A8%80/"/>
    
    <category term="Operating Systems" scheme="https://blog.farmer233.top/tags/Operating-Systems/"/>
    
  </entry>
  
  <entry>
    <title>《OSTEP》学习笔记（四）并发：介绍</title>
    <link href="https://blog.farmer233.top/2023/11/16/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%9B%9B%EF%BC%89%E5%B9%B6%E5%8F%91%EF%BC%9A%E4%BB%8B%E7%BB%8D/"/>
    <id>https://blog.farmer233.top/2023/11/16/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%9B%9B%EF%BC%89%E5%B9%B6%E5%8F%91%EF%BC%9A%E4%BB%8B%E7%BB%8D/</id>
    <published>2023-11-15T16:19:03.000Z</published>
    <updated>2026-04-22T08:30:03.573Z</updated>
    
    <content type="html"><![CDATA[<p>前面我们已经看到了操作系统是如何将一个物理 CPU 编程多个虚拟 CPU，从而支持多个程序同时运行的假象；还看到了如何为每个进程创建巨大、私有的虚拟内存，让每个程序好像拥有自己的内存，而实际操作系统秘密的复用物理内存。</p><p>OSTEP 的第二部分将介绍并发相关的内容，因此本章将首先介绍为单个运行进程提供的新抽象：进程（thread）。经典的观点是一个程序只有一个执行点（一个程序计数器，用来存放要执行的指令），但多线程（multi- threaded）程序会有多个执行点（也就是多个程序计数器，每个都用于取指令与执行）。换个角度来看，每个线程类似于独立的进程，只有一点区别：它们共享地址空间，从而能够访问相同的数据。</p><h2 id="线程的结构"><a href="#线程的结构" class="headerlink" title="线程的结构"></a>线程的结构</h2><p>单个线程的状态与进程状态非常类似。线程有一个<strong>程序计数器</strong>（Program Counter），记录程序从哪里获取指令。<code>每个线程</code>有自己的<code>一组</code>用于计算的<code>寄存器</code>。所以，如果有两个线程运行在一个处理器上，从运行一个线程（T1）<code>切换</code>到另一个线程（T2）时，必定发生<strong>上下文切换</strong>（context switch）。线程之间的上下文切换类似于进程间的上下文切换。对于进程，我们将状态保存到<strong>进程控制块</strong>（Process Control Block，PCB）。现在，我们需要一个或<strong>多个线程控制块</strong>（Thread Control Block，TCB），保存每个线程的状态。但是，与进程相比，线程之间的<strong>上下文切换</strong>有一点主要区别：<strong>地址空间保持不变</strong>（即不需要切换当前使用的<strong>页表</strong>）。</p><p>线程和进程之间另一个主要的区别在于栈。在传统的进程内存模型中，只有一个栈，通常位于地址空间的底部。而多线程的进程中，每个线程独立运行，因此地址空间中不只有一个栈，而是每个线程都有一个栈。如下图所示：</p><img src="/2023/11/16/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%9B%9B%EF%BC%89%E5%B9%B6%E5%8F%91%EF%BC%9A%E4%BB%8B%E7%BB%8D/mem_model.png" class="" title="单线程与多线程的地址空间"><h2 id="实例：进程创建"><a href="#实例：进程创建" class="headerlink" title="实例：进程创建"></a>实例：进程创建</h2><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><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="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;assert.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;pthread.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> *<span class="title function_">mythread</span><span class="params">(<span class="type">void</span> *arg)</span> &#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;%s\n&quot;</span>, (<span class="type">char</span> *) arg);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> *argv[])</span> &#123;</span><br><span class="line">    <span class="type">pthread_t</span> p1, p2;</span><br><span class="line">    <span class="type">int</span> rc;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;main: begin\n&quot;</span>);</span><br><span class="line">    rc = pthread_create(&amp;p1, <span class="literal">NULL</span>, mythread, <span class="string">&quot;A&quot;</span>); assert(rc == <span class="number">0</span>);</span><br><span class="line">    rc = pthread_create(&amp;p2, <span class="literal">NULL</span>, mythread, <span class="string">&quot;B&quot;</span>); assert(rc == <span class="number">0</span>);</span><br><span class="line">    <span class="comment">// join waits for the threads to finish</span></span><br><span class="line">    rc = pthread_join(p1, <span class="literal">NULL</span>); assert(rc == <span class="number">0</span>);</span><br><span class="line">    rc = pthread_join(p2, <span class="literal">NULL</span>); assert(rc == <span class="number">0</span>);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;main: end\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>这个代码中，主程序创建了两个线程，分别执行 <code>mythread()</code> 函数，但传参不一样。一旦线程创建，可能会立即运行或者处于就绪状态等待执行（这取决于调度程序）。下面三个表格中分别列举了几种可能的运行顺序：</p><p>情况一：</p><table data-full-width="false"><thead><tr><th>主程序</th><th>线程一</th><th>线程二</th></tr></thead><tbody><tr><td><ol><li>开始运行</li><li>打印 "main:begin"</li><li>创建线程 1</li><li>创建线程 2</li><li>等待线程 1</li></ol></td><td></td><td></td></tr><tr><td></td><td><ol><li>运行</li><li>打印 “A”</li><li>返回</li></ol></td><td></td></tr><tr><td>等待线程 2</td><td></td><td></td></tr><tr><td></td><td></td><td><ol><li>运行</li><li>打印 “B”</li><li>返回</li></ol></td></tr><tr><td>打印 "main:end"</td><td></td><td></td></tr></tbody></table><p>情况二：</p><table><thead><tr><th width="295">主程序</th><th>线程一</th><th>线程二</th></tr></thead><tbody><tr><td><ol><li>开始运行</li><li>打印 "main:begin"</li><li>创建线程 1</li><li>创建线程 2</li></ol></td><td></td><td></td></tr><tr><td></td><td></td><td><p></p><ol><li>运行</li><li>打印 “B”</li><li>返回</li></ol></td></tr><tr><td>等待线程 1</td><td></td><td></td></tr><tr><td></td><td><p></p><ol><li>运行</li><li>打印 “A”</li><li>返回</li></ol></td><td></td></tr><tr><td><ol><li>等待线程 2</li><li>打印 "main:end"</li></ol></td><td></td><td></td></tr></tbody></table><p>情况三：</p><table><thead><tr><th width="295">主程序</th><th>线程一</th><th>线程二</th></tr></thead><tbody><tr><td><ol><li>开始运行</li><li>打印 "main:begin"</li><li>创建线程 1</li><li>创建线程 2</li></ol></td><td></td><td></td></tr><tr><td></td><td><ol><li>运行</li><li>打印 “A”</li><li>返回</li></ol></td><td></td></tr><tr><td>创建线程 2</td><td></td><td></td></tr><tr><td></td><td></td><td><ol><li>运行</li><li>打印 “B”</li><li>返回</li></ol></td></tr><tr><td><ol><li>等待线程 1</li><li>等待线程 2</li><li>打印 "main:end"</li></ol></td><td></td><td></td></tr></tbody></table><p>不难看出线程使得编程变得复杂：已经很难说出什么时候会运行了！</p><h2 id="为什么更糟糕：共享数据"><a href="#为什么更糟糕：共享数据" class="headerlink" title="为什么更糟糕：共享数据"></a>为什么更糟糕：共享数据</h2><p>两个线程递增同一个数，每次运行最终结果都不一样？原因是共享数据未保证操作<code>原子性</code> ，因为代码中看似一行代码的「加加」，在真实执行时是由几个指令完成的，因此存在并发问题。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;前面我们已经看到了操作系统是如何将一个物理 CPU 编程多个虚拟 CPU，从而支持多个程序同时运行的假象；还看到了如何为每个进程创建巨大、私有的虚拟内存，让每个程序好像拥有自己的内存，而实际操作系统秘密的复用物理内存。&lt;/p&gt;
&lt;p&gt;OSTEP 的第二部分将介绍并发相关的内</summary>
      
    
    
    
    <category term="操作系统" scheme="https://blog.farmer233.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="C语言" scheme="https://blog.farmer233.top/tags/C%E8%AF%AD%E8%A8%80/"/>
    
    <category term="Operating Systems" scheme="https://blog.farmer233.top/tags/Operating-Systems/"/>
    
  </entry>
  
  <entry>
    <title>《OSTEP》学习笔记（三）插叙：进程API</title>
    <link href="https://blog.farmer233.top/2023/11/10/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%89%EF%BC%89%E6%8F%92%E5%8F%99%EF%BC%9A%E8%BF%9B%E7%A8%8BAPI/"/>
    <id>https://blog.farmer233.top/2023/11/10/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%89%EF%BC%89%E6%8F%92%E5%8F%99%EF%BC%9A%E8%BF%9B%E7%A8%8BAPI/</id>
    <published>2023-11-10T03:47:30.000Z</published>
    <updated>2026-04-22T08:30:03.572Z</updated>
    
    <content type="html"><![CDATA[<p>这一章的内容比较偏向实践，主要介绍了 UNIX 系统中的进程相关的 API，即一些列系统的调用，比如进程创建相关的<code>fork()</code> 和 <code>exec()</code> ，进程间等待用的 <code>wait()</code></p><span id="more"></span><h2 id="fork-系统调用"><a href="#fork-系统调用" class="headerlink" title="fork() 系统调用"></a>fork() 系统调用</h2><p>在执行函数 fork()时，创建了一个子进程，此时是两个进程同时运行，fork 出来的进程称为「子进程」</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></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 class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><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><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;hello world (pid:%d)\n&quot;</span>, (<span class="type">int</span>)getpid());</span><br><span class="line">    <span class="type">int</span> rc = fork();</span><br><span class="line">    <span class="keyword">if</span> (rc &lt; <span class="number">0</span>)</span><br><span class="line">    &#123; <span class="comment">// fork failed; exit</span></span><br><span class="line">        <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;fork failed\n&quot;</span>);</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (rc == <span class="number">0</span>)</span><br><span class="line">    &#123; <span class="comment">// child (new process)</span></span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;hello, I am child (pid:%d)\n&quot;</span>, (<span class="type">int</span>)getpid());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">    &#123; <span class="comment">// parent goes down this path (main)</span></span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;hello, I am parent of %d (pid:%d)\n&quot;</span>,</span><br><span class="line">               rc, (<span class="type">int</span>)getpid());</span><br><span class="line">    &#125;</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>输出如下：</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></pre></td><td class="code"><pre><span class="line">prompt&gt; ./p1</span><br><span class="line">hello <span class="title function_">world</span> <span class="params">(pid:<span class="number">29146</span>)</span></span><br><span class="line">hello, I am parent of 29147 <span class="params">(pid:<span class="number">29146</span>)</span></span><br><span class="line">hello, I am <span class="title function_">child</span> <span class="params">(pid:<span class="number">29147</span>)</span></span><br></pre></td></tr></table></figure><p>上面这段程序执行了一次 fork 操作，<code>fork()</code>函数是一个神奇的操作，它只被调用了一次，却产生了两个返回值。对于<code>父进程</code>来说，其返回值是子进程的 pid；对于<code>子进程</code>来说，其返回值为 0。</p><p>子进程并<strong>不是完全拷贝</strong>了父进程，所以子进程不会从 main 开始执行，该程序的首行打印并未被子进程执行。<strong>它拥有自己的「地址空间」（即拥有自己的私有内存）、寄存器、程序计数器等</strong>。</p><p>当然，父进程与子进程的执行顺序并不是绝对的，这都取决于调度器怎么调度，子进程也可能比父进程先执行完。</p><h2 id="wait-系统调用"><a href="#wait-系统调用" class="headerlink" title="wait() 系统调用"></a>wait() 系统调用</h2><p><code>wait()</code>函数用于使父进程（也就是调用 wait()的进程）<strong>阻塞</strong>，直到<code>一个子进程结束</code>或者该进程接收到了一个<strong>指定的信号</strong>为止。</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="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><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="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/wait.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 class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;hello world (pid:%d)\n&quot;</span>, (<span class="type">int</span>)getpid());</span><br><span class="line">    <span class="type">int</span> rc = fork();</span><br><span class="line">    <span class="keyword">if</span> (rc &lt; <span class="number">0</span>)</span><br><span class="line">    &#123; <span class="comment">// fork failed; exit</span></span><br><span class="line">        <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;fork failed\n&quot;</span>);</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (rc == <span class="number">0</span>)</span><br><span class="line">    &#123; <span class="comment">// child (new process)</span></span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;hello, I am child (pid:%d)\n&quot;</span>, (<span class="type">int</span>)getpid());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">    &#123; <span class="comment">// parent goes down this path (main)</span></span><br><span class="line">        <span class="type">int</span> wc = wait(<span class="literal">NULL</span>);</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;hello, I am parent of %d (wc:%d) (pid:%d)\n&quot;</span>,</span><br><span class="line">               rc, wc, (<span class="type">int</span>)getpid());</span><br><span class="line">    &#125;</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><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></pre></td><td class="code"><pre><span class="line">prompt&gt; ./p2</span><br><span class="line">hello world (pid:29266)</span><br><span class="line">hello, I am child (pid:29267)</span><br><span class="line">hello, I am parent of 29267 (<span class="built_in">wc</span>:29267) (pid:29266)</span><br></pre></td></tr></table></figure><p>这里因为父进程调用了<code>wait()</code>方法，因此子进程会先于父进程执行完毕。如果父进程先执行时，会等待子进程结束，才会继续执行。</p><h2 id="exec-系统调用"><a href="#exec-系统调用" class="headerlink" title="exec() 系统调用"></a>exec() 系统调用</h2><p><code>exec()</code>这个系统调用可以让子进程执行与父进程不同的程序</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><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><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 class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><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="meta">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/wait.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 class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;hello world (pid:%d)\n&quot;</span>, (<span class="type">int</span>)getpid());</span><br><span class="line">    <span class="type">int</span> rc = fork();</span><br><span class="line">    <span class="keyword">if</span> (rc &lt; <span class="number">0</span>)</span><br><span class="line">    &#123; <span class="comment">// fork failed; exit</span></span><br><span class="line">        <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;fork failed\n&quot;</span>);</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (rc == <span class="number">0</span>)</span><br><span class="line">    &#123; <span class="comment">// child (new process)</span></span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;hello, I am child (pid:%d)\n&quot;</span>, (<span class="type">int</span>)getpid());</span><br><span class="line">        <span class="type">char</span> *myargs[<span class="number">3</span>];</span><br><span class="line">        myargs[<span class="number">0</span>] = strdup(<span class="string">&quot;wc&quot;</span>);   <span class="comment">// program: &quot;wc&quot; (word count)</span></span><br><span class="line">        myargs[<span class="number">1</span>] = strdup(<span class="string">&quot;p3.c&quot;</span>); <span class="comment">// argument: file to count</span></span><br><span class="line">        myargs[<span class="number">2</span>] = <span class="literal">NULL</span>;           <span class="comment">// marks end of array</span></span><br><span class="line">        execvp(myargs[<span class="number">0</span>], myargs);  <span class="comment">// runs word count</span></span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;this shouldn&#x27;t print out&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">    &#123; <span class="comment">// parent goes down this path (main)</span></span><br><span class="line">        <span class="type">int</span> wc = wait(<span class="literal">NULL</span>);</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;hello, I am parent of %d (wc:%d) (pid:%d)\n&quot;</span>,</span><br><span class="line">               rc, wc, (<span class="type">int</span>)getpid());</span><br><span class="line">    &#125;</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><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">prompt&gt; ./p3</span><br><span class="line">hello world (pid:29383)</span><br><span class="line">hello, I am child (pid:29384)</span><br><span class="line"> 29 107 1030 p3.c</span><br><span class="line">hello, I am parent of 29384 (<span class="built_in">wc</span>:29384) (pid:29383)</span><br></pre></td></tr></table></figure><p>在这个例子中，子进程调用 <code>execvp()</code>来运行字符计数程序 wc。实际上，它针对源代码文件 p3.c 运行 wc，从而告诉我们该文件有多少行、多少单词，以及多少字节。</p><p>给定<strong>可执行程序的名称</strong>（如 wc）及<strong>需要的参数</strong>（如 p3.c）后，<code>exec()</code>会从可执行程序中加载代码和静态数据，<strong>并用它「覆写」自己的代码段（以及静态数据），堆、栈及其他内存空间也会被重新初始化</strong>。然后操作系统就执行该程序，将参数通过 argv 传递给该进程。因此，它并没有创建新进程，而是直接将当前运行的程序（以前的 p3）<code>替换</code>为不同的运行程序（wc）。子进程执行 exec()之后，几乎就像 p3.c 从未运行过一样。对 exec()的成功调用<strong>永远不会返回</strong>。如果 exec 函数执行失败, 它会返回失败的信息, 而且进程<strong>继续执行后面的代码</strong>。</p><blockquote><p>此时子进程的 pid 号并没有变，且还是该父进程的子进程，所以并不会影响 wait()操作，等待该进程的操作（统计字节）完成后，wait()才会返回，父进程同时退出阻塞状态</p></blockquote><h2 id="为什么这么设计-API"><a href="#为什么这么设计-API" class="headerlink" title="为什么这么设计 API"></a>为什么这么设计 API</h2><p>事实证明，这种分离 fork()及 exec()的做法在构建 <code>UNIX shell</code> 的时候非常有用，因为这给了 shell 在 fork 之后 exec 之前运行代码的机会，这些代码可以在运行新程序前改变环境，从而让一系列有趣的功能很容易实现。</p><p>shell 也是一个用户程序，它首先显示一个提示符（prompt），然后等待用户输入。你可以向它输入一个命令（一个可执行程序的名称及需要的参数），大多数情况下，<strong>shell 可以在文件系统中找到这个可执行程序，调用 <code>fork()</code>创建新进程，并调用 <code>exec()</code>的某个变体来执行这个可执行程序，调用 <code>wait()</code>等待该命令完成</strong>。子进程执行结束后，shell 从 wait()返回并再次输出一个提示符，等待用户输入下一条命令。</p><p>fork()和 exec()的分离，让 shell 可以方便地实现很多有用的功能。比如：</p><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">prompt&gt; wc p3.c &gt; newfile.txt</span><br></pre></td></tr></table></figure><p>在上面的例子中，wc 的输出结果被重定向（redirect）到文件 newfile.txt 中（通过 newfile.txt 之前的大于号来指明重定向）。shell 实现结果重定向的方式也很简单，当完成子进程的创建后，shell 在调用 exec()之前先关闭了标准输出（standard output），打开了文件 newfile.txt。这样，即将运行的程序 wc 的输出结果就被发送到该文件，而不是打印在屏幕上。</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><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><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 class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><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="meta">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/wait.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 class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> rc = fork();</span><br><span class="line">    <span class="keyword">if</span> (rc &lt; <span class="number">0</span>)</span><br><span class="line">    &#123; <span class="comment">// fork failed; exit</span></span><br><span class="line">        <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;fork failed\n&quot;</span>);</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (rc == <span class="number">0</span>)</span><br><span class="line">    &#123; <span class="comment">// child: redirect standard output to a file</span></span><br><span class="line">        close(STDOUT_FILENO);</span><br><span class="line">        open(<span class="string">&quot;./p4.output&quot;</span>, O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// now exec &quot;wc&quot;...</span></span><br><span class="line">        <span class="type">char</span> *myargs[<span class="number">3</span>];</span><br><span class="line">        myargs[<span class="number">0</span>] = strdup(<span class="string">&quot;wc&quot;</span>);   <span class="comment">// program: &quot;wc&quot; (word count)</span></span><br><span class="line">        myargs[<span class="number">1</span>] = strdup(<span class="string">&quot;p4.c&quot;</span>); <span class="comment">// argument: file to count</span></span><br><span class="line">        myargs[<span class="number">2</span>] = <span class="literal">NULL</span>;           <span class="comment">// marks end of array</span></span><br><span class="line">        execvp(myargs[<span class="number">0</span>], myargs);  <span class="comment">// runs word count</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">    &#123; <span class="comment">// parent goes down this path (main)</span></span><br><span class="line">        <span class="type">int</span> wc = wait(<span class="literal">NULL</span>);</span><br><span class="line">    &#125;</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>要看懂上面的例子，首先要补充点<strong>Unix文件描述符</strong>的知识:</p><ul><li>每个 Unix 进程（除了可能的守护进程）应均有三个标准的 POSIX 文件描述符，对应于三个标准流：</li></ul><table><thead><tr><th>数值</th><th>名称</th><th>&lt;unistd.h&gt;符号常量</th><th>&lt;stdio.h&gt;文件流</th></tr></thead><tbody><tr><td>0</td><td>Standard input</td><td>STDIN_FILENO</td><td>stdin</td></tr><tr><td>1</td><td>Standard output</td><td>STDOUT_FILENO</td><td>stdout</td></tr><tr><td>2</td><td>Standard error</td><td>STDERR_FILENO</td><td>stderr</td></tr></tbody></table><ul><li>UNIX 系统从 0 开始寻找可以使用的文件描述符，进程启动后默认打开了标准输出<code>STDOUT_FILENO</code>输出到屏幕，此时所有的对<code>标准输出文件描述符</code>的输出，如 <code>printf()</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></pre></td><td class="code"><pre><span class="line">root@hjk:~/repo/os_test<span class="comment"># ./a.out</span></span><br><span class="line">33 113 864 p4.c</span><br></pre></td></tr></table></figure><ul><li>如果使用_close(STDOUT_FILENO)_关闭了这个描述符，再去调用<code>printf()</code>，系统会提示找不到文件描述符</li></ul><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">root@hjk:~/repo/os_test# ./a.out</span><br><span class="line">wc: write error: Bad file descriptor</span><br></pre></td></tr></table></figure><ul><li>此时再打开<strong>新的文件描述符</strong>，会将所有的对<strong>标准输出文件描述符</strong>的输出定向到该文件描述符上（因为上面把「标准输出」关闭了，并且 unix 是从 0 开始查找可用的文件描述符的，0已经被使用了，因此会绑定到 1 上）</li></ul><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">open(<span class="string">&quot;./p4.output&quot;</span>, O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU)</span><br></pre></td></tr></table></figure><h3 id="扩展阅读：管道"><a href="#扩展阅读：管道" class="headerlink" title="扩展阅读：管道"></a>扩展阅读：管道</h3><p><strong>UNIX管道</strong>也是用类似的方式实现的，但用的是 <code>pipe()</code>系统调用。在这种情况下，一个进程的<strong>输出</strong>被链接到了一个<strong>内核管道</strong>（pipe）上（队列），另一个进程的<code>输入</code>也被连接到了同一个管道上。因此，<strong>前一个进程的输出无缝地作为后一个进程的输入</strong>，许多命令可以用这种方式串联在一起，共同完成某项任务。比如通过将 <code>grep</code>、<code>wc</code> 命令用管道连接可以完成从一个文件中查找某个词，并统计其出现次数的功能：</p><h2 id="作业"><a href="#作业" class="headerlink" title="作业"></a>作业</h2><blockquote><p>TODO，目前在读第一遍，先粗读一遍，第二遍再精读与完成课后习题</p></blockquote>]]></content>
    
    
    <summary type="html">&lt;p&gt;这一章的内容比较偏向实践，主要介绍了 UNIX 系统中的进程相关的 API，即一些列系统的调用，比如进程创建相关的&lt;code&gt;fork()&lt;/code&gt; 和 &lt;code&gt;exec()&lt;/code&gt; ，进程间等待用的 &lt;code&gt;wait()&lt;/code&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="操作系统" scheme="https://blog.farmer233.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="C语言" scheme="https://blog.farmer233.top/tags/C%E8%AF%AD%E8%A8%80/"/>
    
    <category term="Operating Systems" scheme="https://blog.farmer233.top/tags/Operating-Systems/"/>
    
  </entry>
  
  <entry>
    <title>《OSTEP》学习笔记（二）抽象：进程</title>
    <link href="https://blog.farmer233.top/2023/11/05/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89%E6%8A%BD%E8%B1%A1%EF%BC%9A%E8%BF%9B%E7%A8%8B/"/>
    <id>https://blog.farmer233.top/2023/11/05/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89%E6%8A%BD%E8%B1%A1%EF%BC%9A%E8%BF%9B%E7%A8%8B/</id>
    <published>2023-11-05T09:31:55.000Z</published>
    <updated>2026-04-22T08:30:03.573Z</updated>
    
    <content type="html"><![CDATA[<p>这章讲的是操作系统提供的最基本的抽象——进程。进程简单的来说其实就是「运行中的程序」。程序本身只是存在磁盘的一些静态数据、指令。</p><p>我们在使用计算机的过程中，通常都会运行多个程序。比如：同时运行浏览器、播放器、游戏等等。除了这些我们熟知的程序外，系统内部还运行着上百个程序。现代的处理器一般都有多个核（注意，这里的核其实是指一个「运算单元」也就是一个 CPU，也就是说我们平时购买的一个物理的 CPU 里面，一般是有多个 CPU 的）。如果每个程序都独占一个CPU，那么就可能需要上百个「运算单元」，那么这样的 CPU 也太过昂贵了。</p><p>这也是操作系统需要解决的问题：「如何提供有许多CPU的假象」。操作系统通过虚拟化（virtualizing）CPU 来提供这种假象。通过让一个进程只运行一个时间片，然后切换到其他进程，操作系统就提供了存在许多 CPU 的假象了。通俗的来讲就是通过超快速的切换进程，从而达到让用户看起来每个进程都在同时运行。</p><h3 id="进程-API"><a href="#进程-API" class="headerlink" title="进程 API"></a>进程 API</h3><ul><li>创建（Create）：用于创建新进程的方法。比如 shell 中键入命令或者在桌面双击应用图标时，就会调用操作系统来创建新的进程，运行指定的程序。</li><li>销毁（destroy）：有创建的接口那当然还需要销毁的接口，因此系统还提供强制销毁进程的接口，确保用户可以随时的终止它们。虽然进程运行完成后正常会自动退出，但也可能会因为意外的操作导致它不退出，因此该接口也是一个兜底的操作。</li><li>等待（wait）：等待进程停止运行。</li><li>状态（status）：获取有关进程的状态信息。例如运行了多长时间、当前处于什么状态等。</li><li>其他控制（miscellaneous control）：除了上面的操作外，有时还可能有其他控制，比如大多数操作系统提供某种方法来暂停进程（停止运行一段时间）。</li></ul><h3 id="进程创建"><a href="#进程创建" class="headerlink" title="进程创建"></a>进程创建</h3><p>首先看一下一个程序是如何转换为进程的。具体来说是操作系统是如何启动并运行一个程序的？</p><p>操作系统运行程序必须做的第一件事是将代码和所有静态数据（例如初始化变量）加载（load）到内存中，加载到进程的地址空间中。简单的来说就是从持久化存储的「磁盘」导入到易失的「内存」中。（如下图所示）</p><img src="/2023/11/05/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89%E6%8A%BD%E8%B1%A1%EF%BC%9A%E8%BF%9B%E7%A8%8B/load_program_to_proc.jpg" class="" title="程序加载图"><p>在早起的操作系统中，加载的这个过程是尽早（eagerly）完成，即在运行程序之前全部完成。现代操作系统则是惰性（lazily）执行该过程，即仅在程序运行期间按需加载代码或数据片段。这个过程涉及到内存虚拟化，后面的对应章节会展开具体讲解。</p><p>加载到内存后操作系统在运行之前还会执行其他一些操作，比如为程序运行时的栈（run-time stack 或 stack）、堆（heap）、局部变量等分配一些内存。此外还会执行一些其他初始化任务，特别是输入输出（I&#x2F;O）相关的任务，例如：stdin、stdout、stderr等等。最后会跳转到程序的入口，也就是 <code>main()</code> 函数，至此 OS 会将 CPU 的控制权转交到新创建的进程中，程序开始运行。</p><p>为此进程的创建过程总结如下：</p><ol><li>加载数据到内存</li><li>为栈分配空间</li><li>为堆分配空间</li><li>I&#x2F;O初始化</li><li>运行程序入口（<code>main</code> 函数）</li></ol><h3 id="进程状态"><a href="#进程状态" class="headerlink" title="进程状态"></a>进程状态</h3><p>进程的三种状态：</p><ul><li>运行（running）：运行状态表示它正在执行指令，意味着进程正在处理器上运行。</li><li>就绪（ready）：该状态表示程序已经准备好运行了，只是操作系统还未选择该进程运行</li><li>阻塞（blocked）：在阻塞状态下，一个进程执行了某种操作，直到发生其他事件时才会准备运行。常见的例子是，当进程向磁盘发起 I&#x2F;O 请求时，它会被阻塞，因此其他进程可以在这时候使用处理器，提高处理器的使用率。</li></ul><img src="/2023/11/05/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89%E6%8A%BD%E8%B1%A1%EF%BC%9A%E8%BF%9B%E7%A8%8B/image.png" class="" title="进程状态转换"><p>上图展示了上述三种状态之间的转换关系，可以根据操作系统的载量，让进程在就绪状态和运行状态之间转换。从<strong>就绪 -&gt; 运行</strong>意味着该进程已经被<strong>调度</strong>（scheduled）。从<strong>运行 -&gt; 就绪</strong>意味着该进程已经<strong>取消调度</strong>（descheduled）。一旦进程被阻塞（例如，通过发起 I&#x2F;O 操作），OS 将保持进程的这种状态，直到发生某种事件（例如，I&#x2F;O 完成）。此时，进程再次转入就绪状态（也可能立即再次运行，如果操作系统这样决定）。</p><p>关于调度的策略，简单总结下，就是<code>一个进程</code><strong><code>阻塞或停止</code></strong><code>时，就会去调度另一个</code><strong><code>就绪</code></strong><code>的进程</code>，让 CPU 一直保持在满负荷状态，从而提高 CPU 的使用率。</p><h3 id="数据结构"><a href="#数据结构" class="headerlink" title="数据结构"></a>数据结构</h3><p>为了跟踪每个进程的状态，操作系统可能会为所有就绪的进程保留某种<code>进程列表</code>（process list），以及跟踪当前正在运行的进程的一些附加信息。操作系统还必须以某种方式跟踪<code>被阻塞的进程</code>。当 I&#x2F;O 事件完成时，操作系统应确保<code>唤醒</code>正确的进程，让它准备好再次运行。</p><p>那么既然操作系统也是个程序，并且是由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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// the registers xv6 will save and restore</span></span><br><span class="line"><span class="comment">// to stop and subsequently restart a process</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">context</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="type">int</span> eip;</span><br><span class="line">    <span class="type">int</span> esp;</span><br><span class="line">    <span class="type">int</span> ebx;</span><br><span class="line">    <span class="type">int</span> ecx;</span><br><span class="line">    <span class="type">int</span> edx;</span><br><span class="line">    <span class="type">int</span> esi;</span><br><span class="line">    <span class="type">int</span> edi;</span><br><span class="line">    <span class="type">int</span> ebp;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// the different states a process can be in</span></span><br><span class="line"><span class="comment">// 可以看到实际操作系统对于进程状态的定义远不止上面介绍的3种</span></span><br><span class="line"><span class="class"><span class="keyword">enum</span> <span class="title">proc_state</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    UNUSED,</span><br><span class="line">    EMBRYO,</span><br><span class="line">    SLEEPING,</span><br><span class="line">    RUNNABLE,</span><br><span class="line">    RUNNING,</span><br><span class="line">    ZOMBIE</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// the information xv6 tracks about each process</span></span><br><span class="line"><span class="comment">// including its register context and state</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">proc</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="type">char</span> *mem;    <span class="comment">// Start of process memory</span></span><br><span class="line">    uint sz;      <span class="comment">// Size of process memory</span></span><br><span class="line">    <span class="type">char</span> *kstack; <span class="comment">// Bottom of kernel stack</span></span><br><span class="line">    <span class="comment">// for this process</span></span><br><span class="line">    <span class="class"><span class="keyword">enum</span> <span class="title">proc_state</span> <span class="title">state</span>;</span>      <span class="comment">// Process state</span></span><br><span class="line">    <span class="type">int</span> pid;                    <span class="comment">// Process ID</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">proc</span> *<span class="title">parent</span>;</span>        <span class="comment">// Parent process</span></span><br><span class="line">    <span class="type">void</span> *chan;                 <span class="comment">// If non-zero, sleeping on chan</span></span><br><span class="line">    <span class="type">int</span> killed;                 <span class="comment">// If non-zero, have been killed</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">file</span> *<span class="title">ofile</span>[<span class="title">NOFILE</span>];</span> <span class="comment">// Open files</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">inode</span> *<span class="title">cwd</span>;</span>          <span class="comment">// Current directory</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">context</span> <span class="title">context</span>;</span>     <span class="comment">// Switch here to run process</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">trapframe</span> *<span class="title">tf</span>;</span>       <span class="comment">// Trap frame for the</span></span><br><span class="line">                                <span class="comment">// current interrupt</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>该数据结构展示了 xv6 内核中每个进程的相关信息类型。这里用到 XV6 内核是因为它足够简单，是上手操作系统的一个不错选择 。 并且在“真正的”操作系统中存在类似的进程结构，如 Linux、macOS X 或 Windows。</p><p>除了<code>运行</code>、<code>就绪</code>和<code>阻塞</code>之外，还有其他一些进程可以处于的状态：</p><ul><li><p><strong>初始（initial）状态</strong></p><p>有时候系统会有一个初始（initial）状态，表示进程在创建时处于的状态。</p></li><li><p><strong>最终（final）状态</strong></p><p>另外，一个进程可以处于已退出但尚未清理的最终（final）状态（在基于 UNIX 的系统中，这称为<code>僵尸状态</code>）。这个最终状态非常有用，因为它允许其他进程（通常是创建进程的父进程）检查进程的<code>返回代码</code>，并查看刚刚完成的进程<code>是否成功执行</code>（通常，在基于 UNIX 的系统中，程序成功完成任务时返回零，否则返回非零）。完成后，父进程将进行最后一次调用（例如，wait()），以等待子进程的完成，并告诉操作系统它可以清理这个正在结束的进程的所有相关数据结构</p></li></ul><h3 id="作业"><a href="#作业" class="headerlink" title="作业"></a>作业</h3><p>关于作业，本文只摘取部分我认为比较重要的部分：立即处理阻塞完成的进程是否是一个好主意?（也就是第六题）</p><ul><li>第六题：在本题中，进程 0 首先进入 IO，此时由于<code>-S SWITCH_ON_IO</code>参数，进程 0 进入阻塞状态，cpu 被切换到运行进程 1，当进程 0 的 IO 完成后，进程 1 继续执行，直到完成。也就是 IO 完成事件不会被立即处理，由于进程 0 的 IO 动作较为频繁，会使它长时间处于 IO 完成等待状态，导致后续的 IO 操作时 cpu 已经无事可做了，在本例条件下降低了效率，输出如下：</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><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></pre></td><td class="code"><pre><span class="line">$ python3 ./process-run.py -l 3:0,5:100,5:100,5:100 -S SWITCH_ON_IO -I IO_RUN_LATER -c -p</span><br><span class="line">Time        PID: 0        PID: 1        PID: 2        PID: 3           CPU           IOs</span><br><span class="line">  1         RUN:io         READY         READY         READY             1          </span><br><span class="line">  2        BLOCKED       RUN:cpu         READY         READY             1             1</span><br><span class="line">  3        BLOCKED       RUN:cpu         READY         READY             1             1</span><br><span class="line">  4        BLOCKED       RUN:cpu         READY         READY             1             1</span><br><span class="line">  5        BLOCKED       RUN:cpu         READY         READY             1             1</span><br><span class="line">  6        BLOCKED       RUN:cpu         READY         READY             1             1</span><br><span class="line">  7*         READY          DONE       RUN:cpu         READY             1          </span><br><span class="line">  8          READY          DONE       RUN:cpu         READY             1          </span><br><span class="line">  9          READY          DONE       RUN:cpu         READY             1          </span><br><span class="line"> 10          READY          DONE       RUN:cpu         READY             1          </span><br><span class="line"> 11          READY          DONE       RUN:cpu         READY             1          </span><br><span class="line"> 12          READY          DONE          DONE       RUN:cpu             1          </span><br><span class="line"> 13          READY          DONE          DONE       RUN:cpu             1          </span><br><span class="line"> 14          READY          DONE          DONE       RUN:cpu             1          </span><br><span class="line"> 15          READY          DONE          DONE       RUN:cpu             1          </span><br><span class="line"> 16          READY          DONE          DONE       RUN:cpu             1          </span><br><span class="line"> 17    RUN:io_done          DONE          DONE          DONE             1          </span><br><span class="line"> 18         RUN:io          DONE          DONE          DONE             1          </span><br><span class="line"> 19        BLOCKED          DONE          DONE          DONE                           1</span><br><span class="line"> 20        BLOCKED          DONE          DONE          DONE                           1</span><br><span class="line"> 21        BLOCKED          DONE          DONE          DONE                           1</span><br><span class="line"> 22        BLOCKED          DONE          DONE          DONE                           1</span><br><span class="line"> 23        BLOCKED          DONE          DONE          DONE                           1</span><br><span class="line"> 24*   RUN:io_done          DONE          DONE          DONE             1          </span><br><span class="line"> 25         RUN:io          DONE          DONE          DONE             1          </span><br><span class="line"> 26        BLOCKED          DONE          DONE          DONE                           1</span><br><span class="line"> 27        BLOCKED          DONE          DONE          DONE                           1</span><br><span class="line"> 28        BLOCKED          DONE          DONE          DONE                           1</span><br><span class="line"> 29        BLOCKED          DONE          DONE          DONE                           1</span><br><span class="line"> 30        BLOCKED          DONE          DONE          DONE                           1</span><br><span class="line"> 31*   RUN:io_done          DONE          DONE          DONE             1          </span><br><span class="line"></span><br><span class="line">Stats: Total Time 31</span><br><span class="line">Stats: CPU Busy 21 (67.74%)</span><br><span class="line">Stats: IO Busy  15 (48.39%)</span><br><span class="line"></span><br></pre></td></tr></table></figure><ul><li>为什么运行一个刚刚完成 I&#x2F;O 的进程会是一个好主意? 对于 IO 频繁的应用来说，立即运行和不立即运行会有这么两种情况：<ul><li>立即运行：因为该进程是一个 IO 频繁的应用，因此立即执行后可以立马的进行下一个 IO 操作，进入阻塞态，让出 CPU 的同时等待下一个 IO 事件。</li><li>不立即运行：在上一个 IO 完成后，CPU 调度前的时间将会被白白浪费，因为该进程是一个 IO 频繁的。输出如下：</li></ul></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><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">$ python3 ./process-run.py -l 3:0,5:100,5:100,5:100 -S SWITCH_ON_IO -I IO_RUN_IMMEDIATE -c -p</span><br><span class="line">Time        PID: 0        PID: 1        PID: 2        PID: 3           CPU           IOs</span><br><span class="line">  1         RUN:io         READY         READY         READY             1          </span><br><span class="line">  2        BLOCKED       RUN:cpu         READY         READY             1             1</span><br><span class="line">  3        BLOCKED       RUN:cpu         READY         READY             1             1</span><br><span class="line">  4        BLOCKED       RUN:cpu         READY         READY             1             1</span><br><span class="line">  5        BLOCKED       RUN:cpu         READY         READY             1             1</span><br><span class="line">  6        BLOCKED       RUN:cpu         READY         READY             1             1</span><br><span class="line">  7*   RUN:io_done          DONE         READY         READY             1          </span><br><span class="line">  8         RUN:io          DONE         READY         READY             1          </span><br><span class="line">  9        BLOCKED          DONE       RUN:cpu         READY             1             1</span><br><span class="line"> 10        BLOCKED          DONE       RUN:cpu         READY             1             1</span><br><span class="line"> 11        BLOCKED          DONE       RUN:cpu         READY             1             1</span><br><span class="line"> 12        BLOCKED          DONE       RUN:cpu         READY             1             1</span><br><span class="line"> 13        BLOCKED          DONE       RUN:cpu         READY             1             1</span><br><span class="line"> 14*   RUN:io_done          DONE          DONE         READY             1          </span><br><span class="line"> 15         RUN:io          DONE          DONE         READY             1          </span><br><span class="line"> 16        BLOCKED          DONE          DONE       RUN:cpu             1             1</span><br><span class="line"> 17        BLOCKED          DONE          DONE       RUN:cpu             1             1</span><br><span class="line"> 18        BLOCKED          DONE          DONE       RUN:cpu             1             1</span><br><span class="line"> 19        BLOCKED          DONE          DONE       RUN:cpu             1             1</span><br><span class="line"> 20        BLOCKED          DONE          DONE       RUN:cpu             1             1</span><br><span class="line"> 21*   RUN:io_done          DONE          DONE          DONE             1          </span><br><span class="line"></span><br><span class="line">Stats: Total Time 21</span><br><span class="line">Stats: CPU Busy 21 (100.00%)</span><br><span class="line">Stats: IO Busy  15 (71.43%)</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;这章讲的是操作系统提供的最基本的抽象——进程。进程简单的来说其实就是「运行中的程序」。程序本身只是存在磁盘的一些静态数据、指令。&lt;/p&gt;
&lt;p&gt;我们在使用计算机的过程中，通常都会运行多个程序。比如：同时运行浏览器、播放器、游戏等等。除了这些我们熟知的程序外，系统内部还运行着</summary>
      
    
    
    
    <category term="操作系统" scheme="https://blog.farmer233.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="C语言" scheme="https://blog.farmer233.top/tags/C%E8%AF%AD%E8%A8%80/"/>
    
    <category term="Operating Systems" scheme="https://blog.farmer233.top/tags/Operating-Systems/"/>
    
  </entry>
  
  <entry>
    <title>《OSTEP》学习笔记（一）操作系统介绍</title>
    <link href="https://blog.farmer233.top/2023/10/13/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%80%EF%BC%89%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E4%BB%8B%E7%BB%8D/"/>
    <id>https://blog.farmer233.top/2023/10/13/%E3%80%8AOSTEP%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%80%EF%BC%89%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E4%BB%8B%E7%BB%8D/</id>
    <published>2023-10-12T17:36:51.000Z</published>
    <updated>2026-04-22T08:30:03.572Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本系列文章将按照笔者阅读章节顺序编写，结合原文与自己的感悟，以作笔记之用，如有不足之处，恳请在评论区指出</p></blockquote><p>操作系统主要的三个部分分别是：虚拟化（virtualization）、并发（concurrency）和持久化（persistence），这是本书主要学习的3个关键概念。通过学习这三个概念来理解操作系统这门课程。</p><p>国内许多教材&#x2F;八股文对操作系统的描述可能与本书差异较大，但我个人感觉其实都是描述一个东西，相比之下教材上只是描述的更具体。比如进程&#x2F;线程其实是操作系统实现虚拟化的其中一个手段的等等。</p><h2 id="虚拟化"><a href="#虚拟化" class="headerlink" title="虚拟化"></a>虚拟化</h2><h3 id="CPU-虚拟化"><a href="#CPU-虚拟化" class="headerlink" title="CPU 虚拟化"></a>CPU 虚拟化</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><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="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;time.h&gt;</span>  </span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;assert.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;common.h&quot;</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 class="type">int</span> argc, <span class="type">char</span> <span class="type">const</span> *argv[])</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (argc != <span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;usage: cpu &lt;string&gt;\n&quot;</span>);</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">char</span> *str = argv[<span class="number">1</span>];</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        Spin(<span class="number">1</span>);</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;%s\n&quot;</span>, str);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line"></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><blockquote><p><code>common.h</code> 头文件将放在本文末尾，当然你也可以访问<a href="https://pages.cs.wisc.edu/~remzi/OSTEP/">官网</a>&#x2F;<a href="https://github.com/FarmerChillax/ostep-code/blob/master/intro/common.h">GitHub</a>来获取</p></blockquote><p>当我们在一个单处理器（cpu）的系统上编译并运行它，我们将看到以下内容：</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">farmer:~/studys/operating-systems/ch02$ ./cpu A</span><br><span class="line">A</span><br><span class="line">A</span><br><span class="line">A</span><br><span class="line">A</span><br><span class="line">A</span><br><span class="line">^C</span><br><span class="line">farmer:~/studys/operating-systems/ch02$</span><br></pre></td></tr></table></figure><p>这个结果看起来十分合理不是吗？现在让我们运行同一个程序的不同实例，来看看结果：</p><figure class="highlight armasm"><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></pre></td><td class="code"><pre><span class="line"><span class="symbol">farmer:</span>~/studys/operating-systems/ch02$ ./cpu A &amp; ./cpu <span class="keyword">B</span> &amp; ./cpu C &amp;</span><br><span class="line">[<span class="number">1</span>] <span class="number">208278</span></span><br><span class="line">[<span class="number">2</span>] <span class="number">208279</span></span><br><span class="line">[<span class="number">3</span>] <span class="number">208280</span></span><br><span class="line"><span class="keyword">B</span></span><br><span class="line">C</span><br><span class="line">A</span><br><span class="line"><span class="keyword">B</span></span><br><span class="line">C</span><br><span class="line">C</span><br><span class="line"><span class="keyword">B</span></span><br><span class="line">A</span><br><span class="line"><span class="keyword">B</span></span><br><span class="line">A</span><br><span class="line">C</span><br><span class="line"><span class="keyword">B</span></span><br><span class="line">A</span><br><span class="line">C</span><br><span class="line"><span class="symbol">...</span></span><br></pre></td></tr></table></figure><p>现在事情开始有趣起来了，尽管只有一个处理器，但是这几个程序在我们用户的视角看来还是在同时运行的，看起来就像有多个处理器在同时执行这3个程序一样！</p><p>而这种将单个CPU（或其中一小部分）转换成看似多个CPU，从而让许多程序看似同时运行的技术，这就是所谓的虚拟化CPU（virtualizing the CPU）</p><h3 id="内存虚拟化"><a href="#内存虚拟化" class="headerlink" title="内存虚拟化"></a>内存虚拟化</h3><p>看完了CPU让我们来看看内存，现代机器提供的物理内存（physical memory）模型其实非常的简单，就是一个字节数组而已。程序通过指定一个地址（address）来访问、写入或更新存在那里的数据。</p><p>程序运行时一直要访问内存，程序将所有数据结构保存在内存中，并通过各种指令来访问它们，因此每次读取指令都会访问内存。下面来看一个简单的demo：</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></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 class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><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="meta">#<span class="keyword">include</span> <span class="string">&quot;common.h&quot;</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 class="type">int</span> argc, <span class="type">char</span> <span class="type">const</span> *argv[])</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> *p = <span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(<span class="type">int</span>)); <span class="comment">//a1</span></span><br><span class="line">    assert(p != <span class="literal">NULL</span>);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;(%d) memory address of p: %08x\n&quot;</span>, getpid(), (<span class="type">unsigned</span>) p); <span class="comment">//a2</span></span><br><span class="line"></span><br><span class="line">    *p = <span class="number">0</span>; <span class="comment">//a3</span></span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        Spin(<span class="number">1</span>);</span><br><span class="line">        *p = *p + <span class="number">1</span>;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;(%d) p: %d\n&quot;</span>, getpid(), *p); <span class="comment">// a4</span></span><br><span class="line">    &#125;</span><br><span class="line"></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>该程序的输出如下：</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">farmer:~/studys/operating-systems/ch02$ ./mem </span><br><span class="line">(208544) memory address of p: 18afa2a0</span><br><span class="line">(208544) p: 1</span><br><span class="line">(208544) p: 2</span><br><span class="line">(208544) p: 3</span><br><span class="line">(208544) p: 4</span><br><span class="line">(208544) p: 5</span><br><span class="line">^C</span><br></pre></td></tr></table></figure><p>这个demo首先为 p 这个变量分配了一些内存（a1行）。然后打印出内存的地址（a2），然后将数字0放入新分配的内存（a3）的第一个空位中。最后程序循环，延迟一秒钟并递增 p 中保存的地址值。在每个打印语句中，它还会打印出正在运行程序的进程标识符（PID）。</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></pre></td><td class="code"><pre><span class="line">farmer:~/studys/operating-systems/ch02$ ./mem &amp; ./mem &amp;</span><br><span class="line">[1] 24113</span><br><span class="line">[2] 24114</span><br><span class="line">(24113) memory address of p: 00200000</span><br><span class="line">(24114) memory address of p: 00200000</span><br><span class="line">(24113) p: 1</span><br><span class="line">(24114) p: 1</span><br><span class="line">(24114) p: 2</span><br><span class="line">(24113) p: 2</span><br><span class="line">(24113) p: 3</span><br><span class="line">(24114) p: 3</span><br><span class="line">(24113) p: 4</span><br><span class="line">(24114) p: 4</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>我们可以看到每个实例都在相同的地址（00200000）分配了内存，但每个似乎都独立更新了00200000处的值！就好像每个正在运行的程序都有自己的私有地址，而不是与其他正在运行的程序共享相同的物理内存。</p><p>而这真是操作系统虚拟化内存（virtualizing memory）时发生的情况，每个进程访问自己的私有虚拟地址空间（virtual address space）（有时称为地址空间，address space），操作系统以某种方式映射到机器的物理内存上，让正在运行的实例完全拥有自己的物理内存。</p><p>但实际情况是，物理内存是由操作系统管理的共享资源。而这也正是上文提到的「虚拟化」的内容。</p><h2 id="并发"><a href="#并发" class="headerlink" title="并发"></a>并发</h2><p>并发是这本书的第二个部分，并发只是一个用来代指「<strong>同时处理很多事情</strong>」所带来一系列问题的代词，而这些问题是在同时（并发）处理很多事情时出现且必须解决的。</p><p>那么为什么并发通常出现在《操作系统》这门课中呢？其实是因为并发问题首先出现在操作系统中，而随着软件工程的发展，现代多线程（multi-threaded）程序也存在相同的问题。我们来看一个具体的多线程例子：</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><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></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 class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;common.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;common_threads.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">volatile</span> <span class="type">int</span> counter = <span class="number">0</span>;</span><br><span class="line"><span class="type">int</span> loops;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> *<span class="title function_">worker</span><span class="params">(<span class="type">void</span> *arg)</span> &#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> ( i = <span class="number">0</span>; i &lt; loops; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        counter++;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</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 class="type">int</span> argc, <span class="type">char</span> *argv[])</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (argc != <span class="number">2</span>) &#123; </span><br><span class="line"><span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;usage: threads &lt;loops&gt;\n&quot;</span>); </span><br><span class="line"><span class="built_in">exit</span>(<span class="number">1</span>); </span><br><span class="line">    &#125; </span><br><span class="line">    loops = atoi(argv[<span class="number">1</span>]);</span><br><span class="line">    <span class="type">pthread_t</span> p1, p2;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;Initial value : %d\n&quot;</span>, counter);</span><br><span class="line">    Pthread_create(&amp;p1, <span class="literal">NULL</span>, worker, <span class="literal">NULL</span>); </span><br><span class="line">    Pthread_create(&amp;p2, <span class="literal">NULL</span>, worker, <span class="literal">NULL</span>);</span><br><span class="line">    Pthread_join(p1, <span class="literal">NULL</span>);</span><br><span class="line">    Pthread_join(p2, <span class="literal">NULL</span>);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;Final value   : %d\n&quot;</span>, counter);</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>这个例子中利用<code>Pthread_create()</code>创建了两个线程（thread），每个线程开始在一个名为<code>worker()</code>的函数中运行，该函数中只是递增一个计数器，循环 loops 次。</p><p>下面是将变量loops的输入值设置为 1000 时的输出结果，根据代码我们可以很容易的猜到运行结果是 2000，因为每个线程会循环 1000 次并对计数器做一个累加的操作。也就是说当输入为 N 时，直观的预计结果为 2N：</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">farmer:~/studys/operating-systems/ch02$ ./thread 1000</span><br><span class="line">Initial value : 0</span><br><span class="line">Final value   : 2000</span><br></pre></td></tr></table></figure><p>但如果对并发编程有点了解的话会发现这段代码中存在一个问题：计数器累加操作并非原子方式（atomically）的。让我们运行相同的程序，但 loops 的值更高，然后看看会发生什么：</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></pre></td><td class="code"><pre><span class="line">farmer:~/studys/operating-systems/ch02$ ./thread 100000</span><br><span class="line">Initial value : 0</span><br><span class="line">Final value   : 100000</span><br><span class="line">farmer:~/studys/operating-systems/ch02$ ./thread 100000</span><br><span class="line">Initial value : 0</span><br><span class="line">Final value   : 105018</span><br><span class="line">farmer:~/studys/operating-systems/ch02$ ./thread 100000</span><br><span class="line">Initial value : 0</span><br><span class="line">Final value   : 135669</span><br></pre></td></tr></table></figure><p>事实证明当我们将 loops 值设置的更高后，得到的最终值不是 200000。并且其最终值在每次运行中都是不一样的结果！</p><p>这些奇怪的，不合常理的结果与指令如何执行有关。上面程序中的关键部分是增加共享计数器（counter）的地方，它需要 3 条指令：一是将计数器的值翀内存中加载到寄存器，二是将其做递增操作，三是将其保存回内存。因为这三条指令并不是以原子的方式执行（所有的指令一次性执行）的，所以才会导致这些奇怪的事情发生。而这种问题通常叫：并发（concurrency）问题</p><h2 id="持久性"><a href="#持久性" class="headerlink" title="持久性"></a>持久性</h2><p>我们都知道程序是运行在内存中的，如果发生断电或系统崩溃等情况，那么内存中的数据是容易丢失的。因为像 DRAM 这样的设备以易失（volatile）的方式存储数据，因此我们需要硬件和软件来持久化（persistently）的存储数据。这样的存储对于所有系统来说都十分重要，因为数据是无价的。</p><p>操作系统中管理硬盘的软件通常称为文件系统（file system）。因此它负责以可靠和高效的方式，将用户创建的任何文件（file）存储在系统的磁盘上。而不像 CPU 和内存一样需要操作系统提虚拟化，因为它是可以被多个程序所共有的。</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></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 class="meta">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;assert.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><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="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/types.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;string.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 class="type">int</span> argc, <span class="type">char</span> *argv[])</span> &#123;</span><br><span class="line">    <span class="type">int</span> fd = open(<span class="string">&quot;/tmp/file&quot;</span>, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);</span><br><span class="line">    assert(fd &gt;= <span class="number">0</span>);</span><br><span class="line">    <span class="type">char</span> buffer[<span class="number">20</span>];</span><br><span class="line">    <span class="built_in">sprintf</span>(buffer, <span class="string">&quot;hello world\n&quot;</span>);</span><br><span class="line">    <span class="type">int</span> rc = write(fd, buffer, <span class="built_in">strlen</span>(buffer));</span><br><span class="line">    assert(rc == (<span class="built_in">strlen</span>(buffer)));</span><br><span class="line">    fsync(fd);</span><br><span class="line">    close(fd);</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>上面的程序向操作系统发出了 3 个调用。一是 <code>open()</code> 调用，用来创建并打开一个文件。第二个是 <code>write()</code> 调用，将一些数据(「hello world\n」这串字符) 写入文件。第三则是 <code>close()</code> 调用来关闭文件，从而表明程序不会再向该文件写入更多的数据。</p><p>而上面提到的这些系统调用（system call）会被转到称为文件系统的操作系统部分，然后由文件系统处理这些请求，并向用户返回某种代码来表示结果。</p><p>首先确定新数据将驻留在磁盘上的哪个位置，然后在文件系统所维护的各种结构中对其进行记录。这样做需要向底层存储设备发出 I&#x2F;O 请求，以读取现有结构或更新（写入）它们。所有写过设备驱动程序（device driver）的人都知道，让设备现表你执行某项操作是一个复杂而详细的过程。它需要深入了解低级别设备接口及其确切的语义。幸运的是，操作系统提供了一种通过系统调用来访问设备的标准和简单的方法。因此，OS 有时被视为标准库（standard library）。</p><p>出于性能方面的原因，大多数文件系统首先会<code>延迟</code>这些写操作一段时间，希望将其<code>批量分组</code>为较大的组。为了处理写入期间系统崩溃的问题，大多数文件系统都包含某种复杂的写入协议，如日志（journaling）或写时复制（copy-on-write），仔细<code>排序</code>写入磁盘的操作，以确保如果在写入序列期间发生故障，系统可以在之后恢复到合理的状态。为了使不同的通用操作更高效，文件系统采用了许多不同的数据结构和访问方法，从简单的列表到复杂的 <code>B 树</code>。</p><h2 id="设计目标"><a href="#设计目标" class="headerlink" title="设计目标"></a>设计目标</h2><p>设计目标指的是「开发和设计操作系统时所设定的主要目标和原则」，或者说是操作系统的一些基本设计原则。</p><ol><li><strong>一个最基本的目标，是建立一些抽象（abstraction）</strong>，让系统方便和易于使用。抽象对我们在计算机科学中做的每件事都很有帮助。抽象使得编写一个大型程序成为可能，将其划分为小而且容易理解的部分</li><li><strong>设计和实现操作系统的第二个目标，是提供高性能（performance）</strong>。换言之，我们的目标是<code>最小化</code>操作系统的<code>开销</code>（minimize the overhead）。但是虚拟化的设计是为了易于使用，无形之中会增大开销，比如虚拟页的切换，cpu 的调度等等，所以尽可能的保持易用性与性能的平衡至关重要</li><li><strong>在应用程序之间以及在 OS 和应用程序之间提供保护（protection）</strong>。因为我们希望让许多程序同时运行，所以要确保一个程序的恶意或偶然的不良行为不会损害其他程序。保护是操作系统基本原理之一的核心，这就是<code>隔离</code>（isolation）。让进程彼此隔离是保护的关键，因此决定了 OS 必须执行的大部分任务</li><li><strong>操作系统往往力求提供高度的可靠性（reliability）</strong>。因为操作系统必须不间断运行，当它失效时，系统上运行的所有应用程序也会失效。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本章节主要用于对操作系统全貌的一个介绍，回答了：“操作系统是什么” 的问题。</p><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><ul><li><a href="https://pages.cs.wisc.edu/~remzi/OSTEP/Chinese/">《Operating Systems: Three Easy Pieces》中文版</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本系列文章将按照笔者阅读章节顺序编写，结合原文与自己的感悟，以作笔记之用，如有不足之处，恳请在评论区指出&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;操作系统主要的三个部分分别是：虚拟化（virtualization）、并发（concurrency）</summary>
      
    
    
    
    <category term="操作系统" scheme="https://blog.farmer233.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="C语言" scheme="https://blog.farmer233.top/tags/C%E8%AF%AD%E8%A8%80/"/>
    
    <category term="Operating Systems" scheme="https://blog.farmer233.top/tags/Operating-Systems/"/>
    
  </entry>
  
  <entry>
    <title>MySQL的存储与索引(InnoDB)</title>
    <link href="https://blog.farmer233.top/2023/09/27/MySQL%E7%9A%84%E5%AD%98%E5%82%A8%E4%B8%8E%E7%B4%A2%E5%BC%95/"/>
    <id>https://blog.farmer233.top/2023/09/27/MySQL%E7%9A%84%E5%AD%98%E5%82%A8%E4%B8%8E%E7%B4%A2%E5%BC%95/</id>
    <published>2023-09-27T07:42:31.000Z</published>
    <updated>2026-04-22T08:30:03.567Z</updated>
    
    <content type="html"><![CDATA[<p>MySQL 是当下一款主流的关系型数据库，它所有的数据均以磁盘文件的方式存储。在日常开发中我们都会听到许多为了加快 SQL 查询的效率而添加索引，同时许多文档也都会告诉我们要添加太多的索引， 索引不要太长， 使用数字或者空字符串来代替 NULL 等等的许多建议。那么为什么会有这些建议？这些建议又是否正确？这些答案都能从MySQL数据的物理存储方式中找到。</p><h2 id="一行记录的存储格式"><a href="#一行记录的存储格式" class="headerlink" title="一行记录的存储格式"></a>一行记录的存储格式</h2><p>我们执行 SQl 的时候，查询的结果是一行行的格式返回的.因此在讲解索引、数据页前我们先来看一下 MySQL 中一行记录是怎么存储的：</p><img src="/2023/09/27/MySQL%E7%9A%84%E5%AD%98%E5%82%A8%E4%B8%8E%E7%B4%A2%E5%BC%95/record.png" class="" title="一行记录的存储格式"><p>在上图中我们可以看到,一条完整的记录分为「记录头」与「真实数据」两部份，下面展开看看记录头的部份：</p><!-- **1. 变长字段长度**我们知道 MySQL 支持一些变长的数据类型，比如 TEXT、VARCHAR 等等。这些类型的字段存储多少字节的数据是不固定的，所以我们在存储数据的时候把长度记录下来，这样后续读取的时候才知道读哪些内容**2. NULL 值列表**在一条记录中某些列是可以存放 NULL 值的，如果把这些 NULL 值都放到真实数据部份中存储会很占地方，所以我们把 NULL 的列统一管理起来 --><p><strong>1. 记录头信息</strong><br>我们知道 MySQL 支持一些变长的数据类型与NULL值，比如 TEXT、VARCHAR 等等。这些类型的字段存储多少字节的数据是不固定的，所以我们在存储数据的时候把对应信息存储下来方便后续读取。因为该部份对本文影响不大，因此没有展开。</p><p><strong>2. next_record 字段</strong><br>在记录头中这个显眼的「next_record」字段相信已经引起了你的注意，正如其字面意思，该字段表示的是当前记录与一下条记录的<strong>偏移量</strong>。换一个说法就是，该字段是一个<strong>指向下一条记录的指针</strong>。</p><p>所以我们可以看到，在 InnoDB 中所有的数据就是通过该字段串起来成了一个单链表。而 MySQL 优化手段也是基于此进行改造的。</p><blockquote><p>上图是我经过简化后的示意图，我把一些与本篇无关的内容隐藏掉了，仅保留了比较关键的内容</p></blockquote><h2 id="InnoDB数据页"><a href="#InnoDB数据页" class="headerlink" title="InnoDB数据页"></a>InnoDB数据页</h2><p>页是 InnoDB 管理存储空间的基本单位，一个页的大小一般是 16k。数据页中则存放着我们上面提到的一行行记录，现在我们知道记录之间是通过 <code>next_record</code> 字段串联形成一个单向链表。那么在一个数据页中要查找某条记录该怎么办呢？</p><p>最笨的办法是从第一条记录开始遍历一遍链表，那么第一条记录我们怎么知道是哪条呢？以及当数据很多时，遍历一遍的时间也是无法忽视的，MySQL是怎么优化的？下面我们来看一下 MySQL 中数据页的格式，如下图：</p><img src="/2023/09/27/MySQL%E7%9A%84%E5%AD%98%E5%82%A8%E4%B8%8E%E7%B4%A2%E5%BC%95/page.png" class="" title="数据页格式"><p>在上图中我们可以看到一个数据页中关键的几个东西：</p><ol><li>页号：页号是唯一的，如同身份证号码一样，InnoDB 通过页号来唯一定位一个页</li><li>最小记录&#x2F;最大记录：页面中的最小记录和最大记录，用于指向真实数据记录，相当于真实记录链表的头尾哨兵</li><li>槽（slot）：页目录，用于快速定位页面中的记录，加速查找过程（通过二分查找）。<ol><li>将所有正常的记录（包括最大&#x2F;最小记录）划分为几个组，一个组对应槽中的一位（对应图中黄色范围）</li><li>每组记录的最后一条记录中的记录头中会有一个 <code>n_owned</code> 属性表示该组内共有几条记录</li><li>将每个组中最后一条记录在页面中的偏移量单独提取出来（就是槽中的每一位均指向每组记录中最大的那条记录）</li></ol></li></ol><h3 id="页目录（槽）"><a href="#页目录（槽）" class="headerlink" title="页目录（槽）"></a>页目录（槽）</h3><p>比如现在图中有5条记录，InnoDB 会把他们分成2个组，第一组只有一个「最小记录」（也就是头哨兵），第二组则是剩余的4条记录，一共两个组。因此对应着2个槽，每个槽中存放每个组中最大的那条记录在页面中的地址偏移量（即指向最大记录的指针）。每个分组中的记录条数都有规定，规定如下：</p><ol><li>最小记录（头哨兵）所在的分组只能有一条记录</li><li>最大记录（尾哨兵）所在的分组拥有的记录数只能在1～8条之间</li><li>剩余的分组中记录的条数范围为4～8条之间</li></ol><p>所以初始情况下只有最小&#x2F;大记录，因此页目录（槽）中只有两个组。之后每插入一条记录，都会从页目录上找到对应记录的主键值比准备插入的主键值大，且差值最小（为了让主键顺序排列-从小到大）的槽，然后把该槽的 <code>n_owned</code> 值加一，表示添加了一条记录，直到该组记录中的记录数等于8个。8个后再插入则不满足第二条规则，因此会产生新的一个组，并将原有记录进行迁移，使得满足三条规则。</p><h3 id="在数据页中定位记录"><a href="#在数据页中定位记录" class="headerlink" title="在数据页中定位记录"></a>在数据页中定位记录</h3><p>当前页面中记录太少，不好演示页目录如何加快查找速度的，为此我们往页面添加几条记录，添加后的数据页如下（为了清晰的展示槽与组之间的关系，我将记录之间的指针隐藏了，详细看上一张图片）：</p><img src="/2023/09/27/MySQL%E7%9A%84%E5%AD%98%E5%82%A8%E4%B8%8E%E7%B4%A2%E5%BC%95/page_more_data.png" class="" title="更多数据的数据页"><p>因为记录的主键都是从小到大排列的，所以我们可以使用二分法快速查找记录，如图中所示一共有5个槽，接下来我们模拟一下在该表中找到「记录6」的过程：</p><ol><li>根据槽的信息，可以得到最低的槽 low &#x3D; 0，最高的槽 high&#x3D;4（也就是二分查找的初始边界）</li><li>计算中间槽的位置：(0+4)&#x2F;2 &#x3D; 2，查看槽2对应记录为8，又因为 8 &gt; 6，所以设置 high&#x3D;2, low 保持不变</li><li>重新计算中间槽的位置：(0+2)&#x2F;2 &#x3D; 1，查看槽1对应组最大记录的主键值为4；又因为 4 &lt; 6,所以设置 low&#x3D;1, high 保持不变</li><li>因为 high-low &#x3D; 1，所以确定主键值为6的记录在槽2对应的组中</li><li>此时我们只需从槽1的最大记录开始遍历每条记录（也就是从槽2的开头遍历），便能很快找到「记录6」了。</li></ol><p>上面我们已经介绍过，每个组中包含的记录条数最多是8条，所以遍历一个组中的记录代价是很小的。因此我们可以将上面的步骤再提炼一下，总结成在一个数据页中查找元素的步骤：</p><ol><li>在页目录中通过二分法确认记录所在的槽</li><li>通过记录的 <code>next_record</code> 属性遍历该槽所在的组中的各个记录</li></ol><p>至此，我们已经知道了如何在数据页中快速查找到一条记录，而将这一个个数据页连接起来便能是我们存储的所有数据了。那么如何才能高效的在这么多数据页中查找记录，接下来就轮到“索引同志”登场了。</p><h2 id="B-树索引"><a href="#B-树索引" class="headerlink" title="B+树索引"></a>B+树索引</h2><p>我们知道一个数据页的大小是 16K，而我们业务中一个表的数据动辄就上G，显然一个数据页是无法装下我们的数据的。那么在一个表中的数据页是怎么链接起来的？我们回到查询一条记录的语句上，要在这么多数据页中找到一条记录，最笨的方法便是遍历所有的数据页，那么为什么要遍历所有的数据页呢？我们不能有个聪明点的方法吗？原因是因为数据页之间是没有规律的，我们并不知道搜索调节会匹配哪些页的记录（因为没有规律，所有的数据页都有可能存在），因此不得不遍历所有数据页。</p><p>那么如何快速定位到记录在哪些数据页中？我们还记得在数据页里面为了快速定位一条记录，我们为页中的数据划分了组，并且为此建立了一个页目录（槽）。同样的我们也可以想办法为了「<strong>快速定位记录所在的页</strong>」而建立一个目录，在建立这个目录的过程中需要满足这几个条件（页与页之间的规定）：</p><ol><li>下一个数据页中「真实记录」的主键值必须大于上一个页中「真实记录」的主键值（叶子节点）</li><li>数据页之间通过前后指针相连</li><li>给所有页建立一个目录项（内节点）</li></ol><p>为此我们得到了一个简易版的索引方案，如下图：</p><img src="/2023/09/27/MySQL%E7%9A%84%E5%AD%98%E5%82%A8%E4%B8%8E%E7%B4%A2%E5%BC%95/simple_index.png" class="" title="简易索引"><p>为什么说这是简易版的索引呢？因为我们这么做的前提是假设了目录项是有连续的存储空间的，但实际并非如此，因此会引起下面的几个问题：</p><ol><li>InnoDB 使用页作为管理存储空间的基本单位，也就是只能保证16KB的连续存储空间</li><li>我们经常会对记录做增、删、改的操作，如果某个页中最后一条记录删除会导致该页被移除，为了紧凑设计会导致目录项中该页后面的所有记录都需要往前挪动</li></ol><p>为此我们需要一种更灵活的方式来管理目录项，我们不难发现目录项与「真实记录」长的很像，只不过目录项中的两个列是主键与页号而已，为此我们可以复用存储用户记录的数据页来存储目录项，我们把这些用来表示目录项的记录称为<strong>目录项记录</strong></p><p>那么我们把前面简易版本的索引方案修改后，便能得到下图所示的结构：</p><img src="/2023/09/27/MySQL%E7%9A%84%E5%AD%98%E5%82%A8%E4%B8%8E%E7%B4%A2%E5%BC%95/index.png" class="" title="索引结构"><p>以查找记录9为例子，我们来看看是如何通过目录项加快查询速度的：</p><ol><li>先到目录项记录所在的页（也就是页10）中通过二分法快速定位到对应的目录项记录，因为 1&lt; 9 &lt; 2，所以定位到对应的「真实记录」所在的页为页21</li><li>再到存储用户记录的页21中根据二分法快速定位到「记录9」</li></ol><p>虽然说目录页中只存放主键值与页号，比用户记录所需要的空间要小很多，但因为一个页只有16K大小，因此能存放的目录项也是有限的。如果表中的数据太多了导致目录页存放不下，那么便会重新分配一个目录页。目录页与目录页之间也通过上述的办法进行复用（利用目录页定位目录页），从而减少单次二分查找的数量，提高效率。在插入很多数据后，整个结构便像下图一样：</p><img src="/2023/09/27/MySQL%E7%9A%84%E5%AD%98%E5%82%A8%E4%B8%8E%E7%B4%A2%E5%BC%95/index_full.png" class="" title="B+树"><p>上图所示的便是许多文章中提到的 <strong>B+树</strong> 了，无论是存放用户记录的数据页，还是存放目录项的数据页，我们都把它们存放到B+树这个数据结构中。从上图中我们也可以看出，我们的**「真实记录」其实都存放在B+树最底层的节点上**（叶子节点）。其余用来存放目录项的节点为非叶子节点或内节点。</p><p>我们来简单的计算一下，假设<strong>叶子节点</strong>所代表的数据页可以存放 100 条用户记录，而<strong>内节点</strong>可以存放1000条记录，那么：</p><ol><li>如果该树有2层，则最多能存放 1000 x 100 &#x3D; 100000 条用户记录</li><li>如果该树有3层，最多能存放 1000 x 1000 x 100 &#x3D; 100,000,000 条用户记录</li><li>如果有4层，最多能存放 1000 x 1000 x 1000 x 100 &#x3D; 100,000,000,000 条记录</li></ol><p>所以在一般情况下，我们用到的B+树都不会超过3层。这样一来在通过主键去查找某条记录时，最多只需要进行4个页面内的查找（3个目录页与一个真实记录的页），又因为每个页中有<strong>页目录</strong>（槽），所以在页面内可以通过二分查找快速定位记录。因此我们可以看到这就是使用B+树查询高效的原因。</p><blockquote><p>这种根据主键ID构建的B+树，我们称为「<strong>聚簇索引</strong>」，这也引申出来了 InnoDB 中所谓的“索引即数据，数据即索引”。</p></blockquote><p>我们可以看到这里只有 ID 作为索引列，但我们平时使用的过程中还可以为其他列建立的索引，因此这就引申出<strong>二级索引</strong>与<strong>联合索引</strong>了。</p><h3 id="二级索引与联合索引"><a href="#二级索引与联合索引" class="headerlink" title="二级索引与联合索引"></a>二级索引与联合索引</h3><p>无论是二级索引亦或是联合索引，其实都是基于上面提到的聚簇索引而引申出来的。我们先来看看二级索引与聚簇索引的区别：</p><ol><li>聚簇索引是根据主键进行排序的，而我们刚刚提到无论是页面内还是页面之间都强调了一个<strong>有序（从小到大）<strong>的特性。因此二级索引也是需要进行排序的，但二级索引是根据</strong>索引列</strong>进行排序，排序的规则则是根据设置的字符集来排序</li><li>二级索引的叶子节点中只存放「索引列」与主键值，因此如果需要获取完整的记录，需要根据主键值到聚簇索引中查询（这个操作也叫回表）</li></ol><p>二级索引如下图所示：</p><img src="/2023/09/27/MySQL%E7%9A%84%E5%AD%98%E5%82%A8%E4%B8%8E%E7%B4%A2%E5%BC%95/sec_index.png" class="" title="二级索引"><p>我们除了可以使用某一列进行索引外，还可以同时为多个列建立索引，而多个列建立的索引则称为「<strong>联合索引</strong>」。许多文章都会提到<strong>最左匹配</strong>等概念，而这也是与我们联合索引的结构息息相关的，比如我们想让B+树按照A和B列的大小进行排序，这里面就包含了两层含义：</p><ol><li>先把各个记录和页按照A列进行排序（因为A在前面）</li><li>在A列相同的情况下，再根据B列进行排序</li></ol><p>该索引如下图所示：</p><img src="/2023/09/27/MySQL%E7%9A%84%E5%AD%98%E5%82%A8%E4%B8%8E%E7%B4%A2%E5%BC%95/cover_index.png" class="" title="联合索引"><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在创建和使用索引时应该注意以下事项：</p><ol><li>只为用于搜索、排序或分组的列创建索引</li><li>当列中不重复值的个数在总记录数中占比很大时，才值得建立索引</li><li>索引列的类型要尽可能的小</li><li>可以只为索引列创建前缀索引，减少索引占用的空间</li><li>尽量覆盖索引进行查询，避免出现回表</li><li>为了减少让<strong>聚簇索引</strong>出现页分裂的情况，主键最好要自增</li></ol><!-- 本文简单粗略的介绍了 MySQL 数据库中 InnoDB 存储引擎的存储与索引结构，对该部份掌握的深浅决定了写出来的 SQL 是否高效、合理。这一年多的工作中可以说天天和 MySQL 打交道，在解决了几个线上慢SQL告警后才意思到要好好补补这方面的知识，希望这篇笔记能让大家对 InnoDB 的存储与索引有一个更清晰的认知吧🚀 -->]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;MySQL 是当下一款主流的关系型数据库，它所有的数据均以磁盘文件的方式存储。在日常开发中我们都会听到许多为了加快 SQL 查询的效率而添加索引，同时许多文档也都会告诉我们要添加太多的索引， 索引不要太长， 使用数字或者空字符串来代替 NULL 等等的许多建议。那么为什么会</summary>
      
    
    
    
    <category term="MySQL" scheme="https://blog.farmer233.top/categories/MySQL/"/>
    
    
    <category term="笔记" scheme="https://blog.farmer233.top/tags/%E7%AC%94%E8%AE%B0/"/>
    
    <category term="MySQL" scheme="https://blog.farmer233.top/tags/MySQL/"/>
    
    <category term="数据库" scheme="https://blog.farmer233.top/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
  </entry>
  
  <entry>
    <title>Go性能分析工具</title>
    <link href="https://blog.farmer233.top/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/"/>
    <id>https://blog.farmer233.top/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/</id>
    <published>2023-07-04T02:08:33.000Z</published>
    <updated>2026-04-22T08:30:03.547Z</updated>
    
    <content type="html"><![CDATA[<!-- ## 前言 --><p>对于 Golang 程序性能分析来说，pprof 一定是一个大杀器般的存在。主要可以分析 CPU、内存的使用情况、阻塞情况、Goroutine 的堆栈信息以及锁争用情况等性能问题。</p><p>pprof 是一个性能分析工具，Go 在语言层面就内置了 profile 采样工具。这会涉及到 <code>runtime/pprof</code> 与 <code>net/http/pprof</code> 这两个包。但本文着重于使用 pprof 来分析问题，故不讲解采样相关内容。</p><h2 id="环境搭建"><a href="#环境搭建" class="headerlink" title="环境搭建"></a>环境搭建</h2><p>以大家优秀的编码水平一般不会写出性能堪忧的程序，所以在此我们使用一个 GitHub 上开源的<a href="https://github.com/FarmerChillax/go-pprof-practice">项目</a>，这个项目预埋了许多炸弹代码。这个性能堪忧的“炸弹”可以有效的帮助我们观测到程序的性能问题。</p><blockquote><p>务必确保你是在个人机器上运行“炸弹”的，能接受机器死机重启的后果（虽然这发生的概率很低）。请你务必不要在危险的边缘试探，比如在线上服务器运行这个程序。</p></blockquote><h3 id="图形化依赖安装"><a href="#图形化依赖安装" class="headerlink" title="图形化依赖安装"></a>图形化依赖安装</h3><p>pprof 默认提供命令行的方式来查看各项数据，但命令行的方式显然不够直观。因此我们安装一个图形化的依赖（<a href="https://graphviz.gitlab.io/download/">graphviz</a>）来更直观的展示堆栈信息。</p><p>你可以在<a href="https://graphviz.gitlab.io/download/">官网</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">brew install graphviz # macos</span><br><span class="line">apt install graphviz # ubuntu</span><br><span class="line">yum install graphviz # centos</span><br></pre></td></tr></table></figure><h3 id="获取炸弹-💣"><a href="#获取炸弹-💣" class="headerlink" title="获取炸弹 💣"></a>获取炸弹 💣</h3><p>该炸弹你可以通过 <code>git</code> 克隆下来，再按照一般的 Go 项目方式运行。为了演示的方便我这使用 <code>go get</code> 的方式展示，注意加上 <code>-d</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">go get -d github.com/FarmerChillax/go-pprof-practice</span><br><span class="line">cd $GOPATH/src/github.com/FarmerChillax/go-pprof-practice</span><br></pre></td></tr></table></figure><h2 id="指标查看"><a href="#指标查看" class="headerlink" title="指标查看"></a>指标查看</h2><p>保持程序的运行，然后打开浏览器访问 <code>http://127.0.0.1:6060/debug/pprof/</code>，可以看到如下页面：</p><!-- ![pprof_web](./pprof_web.jpg) --><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/pprof_web.jpg" class="" title="pprof web"><p>页面上展示了程序运行采样数据，分别有：</p><table><thead><tr><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>allocs</td><td>内存分配情况的采样信息</td></tr><tr><td>blocks</td><td>阻塞操作情况的采样信息</td></tr><tr><td>cmdline</td><td>显示程序启动命令及参数</td></tr><tr><td>goroutine</td><td>当前所有协程的堆栈信息</td></tr><tr><td>heap</td><td>堆上内存使用情况的采样信息</td></tr><tr><td>mutex</td><td>锁争用情况的采样信息</td></tr><tr><td>profile</td><td>CPU 占用情况的采样信息</td></tr><tr><td>threadcreate</td><td>系统线程创建情况的采样信息</td></tr><tr><td>trace</td><td>程序运行跟踪信息</td></tr></tbody></table><p>由于直接阅读采样信息缺乏直观性，我们需要借助 <code>go tool pprof</code> 命令来排查问题，这个命令是 go 原生自带的，所以不用额外安装。</p><h2 id="CPU-占用过高"><a href="#CPU-占用过高" class="headerlink" title="CPU 占用过高"></a>CPU 占用过高</h2><p>首先看一下 CPU 的运行情况，打开管理器可以看到此项目在我电脑上占用了 <code>63.3%</code> 的 CPU。</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/cpu_pre.jpg" class="" title="cpu_pre"><p>这显然是有问题的，我们使用 <code>go tool pprof</code> 来采集 10 秒 CPU 数据排查下问题：</p><p><code>go tool pprof &quot;http://localhost:6060/debug/pprof/profile?seconds=10&quot;</code></p><blockquote><p>因为我们这里采集的是 profile 类型，因此需要等待一定的时间来对 CPU 做采样。你可以通过查询字符串中 seconds 参数来调节采样时间的长短（单位为秒）</p></blockquote><p>等待一会儿后，会进入一个交互式终端：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/cpu_terminal.jpg" class="" title="cpu_terminal"><p>输入 <code>top</code> 命令，查看 CPU 占用较高的调用：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/cpu_top.jpg" class="" title="cpu top"><p><strong>参数说明：</strong></p><table><thead><tr><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>flat</td><td>当前函数本身的执行耗时</td></tr><tr><td>flat%</td><td>flat 占 CPU 总时间的比例</td></tr><tr><td>sum%</td><td>上面每一行的 flat% 总和</td></tr><tr><td>cum</td><td>当前函数本身加上其周期函数的总耗时</td></tr><tr><td>cum%</td><td>cum 占 CPU 总时间的比例</td></tr></tbody></table><p>很明显 <code>Eat</code> 方法是造成 CPU 占用过高的原因，输入 <code>list Eat</code>，查看问题具体在代码的哪一个位置：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/list_eat.jpg" class="" title="list eat"><p>从输出结果里可以看到对应的文件为 <code>animal/felidae/tiger/tiger.go</code>，而且具体的代码行为 24 行的一百亿次 for 循环导致的。</p><p>还记得我们一开始安装的 graphviz 图形依赖吗？在安装这个工具后，我们可以通过 <code>web</code> 命令来生成一个可视化的页面：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/cpu_web.jpg" class="" title="cpu web"><blockquote><p>注意，虽然这个命令叫 web，但它实际上是生成一个 <code>.svg</code> 文件，并调用系统默认打开它。如果你的系统打开 <code>.svg</code> 默认不是浏览器，这时候你需要设置下默认使用浏览器打开，或者使用你喜欢的查看方式来查看 svg 文件</p></blockquote><p>上图中，<code>Eat</code> 函数的框特别大，箭头特别粗，就是代表这个函数的 CPU 占用很高（pprof 生怕你不知道.jpg）。到这为止我们已经发现了 cpu 占用过高的原因了，我们修复下这个问题：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/fix_cpu.jpg" class="" title="fix cpu"><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/cpu_result.jpg" class="" title="fix cpu result"><h2 id="内存占用过高（Heap）"><a href="#内存占用过高（Heap）" class="headerlink" title="内存占用过高（Heap）"></a>内存占用过高（Heap）</h2><p>经过改造，可以发现CPU的问题已经解决了，但是内存使用还是很高，我们需要继续排查内存问题。</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/heap_pre.jpg" class="" title="pre mark heap"><p>上面我们介绍了命令行与 web 页面两种方式，因为 web 页面可视化的方式排查比较直观，因此命令行排查的方式就不再展开了，输入以下命令可以看到堆内存的占用情况：</p><p><code>go tool pprof -http=:8080 &quot;http://localhost:6060/debug/pprof/heap&quot;</code></p><blockquote><p>这个命令中 http 选项将会启动一个 web 服务器并自动打开网页。其值为 web 服务器的 endpoint</p></blockquote><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/heap_web.jpg" class="" title="heap web"><p>从上图我们可以看出 <code>Mouse</code> 这个对象的 <code>Steal</code> 方法占用的内存最多，然后我们点击 <code>view</code> -&gt; <code>source</code> 还可以查看到具体的代码文件及其行数：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/heap_source.jpg" class="" title="heap source"><p>采样说明，<code>SAMPLE</code> 菜单中有几个选项，对应说明如下：</p><table><thead><tr><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>alloc_objects</td><td>程序累计申请的对象数</td></tr><tr><td>alloc_space</td><td>程序累计申请的内存大小</td></tr><tr><td>inuse_objects</td><td>程序当前持有的对象数</td></tr><tr><td>inuse_space</td><td>程序当前占用的内存大小</td></tr></tbody></table><p>从代码中可以看到这么高的内存占用是因为会一直向 m.buffer 里追加长度为 1 MiB 的数组，直到总容量到达 1 GiB 为止，因此我们注释掉相关代码来解决这个问题。<br>再次编译运行，查看内存占用：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/heap_result.jpg" class="" title="heap result"><blockquote><p>如果你发现程序运行时间长后，内存还是会升高。请不用担心，这是因为后面用于模拟内存泄漏的炸弹</p></blockquote><h2 id="协程（Goroutine）"><a href="#协程（Goroutine）" class="headerlink" title="协程（Goroutine）"></a>协程（Goroutine）</h2><p>虽然 Go 是带 GC 的，一般不会发生内存泄漏。但凡事都有例外，<code>goroutine</code> 泄露也会导致内存泄露。我们在浏览器 debug 页面能看到此时程序的协程数有 106 条，虽然 106 不多，但对这样一个小程序来说显然是不正常的。</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/goroutine_count_pre.jpg" class="" title="goroutine pre"><p>我们仍然以可视化的方式来排查这个问题，输入以下命令查看堆内存占用情况：</p><p><code>go tool pprof -http=:8080 &quot;http://localhost:6060/debug/pprof/goroutine&quot;</code></p><p><code>Graph</code> 类型我们在上面已经介绍过了方块与箭头的关系，相信你也一定能理解这里的关系。那么我们这次来看看 <code>Flame Graph</code> 类型，点击 <code>view</code> -&gt; <code>Flame Graph</code>：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/goroutine_web.jpg" class="" title="goroutine web"><p>我们在火焰图中可以看到 <code>wolf.(*Wolf).Drink.func1</code> 这个函数占了总 goroutine 数量的 95%，我们还是像排查内存一样切换的 <code>source</code> 页面，查看代码具体位置：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/goroutine_source.jpg" class="" title="goroutine source"><p>可以看到，Drink 方法每次会起 10 个协程，每个协程会 sleep 30 秒再推出，而 Drink 函数又被反复的调用，这才导致了大量的协程泄漏。试想一下，如果我们业务中起的协程会永久阻塞，那么泄漏的协程数量便会持续增加，从而导致内存的持续增加，那么迟早会被 OS Kill 掉。我们通过注释掉问题代码，重新运行可以看到协程数量已经降低到个位数的水平了。</p><h2 id="Mutex（锁）"><a href="#Mutex（锁）" class="headerlink" title="Mutex（锁）"></a>Mutex（锁）</h2><p>如果你跟着本文一步步走下来，到此为止我们可以说已经完成了拆蛋的工作了，这个程序的所有资源占用问题已经解决了。但日常业务中排查问题除了资源占用问题外，还有性能问题。</p><p>接下来我们进一步排查性能问题，首先能想到的便是<strong>不合理的锁争用</strong>的问题，比如加锁时间太长等等。我们重新看一下 debug 页面，虽然协程数量已经大幅度降低，但还显示有一个 <code>mutex</code> 争用问题：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/mutex_pre.jpg" class="" title="goroutine source"><p>相信看到这里你已经触类旁通了，通过 Graph 查看问题出现的函数，然后通过 <code>source</code> 定位具体的代码。还是和之前一样，打开可视化页面：</p><p><code>go tool pprof -http=:8080 http://localhost:6060/debug/pprof/mutex</code></p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/mutex_pre_web.jpg" class="" title="mutex web"><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/mutex_source.jpg" class="" title="mutex source"><p>可以看到，这个锁由主协程 Lock，并启动子协程去 Unlock，主协程会阻塞在第二次 Lock 这里等待子协程完成任务，但由于子协程足足睡眠了一秒，导致主协程等待这个锁释放足足等了一秒钟。我们对此处代码进行修改即可修复问题。</p><h2 id="Block（阻塞）"><a href="#Block（阻塞）" class="headerlink" title="Block（阻塞）"></a>Block（阻塞）</h2><p>在程序中除了锁的竞争会导致阻塞外，还有很多逻辑会导致阻塞。我们继续看 debug 页面会发现，这里仍有 2 个阻塞的操作：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/block_pre.jpg" class="" title="block pre mark"><p>阻塞不一定是有问题的，但为了保证程序的性能，我们还是要排查一下。还是上面的那三板斧：</p><p><code>go tool pprof -http=:8080 http://localhost:6060/debug/pprof/block</code></p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/block_web.jpg" class="" title="block web graph"><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/block_source.jpg" class="" title="block source"><p>可以看到这里不同于直接 sleep 一秒，这里是从一个 channel 里读数据时，发生了阻塞。直到这个 channel 在一秒后才有数据读出，因此这里会导致程序阻塞，而不是睡眠。<br>我们对此代码注释掉，重新编译运行后发现程序还有一个 block。通过排查分析后我们发现是因为程序提供了 HTTP 的 pprof 服务，程序阻塞在对 HTTP 端口的监听上，因此这个阻塞是正常的。</p><h2 id="基准比对的排查方式"><a href="#基准比对的排查方式" class="headerlink" title="基准比对的排查方式"></a>基准比对的排查方式</h2><p>pprof 中有一个 <code>-base</code> 选项，它用于指定基准采样文件，这样可以通过比较两个采样数据，从而查看到指标的变化，例如函数的 CPU 使用时间或内存分配情况。</p><p>举个具体的例子，在业务中有一个低频调用的接口存在内存泄漏（OOM），它每被调用一就会泄漏 1MiB 的内存。这个接口每天被调用10次。假设我们给这个服务分配了 100Mi 空余的内存，也就是说这个接口基本上每十天就会挂一次，但当我们排查问题的时候，会发现内存是缓慢增长的。此时如果你仅通过 pprof 采样单个文件来观察，基本上很难会发现泄漏点。</p><p>这时候 <code>base</code> 选项就派上用场了，我们可以在服务启动后采集一个基准样本，过几天后再采集一次。通过比对这两个样本增量数据，我们就很容易发现出泄漏点。</p><p>同样的，这个炸弹我也已经预埋了这样一个缓慢的泄漏点，但时间我缩短了一下。相信在上面的实操过程中你也发现了端倪，下面我们开始实操一下。</p><p>我们运行这个炸弹程序，将启动时的堆内存分配情况保存下来，你可以在 debug 页面点击下载，也可以在终端中执行 <code>curl -o heap-base http://localhost:6060/debug/pprof/heap</code> 来下载。</p><p>在资源管理器中我们可以看到程序刚启动的时候，内存占用并不高：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/base_pre.jpg" class="" title="base pre"><p>在程序运行了半分钟后，我们可以清楚的发现程序内存开始逐渐增长：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/base_pre_target.jpg" class="" title="base pre target"><p>此时我们再执行 <code>curl -o heap-target http://localhost:6060/debug/pprof/heap</code> 获取到当前的采样数据。</p><blockquote><p>在 debug 页面中直接点击 heap 链接会打开一个新的页面，你可以通过删除该页面 URL 中的 debug&#x3D;1 这个查询字符串来下载数据文件</p></blockquote><p>再获取到两个样本数据后，我们通过 <code>base</code> 选项将 <code>heap-base</code> 作为基准，查看运行的这段时间内哪里内存增长了</p><p><code>go tool pprof -http=:8080 -base heap-base heap-target</code></p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/base_web.jpg" class="" title="base web compare"><p>显然在这段时间中 <code>mouse</code> 的 <code>Pee</code> 方法增长了 1.20GB，显然这就是内存泄漏的地方，接下来还是通过那三板斧来定位出问题的代码，然后注释修复：</p><img src="/2023/07/04/Go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/base_source.jpg" class="" title="base source"><p>这里主要展示了如果通过 <code>base</code> 参数比对两个 pprof 采样文件，从而提高我们排查问题的效率。</p><blockquote><p>在 Graph 页面你可能会发现有一些绿色框框存在，机智的你肯定也能猜到绿色框框代表的就是减少的使用量</p></blockquote><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文主要内容为 pprof 工具的使用，介绍了通过命令行、可视化等方式进行排查。虽然例子比较简单，但是相信通过这些简单的例子可以让你不在畏惧 pprof。</p><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><ul><li><a href="https://blog.wolfogre.com/posts/go-ppof-practice/">golang pprof 实战</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;!-- ## 前言 --&gt;

&lt;p&gt;对于 Golang 程序性能分析来说，pprof 一定是一个大杀器般的存在。主要可以分析 CPU、内存的使用情况、阻塞情况、Goroutine 的堆栈信息以及锁争用情况等性能问题。&lt;/p&gt;
&lt;p&gt;pprof 是一个性能分析工具，Go 在语言层</summary>
      
    
    
    
    <category term="学了些什么" scheme="https://blog.farmer233.top/categories/%E5%AD%A6%E4%BA%86%E4%BA%9B%E4%BB%80%E4%B9%88/"/>
    
    
    <category term="Golang" scheme="https://blog.farmer233.top/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>生，活</title>
    <link href="https://blog.farmer233.top/2022/03/15/%E7%94%9F%EF%BC%8C%E6%B4%BB/"/>
    <id>https://blog.farmer233.top/2022/03/15/%E7%94%9F%EF%BC%8C%E6%B4%BB/</id>
    <published>2022-03-15T08:27:37.000Z</published>
    <updated>2026-04-22T08:30:03.598Z</updated>
    
    <content type="html"><![CDATA[<p>晃眼间大半年的休闲时光过去了，平淡而消沉地惶惶不可终日。混吃等死四年多，有可能即将面对人生的又一次十字路口，思量再三，发现终归是凡夫俗子一介草民，个人的选择终究会湮没在历史的进程中，索性随波逐流，逃离现场，美其名曰无欲无求，人各有志。</p><p>越来越多的故人失去联系，仅能从偶尔的一条朋友圈窥得一丝身影，惊然发现竟恍如隔世，不知不觉间发现自己正一步步的走向荒原、孤冢。愈发感觉到天机命途，不由我定，唯一能做的就是活着，好好的活着而已。</p><p>生活生活，先生后活。如此生活，别人与我，自有期望。</p><h2 id="不要败给生活"><a href="#不要败给生活" class="headerlink" title="不要败给生活"></a>不要败给生活</h2><p>我相信，慢慢我什么都会有的。</p><p>我会有很多懂我又不懂我的朋友，我会有很多不同姓的家人，我会见到很多风景，也会明白冷暖自知的人间苦楚。我有我的快乐、烦恼，我一步一步地走。</p><p>许久之后，我再回望我的青春时光，那样地生活过。他的身影印在这个城市里，我看见了他那时的理想和热血再一刻不停地催促着他向前走去。</p><h2 id="朝悟道，夕死可矣"><a href="#朝悟道，夕死可矣" class="headerlink" title="朝悟道，夕死可矣"></a>朝悟道，夕死可矣</h2><p>少年心气，已经深刻地认识到了我们自身的平庸，却依然在等待着生活中的诸多无意义。我在路上，不知是在企图逃离现实所赋予我的诸多责任，还是尝试寻找自己所信奉的东西。</p><p>我们这一代人，如此开放却又狭隘，精神如此丰富又贫瘠，如此乐观又封闭，如此喧嚣却又静寂。我们这群寂寞的人类都在忙着说话、交流、呐喊，然后在现实到来的那一刹那成长。我们眼界开阔，却又因为各种价值观，社会压力中被压得踹不过气来。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;晃眼间大半年的休闲时光过去了，平淡而消沉地惶惶不可终日。混吃等死四年多，有可能即将面对人生的又一次十字路口，思量再三，发现终归是凡夫俗子一介草民，个人的选择终究会湮没在历史的进程中，索性随波逐流，逃离现场，美其名曰无欲无求，人各有志。&lt;/p&gt;
&lt;p&gt;越来越多的故人失去联系，</summary>
      
    
    
    
    <category term="胡思乱语" scheme="https://blog.farmer233.top/categories/%E8%83%A1%E6%80%9D%E4%B9%B1%E8%AF%AD/"/>
    
    
    <category term="胡言乱语" scheme="https://blog.farmer233.top/tags/%E8%83%A1%E8%A8%80%E4%B9%B1%E8%AF%AD/"/>
    
    <category term="emotion" scheme="https://blog.farmer233.top/tags/emotion/"/>
    
  </entry>
  
  <entry>
    <title>手把手教你用APIFlask做毕业设计后端—基本简介与Hello APIFlask!(一)</title>
    <link href="https://blog.farmer233.top/2022/03/09/%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0%E7%94%A8APIFlask%E5%81%9A%E6%AF%95%E4%B8%9A%E8%AE%BE%E8%AE%A1%E2%80%94%E2%80%94%E5%90%8E%E7%AB%AF/"/>
    <id>https://blog.farmer233.top/2022/03/09/%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0%E7%94%A8APIFlask%E5%81%9A%E6%AF%95%E4%B8%9A%E8%AE%BE%E8%AE%A1%E2%80%94%E2%80%94%E5%90%8E%E7%AB%AF/</id>
    <published>2022-03-09T11:52:49.000Z</published>
    <updated>2026-04-22T08:30:03.590Z</updated>
    
    <content type="html"><![CDATA[<!-- **『注：本文更新于2022年3月15日』** --><blockquote><p>本文项目对应地存储库: <a href="https://github.com/Farmer-chong/HelloAPIFlask">https://github.com/Farmer-chong/HelloAPIFlask</a></p></blockquote><p>最近正值毕业阶段，相信许多人的毕业设计都无不例外的选择了Web相关的内容，毕竟这个比较容易实现。我发现许多人都出于从众心理无不例外的使用了Spring全家桶，Spring有着很庞大的生态、久远的历史以及很丰富的社区资料，这使得它被许多从业者使用。但也正是这些优势导致它对新手或者快速上手开发变得十分不好，再加之国内的毒瘤技术社区等原因，使得一个小问题往往需要花费大量的时间来排除错误的答案。</p><p>相比于繁重、学习成本较高的Spring，Flask其简单的语法可以是我们快速的上手完成(应对)毕设这一类学业任务, 因此本文仅停留在 <strong>能用</strong> 阶段, 更多底层原理需要读者自行了解(这里推荐一下李大的狗书<a href="https://helloflask.com/book/1/">《Flask Web 开发实战 》</a>) 另外出于为社区做贡献等情怀原因也是写这一系列的初衷。</p><h2 id="系列文章"><a href="#系列文章" class="headerlink" title="系列文章"></a>系列文章</h2><ol><li><a href="./">后端系列: 基本简介与Hello APIFlask! (一)</a></li></ol><!-- 2.  --><h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><blockquote><p>本文默认使用Windows进行开发，Linux&#x2F;macOS会特别说明</p></blockquote><blockquote><p>相比于传统的后端渲染模板架构，本文采用目前主流的前后端分离结构</p></blockquote><p>对于前后端分离，服务端只需要提供API接口来处理请求。简单的来说就是前端不再局限于浏览器，还可以是app、小程序亦或者是爬虫程序。对后端而言，我们不再渲染html返回给前端，而是与前端共同约定一个数据格式进行交互。</p><p>如果读者的思想还停留在具体的浏览器中，那需要读者打开思维的局限来理解。无论是传统的后端渲染还是前后端分离，其本质都是操作 <strong>HTTP报文</strong> 。</p><blockquote><p>许多人此时会有个疑问：为什么一会儿Flask一会APIFlask的，这两者有什么关系？</p></blockquote><p>先来简单的说一下，<code>Flask</code>是一个十分流行的 <strong>Python Web</strong> 框架。而<code>APIFlask</code>则是基于<code>Flask</code>框架针对 <strong>Web API</strong>开发的特点二次开发的框架，这是一个针对API而生的框架(或者具体点前后端分离)。如果读者有<code>Java</code>开发经验，可以理解类似于<code>Spring</code>与<code>Spring Boot</code>的关系。</p><p>扩展阅读: </p><ol><li><a href="https://greyli.com/hello-apiflask/">APIFlask是什么</a></li><li><a href="https://greyli.com/flask-fastapi/">Flask、APIFlask与其他Web框架之间的关系</a></li></ol><p><em>因为APIFlask是一个Flask的扩展补充框架，因此它大部分都是与Flask保持一致的，你甚至可以把APIFlask当成Flask来写(当然这并不推荐)。因此对Flask有经验的读者许多内容是可以跳过阅读的，我甚至更推荐你去阅读 <a href="https://apiflask.com/">APIFlask的官方文档</a>。</em></p><h2 id="初识APIFlask-Hello-APIFlask"><a href="#初识APIFlask-Hello-APIFlask" class="headerlink" title="初识APIFlask(Hello APIFlask)"></a>初识APIFlask(Hello APIFlask)</h2><p>废话放在后面说，作为快速上手教程，理所应当地先来个Hello World感受一下<code>Flask</code>的魅力，作为一篇保姆级教程下面就让从<code>pip</code>这个包管理器开始安装一下<code>APIFlask</code>吧（默认安装好python及其包管理器）</p><h3 id="快速开始"><a href="#快速开始" class="headerlink" title="快速开始"></a>快速开始</h3><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">pip install apiflask # Linux环境用pip3</span><br></pre></td></tr></table></figure><p>当然本人还是强烈建议使用虚拟环境哈。细节就不多赘述了，下面开始写一个Hello World吧:</p><figure class="highlight python"><figcaption><span>hello/app.py</span></figcaption><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="keyword">from</span> apiflask <span class="keyword">import</span> APIFlask</span><br><span class="line">app = APIFlask(__name__)</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.route(<span class="params"><span class="string">&#x27;/&#x27;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">index</span>():</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&#x27;Hello apiflask!&#x27;</span></span><br></pre></td></tr></table></figure><p>哪怕你没有开发经验，单纯的看单词也许已经猜到它是做什么了，下面就来一步步的分解这个程序。</p><h3 id="创建应用实例-application-对象"><a href="#创建应用实例-application-对象" class="headerlink" title="创建应用实例(application 对象)"></a>创建应用实例(application 对象)</h3><p>我们使用<code>pip</code>安装<strong>apiflask</strong>，其实也就是下载了一个叫<strong>apiflask</strong>的包。因此我们可以通过<strong>apiflask</strong>包的构造文件导入开发的类和函数。我们一开始从<strong>apiflask</strong>包导入<strong>APIFlask</strong>类，实例化这个类，就得到了我们的程序实例<code>app</code>:</p><figure class="highlight python"><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> apiflask <span class="keyword">import</span> APIFlask</span><br><span class="line">app = APIFlask(__name__)</span><br></pre></td></tr></table></figure><p>传入<strong>apiflask</strong>的第一个参数是模块或者包名词，这里使用Python的特殊变量<code>__name__</code>。</p><h3 id="注册路由"><a href="#注册路由" class="headerlink" title="注册路由"></a>注册路由</h3><blockquote><p>首先对于很多初学者来说都会有这么一个问题：<strong>路由是什么？</strong></p></blockquote><p>回答这个问题之前我们先来看看一个传统的Web应用，客户端和服务器上的程序是如何交互的:</p><ol><li>用户在浏览器输入URL访问某个资源</li><li>APIFlask接收用户请求并分析请求的URL</li><li>APIFlask为这个URL找到对应的处理函数</li><li>执行函数并生成响应，然后返回给浏览器</li><li>浏览器接收并解析响应</li></ol><p>而我们现在要做前后端分离，整个流程和上面是一致的，但客户端具体的对象则从浏览器变成了一段发起 http 请求的代码，以网页前后端分离为例稍微完善一下如下:</p><ol><li>前端使用AJAX向后端发起请求</li><li>APIFlask接收请求并分析请求的URL</li><li>APIFlask为这个URL找到对应的处理函数</li><li>执行函数并生成 HTTP 响应</li><li>AJAX接收响应</li><li>根据后续的业务代码进行页面的修改</li></ol><blockquote><p><em>⚠⚠注意⚠⚠</em> 这里的 <strong>AJAX</strong> 是指 <strong>Asynchronous JavaScript + XML（异步JavaScript和XML）</strong> 这是一个技术名词，读者需要注意与较多人熟知的<code>jQuery</code>库的<code>ajax</code>方法进行区分！！！<br>简单的来讲，<code>jQuery</code>的<code>ajax</code>是 <strong>AJAX</strong> 技术的一个实现，有着许多同类的替代品，比如<code>axios</code>。由于笔者见过太多的人将两者混淆，故在此着重强调。</p></blockquote><p>这些步骤中，大部分都是由APIFlask完成，我们只需要将<strong>处理函数与URL对应起来</strong>。只需在函数添加<code>app.route()</code>装饰器，并传入URL规则作为参数，就可以让URL与函数建立关系。这个过程我们称为注册路由(route)，路由复制管理URL和函数之间的映射，而这个函数则被称为视图函数(view function)。</p><h4 id="1-绑定多个URL"><a href="#1-绑定多个URL" class="headerlink" title="1. 绑定多个URL"></a>1. 绑定多个URL</h4><p>一个视图函数可以绑定多个URL，具体就像下面这段代码:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@app.route(<span class="params"><span class="string">&#x27;/&#x27;</span></span>)</span></span><br><span class="line"><span class="meta">@app.route(<span class="params"><span class="string">&#x27;/hello&#x27;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">index</span>():</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&#x27;Hello apiflask!&#x27;</span></span><br></pre></td></tr></table></figure><h4 id="2-动态URL"><a href="#2-动态URL" class="headerlink" title="2. 动态URL"></a>2. 动态URL</h4><p>我们还可以在URL规则中添加变量部分, 使用<code>&lt;变量名&gt;</code>的形式表示。APIFlask处理请求时会把变量传入视图函数，代码如下:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@app.route(<span class="params"><span class="string">&#x27;/hello/&lt;name&gt;&#x27;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">hello</span>(<span class="params">name:<span class="built_in">str</span></span>):</span><br><span class="line">    <span class="keyword">return</span> <span class="string">f&#x27;Hello <span class="subst">&#123;name&#125;</span>!&#x27;</span></span><br></pre></td></tr></table></figure><p>如果URL变量中包含变量，但如果又要适配没有变量的情况，比如与多个URL配合起来使用，那么我们可以给一个默认值:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@app.route(<span class="params"><span class="string">&#x27;/hello&#x27;</span>, defaults=&#123;<span class="string">&#x27;name&#x27;</span>: <span class="string">&#x27;farmer&#x27;</span>&#125;</span>)</span></span><br><span class="line"><span class="meta">@app.route(<span class="params"><span class="string">&#x27;/hello/&lt;name&gt;&#x27;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">say_hello</span>(<span class="params">name:<span class="built_in">str</span></span>):</span><br><span class="line">    <span class="keyword">return</span> <span class="string">f&#x27;Hello <span class="subst">&#123;name&#125;</span>!&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="启动开发服务器"><a href="#启动开发服务器" class="headerlink" title="启动开发服务器"></a>启动开发服务器</h3><p>Flask内置了一个开发服务器(由依赖包Werkzeug)提供，足够在开发和测试阶段使用，而APIFlask是基于Flask二次开发的，因此我们一样可以使用。</p><blockquote><p>在生产环境需要使用性能够好的生产服务器，以提升安全和性能，具体更多内容移步到<a href="https://helloflask.com/book/1/">《Flask Web 开发实战 》</a></p></blockquote><p>在项目的<strong>根目录</strong>下，输入下面这条命令:</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">flask run</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">* Tip: There are .env or .flaskenv files present. Do &quot;pip install python-dotenv&quot; to use them.</span><br><span class="line">* Environment: production</span><br><span class="line">  WARNING: This is a development server. Do not use it in a production deployment.</span><br><span class="line">  Use a production WSGI server instead.</span><br><span class="line">* Debug mode: off</span><br><span class="line">* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)</span><br></pre></td></tr></table></figure><p>这时候我们在浏览器访问这个URL，会看到这样显示:</p><img src="/2022/03/09/%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0%E7%94%A8APIFlask%E5%81%9A%E6%AF%95%E4%B8%9A%E8%AE%BE%E8%AE%A1%E2%80%94%E2%80%94%E5%90%8E%E7%AB%AF/index.png" class="" title="hello_apiflask"><p>同理，其他URL自行替换即可得到相应的结果。但由于我们开发的是前后端分离架构，后端只负责提供API进行访问，除了基本的<code>GET</code>请求外还有大量其他请求报文。因为浏览器的调试无疑是不够的，为此<code>APIFlask</code>提供了交互式API文档(基于<strong>Swagger UI</strong> and <strong>Redoc</strong>)</p><p>要进入这个交互式API文档，默认的方式是访问<code>/docs</code>路由，为此我们重新在浏览器访问: <a href="http://127.0.0.1:5000/docs%EF%BC%8C%E5%8D%B3%E5%8F%AF%E7%9C%8B%E5%88%B0%E4%B8%8B%E9%9D%A2%E7%9A%84%E8%BF%99%E4%B8%AA%E9%A1%B5%E9%9D%A2">http://127.0.0.1:5000/docs，即可看到下面的这个页面</a>:</p><img src="/2022/03/09/%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0%E7%94%A8APIFlask%E5%81%9A%E6%AF%95%E4%B8%9A%E8%AE%BE%E8%AE%A1%E2%80%94%E2%80%94%E5%90%8E%E7%AB%AF/swagger.png" class="" title="api_docs"><p>后续都会基于该API文档进行演示，当然你也可以访问: <a href="http://127.0.0.1:5000/redoc">http://127.0.0.1:5000/redoc</a> 查看基于Redoc生成的API文档。</p><blockquote><p>需要注意，网上许多教程都是通过<code>app.run()</code>形式来运行开发服务器的，这种方式其实是旧的启动方式，已经过时了目前已不推荐使用(deprecated)。</p></blockquote><p>Flask通过依赖包Click内置了一个CLI(Command Line Interface, 命令行交互界面)系统。当我们安装Flask后，会自动添加一个flask命令脚本，我们可以通过flask命令执行内置命令、扩展插件的命令与我们自定义的命令。其中，<code>flask run</code>命令用来启动开发服务器。</p><p>此外，你还可以执行<code>flask --help</code>查看所有命令，这些命令都需要在项目的根目录执行。</p><h3 id="自动发现程序实例"><a href="#自动发现程序实例" class="headerlink" title="自动发现程序实例"></a>自动发现程序实例</h3><p>上面启动开发服务器我们使用了<code>flask run</code>来启动，这就不禁让人好奇，<strong>Flask</strong>是如何找到程序的？</p><p>其实这个问题是因为<strong>Flask</strong>会自动探测程序实例，一般来说，在执行<code>flask run</code>命令运行程序前，我们需要提供程序实例所在模块的位置，而自动探测是按照下面这些规则:</p><ol><li>从当前目录寻找<code>app.py</code>和<code>wsgi.py</code>模块，并从中寻找名为<code>app</code>或<code>application</code>的程序实例</li><li>从环境变量<strong>FLASK_APP</strong>对应的模块名&#x2F;导入路径寻找名为<code>app</code>或<code>application</code>的程序实例</li></ol><p>因为我们上面的程序代码文件名为<code>app.py</code>，所以<code>flask run</code>命令会自动在其中寻找应用实例。如果你的程序文件名是其他名称，比如<code>hello_apiflask.py</code>，那么需要设置环境变量<strong>FLASK_APP</strong>，将包含程序实例的模块名赋值给这个变量。命令如下:</p><figure class="highlight routeros"><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">set</span> <span class="attribute">FLASK_APP</span>=hello_apiflask</span><br></pre></td></tr></table></figure><p>Linux或macOS系统使用<code>export</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">export FLASK_APP=hello_apiflask</span><br></pre></td></tr></table></figure><h3 id="管理环境变量"><a href="#管理环境变量" class="headerlink" title="管理环境变量"></a>管理环境变量</h3><p><strong>FLASK</strong>的自动发现程序实例机制还有第三条规则: 如果安装了<strong>python-dotenv</strong>，那么在使用<code>flask run</code>或其他命令时会自动使用它从<code>.flaskenv</code>文件和<code>.env</code>文件中加载环境变量。</p><p>安装<strong>python-dotenv</strong>包命令如下:</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">pip install python-dotenv # Linux环境用pip3</span><br></pre></td></tr></table></figure><p>除了管理自动发现程序实例外，我们还可以用来管理程序需要的环境变量。我们在项目根目录下分别创建两个文件:<code>.env</code>和<code>.flaskenv</code>，其中<code>.flaskenv</code>用来管理和<strong>Flask</strong>相关的公开环境变量。而<code>.env</code>文件则用来管理包含敏感信息的环境变量。</p><blockquote><p><code>.env</code>包含敏感信息，除非是私有项目，否则绝对不能提交到Git仓库中</p></blockquote><h2 id="扩展阅读"><a href="#扩展阅读" class="headerlink" title="扩展阅读"></a>扩展阅读</h2><blockquote><p>为什么写扩展阅读，扩展阅读的作用?</p></blockquote><p>扩展阅读模块主要是对上面的内容进行补充，内容并非必读内容并不会影响程序的运行，更多是补充开发相关各方面的知识内容。</p><h3 id="配置热更新-重载器"><a href="#配置热更新-重载器" class="headerlink" title="配置热更新(重载器)"></a>配置热更新(重载器)</h3><p>开发环境(development environment)和生产环境(production environment)。根据运行环境的不同，<strong>Flask</strong>程序、扩展以及其他程序会改变相应的行为和设置。为了区分运行环境，<strong>Flask</strong>提供了一个<strong>FLASK_ENV</strong>环境变量用来设置环境，默认为<code>production</code>(生产)。在开发时我们可以将其设置为<code>development</code>，这会开启所有支持开发的特性，为了方便管理我们还可以将其写入到<code>.flaskenv</code>文件中:</p><figure class="highlight shell"><figcaption><span>./.flaskenv</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">FLASK_ENV=development</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><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">(env) E:\Github\HelloAPIFlask\demos\hello&gt;flask run</span><br><span class="line"> * Environment: development</span><br><span class="line"> * Debug mode: on</span><br><span class="line"> * Restarting with stat</span><br><span class="line"> * Debugger is active!</span><br><span class="line"> * Debugger PIN: 500-487-380</span><br><span class="line"> * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)</span><br></pre></td></tr></table></figure><p>在开发环境下，调试模式(Debug Mode)将被开启，这时执行<code>flask run</code>启动程序会自动激活<strong>Werkzeug</strong>内置的调试器(debugger)和重载器(reloader)，他们会为开发带来很大的帮助。</p><p><strong>如果你想单独控制调试模式开关，可以通过FLASK_DEBUG环境变量来设置，设为1为开启，设为0则关闭，不过通常不推荐手动设置这个值</strong></p><h3 id="创建虚拟环境"><a href="#创建虚拟环境" class="headerlink" title="创建虚拟环境"></a>创建虚拟环境</h3><blockquote><p>在 Python 中，虚拟环境(virtual enviroment)就是隔离的 Python 解释器环境。通过创建虚拟环境，你可以拥有一个独立的 Python 解释器环境。这样做的好处是可以为每一个项目创建独立的 Python 解释器环境，因为不同的项目经常会依赖不同版本的库或Python版本。使用虚拟环境可以保持全局 Python 解释器环境的干净，避免包和版本的混乱，并且可以方便地区分和记录每个项目的依赖，以便在新环境下复现依赖环境。</p></blockquote><p>虚拟环境通常使用<strong>Virtualenv</strong>来创建，除此之外还有<strong>Pipenv</strong>、<strong>PDM</strong>等诸多环境管理工具。简单起见，我们使用<strong>Python3</strong>自带的工具来创建虚拟环境，首先确保我们当前工作目录在项目的根目录，然后使用<code>python3 -m venv env</code>命令为当前项目创建虚拟环境:</p><figure class="highlight shell"><figcaption><span>Windows</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python -m venv env</span><br></pre></td></tr></table></figure><figure class="highlight shell"><figcaption><span>Linux</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python3 -m venv env</span><br></pre></td></tr></table></figure><p><strong>提示: Windows用户可直接复制，但Linux系统大多存在python2与python3，Linux用户需要注意这点</strong></p><p>这会在当前目录创建一个名为<strong>env</strong>的文件夹(命令最后一个选项为文件夹名)，其中包含隔离的Python解释器环境。</p><h3 id="激活虚拟环境"><a href="#激活虚拟环境" class="headerlink" title="激活虚拟环境"></a>激活虚拟环境</h3><p>在创建虚拟环境后，我们还需要载入(激活)这个环境，接下来使用<code>.\env\Script\active</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></pre></td><td class="code"><pre><span class="line">E:\Github\HelloAPIFlask&gt;.\env\Scripts\activate</span><br><span class="line"></span><br><span class="line">(env) E:\Github\HelloAPIFlask&gt;</span><br></pre></td></tr></table></figure><p>Linux环境使用<code>source</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></pre></td><td class="code"><pre><span class="line">farmer@farmer-ubuntu:~/HelloAPIFlask$ source ./env/bin/activate</span><br><span class="line"></span><br><span class="line">(env) farmer@farmer-ubuntu:~/HelloAPIFlask$ source ./env/bin/activate</span><br></pre></td></tr></table></figure><p>可以看到虚拟环境激活后命令行前面多了一个虚拟环境的标识，这就表示虚拟环境激活啦！使用<code>pip list</code>我们可以看到该环境是一个全新的环境。</p><h3 id="更多启动项"><a href="#更多启动项" class="headerlink" title="更多启动项"></a>更多启动项</h3><ol><li>使服务器外部可见<br>我们上面启动的Web服务器默认是对外不可见的，我们可以通过<code>--host</code>选项将主机地址改为<code>0.0.0.0</code>使其对外部可见:</li></ol><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">flask run --host=0.0.0.0</span><br></pre></td></tr></table></figure><blockquote><p>需要注意这里的外部是指你计算机外部，并不是指公网。一般个人的电脑是没有公网IP(公有地址)，所以此时你的程序只能被局域网内的其他用户通过你电脑的IP(内网)进行访问。</p></blockquote><ol start="2"><li>改变默认端口</li></ol><p><strong>Flask</strong>提供的Web服务器默认监听5000端口，你可以通过<code>--port</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">flask run --port=2333</span><br></pre></td></tr></table></figure><p>此时会监听来自2333端口的请求，对应的程序网址也变成了 <a href="http://127.0.0.1:2333/%E3%80%82">http://127.0.0.1:2333/。</a></p><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><blockquote><p>这一切开始于2010年4月1日，<strong>Armin Ronacher</strong>在网上发布了一篇关于”下一代 Python 微框架”的介绍文章，文章里称这个 Denied 框架不依赖 Python 标准库，只需要复制一份 <code>deny.py</code> 放到你的项目文件夹就可以开始编程。随着一本正经的介绍、名人推荐语、示例代码和演示视频，这个”虚假“的项目让不少人信以为真。5天后 <strong>Flask</strong> 就从这么一个愚人节玩笑诞生了。</p></blockquote><p>同样的，<strong>APIFlask</strong>这个框架开始于 2021年4月1日，不知道是机缘巧合还是<a href="https://greyli.com/">Grey Li</a>有意为之的一个小玩笑，<strong>APIFlask</strong>与<strong>Flask</strong>均诞生于4月1日这一天。🎉🚀✨</p><p>在与<a href="https://greyli.com/">Grey Li</a>求证后得知两者选择同一天发布是有意为之的！！😄</p>]]></content>
    
    
      
      
    <summary type="html">&lt;!-- **『注：本文更新于2022年3月15日』** --&gt;


&lt;blockquote&gt;
&lt;p&gt;本文项目对应地存储库: &lt;a href=&quot;https://github.com/Farmer-chong/HelloAPIFlask&quot;&gt;https://github.com/Fa</summary>
      
    
    
    
    <category term="学了些什么" scheme="https://blog.farmer233.top/categories/%E5%AD%A6%E4%BA%86%E4%BA%9B%E4%BB%80%E4%B9%88/"/>
    
    
    <category term="python" scheme="https://blog.farmer233.top/tags/python/"/>
    
    <category term="笔记" scheme="https://blog.farmer233.top/tags/%E7%AC%94%E8%AE%B0/"/>
    
    <category term="web" scheme="https://blog.farmer233.top/tags/web/"/>
    
    <category term="Flask/APIFlask" scheme="https://blog.farmer233.top/tags/Flask-APIFlask/"/>
    
  </entry>
  
  <entry>
    <title>用Rust为Python加双翅膀</title>
    <link href="https://blog.farmer233.top/2022/01/15/%E7%94%A8Rust%E4%B8%BAPython%E5%8A%A0%E5%8F%8C%E7%BF%85%E8%86%80/"/>
    <id>https://blog.farmer233.top/2022/01/15/%E7%94%A8Rust%E4%B8%BAPython%E5%8A%A0%E5%8F%8C%E7%BF%85%E8%86%80/</id>
    <published>2022-01-15T14:22:42.000Z</published>
    <updated>2026-04-22T08:30:03.598Z</updated>
    
    <content type="html"><![CDATA[<p>最近在学习<code>Rust</code>，这是一门系统级语言。保证安全的同时摆脱了<code>GC</code>, 它很香同时也很难上手, 刚学完一点皮毛知识决定做个小玩意儿玩玩，是骡子是马总要拉出来溜溜~</p><p>目前个人认为<code>Rust</code>在性能上是可以和<code>C++</code>媲美的一门语言，既然如此那就用它来为<code>Python</code>加个速吧！😊</p><h2 id="起步准备"><a href="#起步准备" class="headerlink" title="起步准备"></a>起步准备</h2><p>为<code>Python</code>加速与写<code>C语言</code>扩展类似，最终通过<code>pyd</code>来调用。在此之前我们用到<code>Rust</code>的<code>pyo3</code>库，另外我们编写的是一个<code>lib</code>而不是应用程序，因此我们要创建<code>lib</code>项目。</p><p>创建<code>lib</code>库项目: </p><p><code>$ cargo new &lt;project name&gt; --lib</code></p><p>在 <strong>Cargo.toml</strong> 文件添加<code>pyo3</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><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="section">[lib]</span></span><br><span class="line"><span class="attr">name</span> = <span class="string">&quot;string_sum&quot;</span></span><br><span class="line"><span class="attr">crate-type</span> = [<span class="string">&quot;cdylib&quot;</span>]</span><br><span class="line"></span><br><span class="line"><span class="section">[dependencies.pyo3]</span></span><br><span class="line"><span class="attr">version</span> = <span class="string">&quot;0.15.1&quot;</span></span><br><span class="line"><span class="attr">features</span> = [<span class="string">&quot;extension-module&quot;</span>]</span><br></pre></td></tr></table></figure><h2 id="一个小Demo"><a href="#一个小Demo" class="headerlink" title="一个小Demo"></a>一个小Demo</h2><p>使用文档的例子，编写一个<code>Hello World</code>试试，代码如下: </p><figure class="highlight rust"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">use</span> pyo3::prelude::*;</span><br><span class="line"></span><br><span class="line"><span class="comment">/// Formats the sum of two numbers as string.</span></span><br><span class="line"><span class="meta">#[pyfunction]</span></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">sum_as_string</span>(a: <span class="type">usize</span>, b: <span class="type">usize</span>) <span class="punctuation">-&gt;</span> PyResult&lt;<span class="type">String</span>&gt; &#123;</span><br><span class="line">    <span class="title function_ invoke__">Ok</span>((a + b).<span class="title function_ invoke__">to_string</span>())</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/// A Python module implemented in Rust.</span></span><br><span class="line"><span class="meta">#[pymodule]</span></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">string_sum</span>(py: Python, m: &amp;PyModule) <span class="punctuation">-&gt;</span> PyResult&lt;()&gt; &#123;</span><br><span class="line">    m.<span class="title function_ invoke__">add_function</span>(wrap_pyfunction!(sum_as_string, m)?)?;</span><br><span class="line">    m.<span class="title function_ invoke__">add_function</span>(wrap_pyfunction!(test_func, m)?)?;</span><br><span class="line"></span><br><span class="line">    <span class="title function_ invoke__">Ok</span>(())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后用打包命令将这个项目build一下:<br><code>$ cargo build --release</code></p><p>这时候应该能看到项目的结构如下: </p><figure class="highlight 1c"><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><br><span class="line">├─src</span><br><span class="line">└─target</span><br><span class="line">    ├─release</span><br><span class="line">    │  ├─.fingerprint</span><br><span class="line">    │  ├─build</span><br><span class="line">    │  ├─deps</span><br><span class="line">    │  ├─examples</span><br><span class="line">    <span class="string">|  ├─string_sum.dll</span></span><br><span class="line">    <span class="string">|  ├─string_sum.d</span></span><br><span class="line">    <span class="string">|  ├─libstring_sum.dll.a</span></span><br><span class="line">    │  └─incremental</span><br><span class="line">    └─rls</span><br></pre></td></tr></table></figure><p>这里的<strong>string_sum.dll</strong>就是我们需要的，我们将文件的扩展名改成<code>pyd</code>即可得到我们的<code>python</code>扩展文件 -&gt; <code>string_sum.pyd</code></p><p>完成之后尝试一下调用这个文件，新建一个<code>test</code>文件夹并将其复制进去, 最后调用一下:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> string_sum</span><br><span class="line"><span class="built_in">print</span>(string_sum.sum_as_string(<span class="number">200</span>, <span class="number">33</span>))</span><br><span class="line"></span><br><span class="line"><span class="comment"># export: 233</span></span><br></pre></td></tr></table></figure><h2 id="CPU运算性能Demo"><a href="#CPU运算性能Demo" class="headerlink" title="CPU运算性能Demo"></a>CPU运算性能Demo</h2><p>继续在刚刚的文件中添加一个需要大量计算的函数:</p><figure class="highlight rust"><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></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="meta">#[pyfunction]</span></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">test_func</span>() <span class="punctuation">-&gt;</span> PyResult&lt;<span class="type">String</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">for</span> <span class="variable">a</span> <span class="keyword">in</span> <span class="number">0</span>..<span class="number">10</span> &#123;</span><br><span class="line">        <span class="keyword">for</span> <span class="variable">b</span> <span class="keyword">in</span> <span class="number">0</span>..<span class="number">10</span> &#123;</span><br><span class="line">            <span class="keyword">for</span> <span class="variable">c</span> <span class="keyword">in</span> <span class="number">0</span>..<span class="number">10</span> &#123;</span><br><span class="line">                <span class="keyword">for</span> <span class="variable">d</span> <span class="keyword">in</span> <span class="number">0</span>..<span class="number">10</span> &#123;</span><br><span class="line">                    <span class="keyword">for</span> <span class="variable">e</span> <span class="keyword">in</span> <span class="number">0</span>..<span class="number">10</span> &#123;</span><br><span class="line">                        <span class="keyword">for</span> <span class="variable">f</span> <span class="keyword">in</span> <span class="number">0</span>..<span class="number">10</span> &#123;</span><br><span class="line">                            <span class="keyword">if</span> <span class="title function_ invoke__">m</span>(a) + <span class="title function_ invoke__">m</span>(b) + <span class="title function_ invoke__">m</span>(c) + <span class="title function_ invoke__">m</span>(d) + <span class="title function_ invoke__">m</span>(e) + <span class="title function_ invoke__">m</span>(f)</span><br><span class="line">                                == a * <span class="number">10_0000</span> + b * <span class="number">1_0000</span> + c * <span class="number">1000</span> + d * <span class="number">100</span> + e * <span class="number">10</span> + f</span><br><span class="line">                            &#123;</span><br><span class="line">                                <span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125;&#123;&#125;&#123;&#125;&#123;&#125;&#123;&#125;&#123;&#125;&quot;</span>, a, b, c, d, e, f);</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><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_ invoke__">Ok</span>(<span class="type">String</span>::<span class="title function_ invoke__">new</span>());</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/// A Python module implemented in Rust.</span></span><br><span class="line"><span class="meta">#[pymodule]</span></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">string_sum</span>(py: Python, m: &amp;PyModule) <span class="punctuation">-&gt;</span> PyResult&lt;()&gt; &#123;</span><br><span class="line">    m.<span class="title function_ invoke__">add_function</span>(wrap_pyfunction!(test_func, m)?)?;</span><br><span class="line"></span><br><span class="line">    <span class="title function_ invoke__">Ok</span>(())</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>后续的步骤与上面一致，编译重命名后拷贝到<code>python</code>目录下，修改一下刚才的<code>python</code>代码:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><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="keyword">import</span> string_sum</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">target_func</span>():</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">m</span>(<span class="params">x</span>):</span><br><span class="line">        <span class="keyword">return</span> x * x * x * x * x</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> a <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>,<span class="number">10</span>): </span><br><span class="line">        <span class="keyword">for</span> b <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>,<span class="number">10</span>): </span><br><span class="line">            <span class="keyword">for</span> c <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>,<span class="number">10</span>): </span><br><span class="line">                <span class="keyword">for</span> d <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>,<span class="number">10</span>): </span><br><span class="line">                    <span class="keyword">for</span> e <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>,<span class="number">10</span>): </span><br><span class="line">                        <span class="keyword">for</span> f <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>,<span class="number">10</span>): </span><br><span class="line">                            <span class="keyword">if</span> m(a) + m(b) + m(c) + m(d) + m(e) + m(f) == a * <span class="number">10_0000</span> + b * <span class="number">1_0000</span> + c * <span class="number">1000</span> + d * <span class="number">100</span> + e * <span class="number">10</span> + f:</span><br><span class="line">                                <span class="built_in">print</span>(<span class="string">f&quot;<span class="subst">&#123;a&#125;</span><span class="subst">&#123;b&#125;</span><span class="subst">&#123;c&#125;</span><span class="subst">&#123;d&#125;</span><span class="subst">&#123;e&#125;</span><span class="subst">&#123;f&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">test_fun</span>(<span class="params">func, flage:<span class="built_in">str</span> = <span class="string">&quot;Python&quot;</span></span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;计时函数</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    Args:</span></span><br><span class="line"><span class="string">        func (function): 回调函数</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    start = time.time()</span><br><span class="line">    func()</span><br><span class="line">    end = time.time()</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;<span class="subst">&#123;flage&#125;</span> time: &quot;</span>,end - start)</span><br><span class="line">    <span class="keyword">return</span> start - end</span><br><span class="line"></span><br><span class="line"><span class="comment"># 开始计时 -&gt; Rust</span></span><br><span class="line">rust_time = test_fun(string_sum.test_func, flage=<span class="string">&quot;Rust&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 测试纯Python</span></span><br><span class="line">python_time = test_fun(target_func)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;Rate: <span class="subst">&#123;python_time/rust_time&#125;</span>&quot;</span>)</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>运行看一下结果，从结果看到<code>Rust</code>比<code>Python</code>足足快了<strong>125倍</strong>多！！！</p><img src="/2022/01/15/%E7%94%A8Rust%E4%B8%BAPython%E5%8A%A0%E5%8F%8C%E7%BF%85%E8%86%80/rust2python.png" class="" title="result">]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近在学习&lt;code&gt;Rust&lt;/code&gt;，这是一门系统级语言。保证安全的同时摆脱了&lt;code&gt;GC&lt;/code&gt;, 它很香同时也很难上手, 刚学完一点皮毛知识决定做个小玩意儿玩玩，是骡子是马总要拉出来溜溜~&lt;/p&gt;
&lt;p&gt;目前个人认为&lt;code&gt;Rust&lt;/code&gt;在</summary>
      
    
    
    
    <category term="学了些什么" scheme="https://blog.farmer233.top/categories/%E5%AD%A6%E4%BA%86%E4%BA%9B%E4%BB%80%E4%B9%88/"/>
    
    
    <category term="python" scheme="https://blog.farmer233.top/tags/python/"/>
    
    <category term="笔记" scheme="https://blog.farmer233.top/tags/%E7%AC%94%E8%AE%B0/"/>
    
    <category term="Rust" scheme="https://blog.farmer233.top/tags/Rust/"/>
    
  </entry>
  
  <entry>
    <title>校招杂谈</title>
    <link href="https://blog.farmer233.top/2021/12/08/%E7%A7%8B%E6%8B%9B%E6%9D%82%E8%B0%88/"/>
    <id>https://blog.farmer233.top/2021/12/08/%E7%A7%8B%E6%8B%9B%E6%9D%82%E8%B0%88/</id>
    <published>2021-12-08T12:09:49.000Z</published>
    <updated>2026-04-22T08:30:03.599Z</updated>
    
    <content type="html"><![CDATA[<p>秋招结束后就一直想写一篇总结，但个人比较懒躺平了好久:D。最近老师给我布置了个任务写一下秋招这段时间的面试经验总结, 就不继续鸽了吧😄 继回顾完大学生涯后再来看看近段时间的校招悲剧吧 :(</p><p>如果不读研，校招可以说每个人一生只有一次，当你毕业了也就失去应届生这个身份了也就不能参与校招了。因此校招这个机会是十分重要且难得的，一定要尽早准备参加，重视重视再重视！！！⚠️ </p><p>切记 <strong>春招找实习，秋招拿offer</strong></p><p>虽然应届生身份法规上是两年内，但又有几个公司这么看尼～</p><h2 id="重要的东西写在前头"><a href="#重要的东西写在前头" class="headerlink" title="重要的东西写在前头"></a>重要的东西写在前头</h2><p>首先说一个思想上的几个误区：</p><ol><li>许多人认为没毕业就不能去应聘</li><li>我才大三，校招离我还远</li><li>学校大四还有课，找到了也去不了不如不去面试</li><li>我是前端，后端的那些就不复习了</li></ol><p>据我观察我们学校很多人都有这个误区，但其实并非这样。校招可以说当你踏入大三，就要开始准备了！另外对于面试官来说，应届生只有CS专业与非CS专业的两种情况，你前端还是后端只是决定了考察的重点或者问题方向罢了。下面就来详细的展开说说校招的这些事吧～</p><p>开始之前先介绍一下个人情况吧，看官还请结合自身实际参考阅读：</p><ol><li><code>Python</code>、<code>JavaScript</code>、<code>golang</code>这三门语言为我的主技术栈, 当然别的语言也会点。</li><li>框架方面：<code>Flask</code>、<code>APIflask</code>、<code>gin</code>、<code>Vue</code>等常见的web开发框架我都能熟练的使用</li><li>裸考软考网络工程师</li><li>有一颗热爱计算机、热爱折腾的心</li></ol><p>在编程方面，你应该有以下的基本素养：</p><ol><li>会写注释、会写文档</li><li>变量名清晰明了</li><li>函数功能划分清楚、职责单一（❌冗长的函数）</li><li>习惯看官方文档，拒绝看垃圾博客（如csdn）</li><li>接口协议设计合理、规范清晰</li><li>添加功能的时候多一点思考（如扩展性）</li><li>习惯写测试用例、单元测试（正所谓一行代码千行测试）</li><li>代码问题输出使用规范化的log记录</li><li>提交代码要规范</li></ol><h2 id="一个比较常规的应届生校招流程"><a href="#一个比较常规的应届生校招流程" class="headerlink" title="一个比较常规的应届生校招流程"></a>一个比较常规的应届生校招流程</h2><img src="/2021/12/08/%E7%A7%8B%E6%8B%9B%E6%9D%82%E8%B0%88/school-recruit.png" class="" title="recruit"><p>上图可以说是大学生应该知道的招聘流程，我以一个刚步入大三的学生身份来remake整个流程。是的，大三开学就需要开始准备校招事宜了！</p><h3 id="大三开学首先要做这几件事："><a href="#大三开学首先要做这几件事：" class="headerlink" title="大三开学首先要做这几件事："></a>大三开学首先要做这几件事：</h3><ol><li>制作简历</li><li>浏览意向的岗位资讯</li><li>开始复习八股文</li><li>开始刷题、刷算法</li></ol><p>在学期末的时候开始投递简历，应聘大厂的实习岗位。实习可以说是我们三本学生进入大厂为数不多的机会了，切记要把握好！</p><h3 id="大三第二个学期"><a href="#大三第二个学期" class="headerlink" title="大三第二个学期"></a>大三第二个学期</h3><p>常言道：春招找实习、秋招拿offer。这个学期开学一般都是3月份了，这时候你应该已经在寒假就完成简历的投递了。这个学期的主要任务就是：</p><ol><li>找到实习</li><li>继续八股文、算法的复习</li><li>了解提前批动态</li></ol><p>每年的提前批时间都不一样，需要自行上牛客网浏览相关资讯。关注大厂提前批的动态。</p><h3 id="大三与大四中间的暑假"><a href="#大三与大四中间的暑假" class="headerlink" title="大三与大四中间的暑假"></a>大三与大四中间的暑假</h3><p>这个暑假一般而言是在秋招应聘的实习公司与面试中度过的，争取拿到正式offer是这个时间段唯一目标！有实习经历与没有实习经历是有很大差距的！具体原因下文会讲到。</p><h3 id="大四"><a href="#大四" class="headerlink" title="大四"></a>大四</h3><p>大四开学就是9月了，也是正式的校招与秋招的开始。是厮杀的最火热的时间段。    </p><p>在这个时间段往往会遇到<strong>卡简历</strong>、<strong>卡学历</strong>等等的不公平待遇，这也是为什么说提前批争取上岸的原因。<br>这个时间段的笔试，除非你能答到基本满分，否则你可能连面试官都见不到。</p><p>至于笔试题的难度，按ACM标准来看就好了。至于秋招结束还没上岸或者拿不到保底，那只能希望来年的春招了。</p><h2 id="个人简历"><a href="#个人简历" class="headerlink" title="个人简历"></a>个人简历</h2><p>简历是十分重要的，最重要的是一个“真”字。内容一定要真实会就会，不会就不会。其次是“精”，切忌杂七杂八的东西往上丢，比如项目部分写学校的课程实训就很掉分。</p><p>最后需要注意里面的内容需要突出重点而不是一股脑的全丢上面。尽可能控制在一页内，主要包含你会的技术栈、能拿得出手的项目、经历与技能证书、自我评价这几个部分，下面详细展开来说。</p><h3 id="技术栈"><a href="#技术栈" class="headerlink" title="技术栈"></a>技术栈</h3><p>在技术栈上，经可能的突出与求职岗位所匹配的技术。另外需要注意描述技能掌握程度时的词语<strong>了解</strong>，<strong>熟悉</strong>和<strong>精通</strong>的区别。<br>“了解”指对某项技术只是全面学习过或看过书，但并没有做过实际的项目。一般不建议在简历中写只是肤浅地了解一点的技能，如：只是在我校Java课程中学过java，那就只能算了解。</p><p>在简历描述中的掌握程度大部分应该是“熟悉”，一般毕业生是使用不到“精通”的。“熟悉”意味着你对这门技术有着深入的使用且已经有较长的时间，通过查阅相关文档可以独立解决大部分问题，那么我们就能熟悉它了。</p><p>⚠️需要注意，学习的课程实训并不能算数！举几个例子，熟悉 MySQL 你应该能清楚的知道<strong>事务隔离级别</strong>、<strong>sql调优</strong>、处理过数据库事物并发带来的相关问题等许多细节；熟悉 Python 你应该知道<strong>GIL(全局解释锁s)</strong>、<strong>元编程</strong>、<strong>魔法方法(magic function)</strong>、<strong>迭代器生成器装饰器(iterator, generator, decorator)<strong>等等（如果你刚好也是Python技术栈，那我推荐你看一下《流畅的Python》这本书）；最后再以人数最多Java来举例子，Java你应该知道</strong>JVM内存区域布局</strong>、<strong>基本的垃圾回收机制和原理</strong>、各种集合类的底层原理、各种<code>InputStream/OutputStream</code>的区别，特别是HashMap中的<strong>桶结构的进化与退化</strong>以及接口，抽象类区别，应用场景。</p><p>还有一些计算机通用的底层原理（八股文）就不过多赘述了。</p><h3 id="项目与经历部分"><a href="#项目与经历部分" class="headerlink" title="项目与经历部分"></a>项目与经历部分</h3><p>项目与经历最好就是写你实习负责的项目与实习经历⚠️<strong>切忌将学校课程实训的项目也往上写</strong>⚠️因为这类项目根本无法吸引面试官眼球！甚至让面试官感觉你技术就那样儿～ 因此宁可只有一个也不写多，宁愿刚八股文也不要让面试官对你没兴趣！</p><p>项目这部分是简历中最重要的，因为它直接关系到面试官与你谈的内容。以我这几次面试来看面试官对你的项目问的越多，了解的越深入越感兴趣你就越有戏，我有几次甚至因为和面试官聊项目聊到timeout了😂面试官也对这个项目给出了好多有用有意思的建议！如果项目面试官不感兴趣的话，那就只能问你八股文了，这样除了要有扎实的基本功外还要看你能否答到面试官想听的点了，这难度就可想而知了～ 总的来说你的项目就是要让面试官感兴趣，因此项目部分提炼出来的要点如下：</p><ol><li>最好是实习的项目</li><li>项目比较有趣、新颖、吸引眼球</li><li>切忌把烂大街的项目放到简历上</li></ol><h3 id="技能证书与自我评价"><a href="#技能证书与自我评价" class="headerlink" title="技能证书与自我评价"></a>技能证书与自我评价</h3><p>自我评价想不到说啥，就来说说技能证书吧～ 技能证书其实也是按照精而不多原则来填写（这不是废话），当然真实的情况往往是没有几个能拿得出手的证书😄 这时候就有要按公司、按职位来写了。</p><p>以英语证书为例，如果你投的岗位与公司对英语水平没有太大要求而你又只有四级证书，那最好还是不要填写四级证书为好。因为在整个应届生群体中雅思托福的不在少数，你的简历上的四级反而是一个掉分项！</p><h2 id="面试八股文"><a href="#面试八股文" class="headerlink" title="面试八股文"></a>面试八股文</h2><ol><li>数据结构与算法</li><li>操作系统原理</li><li>计算机网络</li><li>数据库</li></ol><p>在复习上面的内容时，切记不要按学校教的来复习! 因为学校教的不够深入，前三部分建议参考考研408的真题。按考研的难度来复习就对了。八股文与考研这些东西，网上资料十分详细（特别是GitHub上面）这里就不展开细说了，列几个我认为秋招一定要会的基本功吧：（后面想起来会动态补充）</p><h3 id="数据结构与算法"><a href="#数据结构与算法" class="headerlink" title="数据结构与算法"></a>数据结构与算法</h3><p>这部分按考研标准来就好了，几种常见排序、数据结构是一定要会的～ 这里推荐《小灰算法》这本书，里面所讲的都是面试的基础。</p><h3 id="计算机网络"><a href="#计算机网络" class="headerlink" title="计算机网络"></a>计算机网络</h3><ol><li>OSI 七层模型分别是？各自的功能是什么？</li><li>一次完整的http请求过程包括那些内容</li><li>http本质上是什么？ get和post的区别有哪些？从底层原理上回答</li><li>http与https的区别？https如何保证数据传输安全</li><li>什么是SSL&#x2F;TLS？</li><li>一个tcp链接可以对应几个http request？</li><li>浏览器对同一个Host建立tcp链接的数量有没有限制？</li><li>DNS是什么？两种查询方法了解过吗？工作原理是什么？</li><li>http长链接与短链接的区别</li><li>get方法的长度限制是怎么回事？</li><li>Cookie与Session的区别？session的工作原理是什么？不同框架对session的实现有什么不同？</li><li>SQL注入了解吗？ 简单的注入会不会？</li><li>CSRF攻击、CSS攻击是什么？</li><li>TCP包头结构，各字段的功能与作用</li><li>TCP的粘包&#x2F;拆包问题，发生的原因？</li><li>常见的tcp攻击了解吗？会复现吗？</li><li>tcp的三次握手四次挥手</li><li>tcp四大拥塞控制算法，tcp的滑动窗口</li><li>流量控制原理会吗？tcp如何保证可靠性传输？</li><li>udp是什么？tcp与udp的区别</li><li>服务器出现大量close_wait的原因是什么？怎么解决？</li></ol><h3 id="数据库"><a href="#数据库" class="headerlink" title="数据库"></a>数据库</h3><p>这一part本人不熟，以 MySQL 与 Redis 为主：</p><ol><li>事务是什么？</li><li>MySQL事务隔离级别</li><li>并发问题？脏读、幻读与不可重复读？</li><li>MySQL对repeat table read做了什么？</li><li>索引原理</li><li>B+树是什么？并且对比红黑树与其他数据结构</li><li>死锁怎么造成的？怎么处理？</li><li>什么是MVCC？</li><li>MyISAM与Innodb</li><li>表结构优化、sql调优</li><li>跳表(SkipList)</li><li>Redis的两种存储是什么？</li><li>服务器宕机后数据怎么恢复</li></ol><h3 id="操作系统"><a href="#操作系统" class="headerlink" title="操作系统"></a>操作系统</h3><ol><li>进程、线程与协程的区别</li><li>线程进程到底是什么？</li><li>一个进程可以创建几个线程？</li><li>进程线程模型</li><li>Linux下进程间通信方式</li><li>Linux下同步机制</li><li>内存交换与覆盖的区别</li><li>虚拟技术了解吗？容器技术了解吗？</li><li>进程状态的切换知道多少？</li><li>一个程序从开始到结束整个生命周期的过程</li><li>进程同步的四种方法？</li><li>两个经典问题：哲学家与消费者</li><li>说说几种典型的锁？</li><li>死锁相关</li><li>malloc申请内存时候，操作系统做了什么？</li><li>说说new、malloc、del与free</li><li>说说守护进程与僵尸进程和孤儿进程</li><li>服务器高并发的解决方案有哪几个？</li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;秋招结束后就一直想写一篇总结，但个人比较懒躺平了好久:D。最近老师给我布置了个任务写一下秋招这段时间的面试经验总结, 就不继续鸽了吧😄 继回顾完大学生涯后再来看看近段时间的校招悲剧吧 :(&lt;/p&gt;
&lt;p&gt;如果不读研，校招可以说每个人一生只有一次，当你毕业了也就失去应届生这</summary>
      
    
    
    
    <category term="胡思乱语" scheme="https://blog.farmer233.top/categories/%E8%83%A1%E6%80%9D%E4%B9%B1%E8%AF%AD/"/>
    
    
    <category term="胡言乱语" scheme="https://blog.farmer233.top/tags/%E8%83%A1%E8%A8%80%E4%B9%B1%E8%AF%AD/"/>
    
  </entry>
  
  <entry>
    <title>Review of life in university ✔</title>
    <link href="https://blog.farmer233.top/2021/11/07/Review-of-life-in-university/"/>
    <id>https://blog.farmer233.top/2021/11/07/Review-of-life-in-university/</id>
    <published>2021-11-06T17:05:52.000Z</published>
    <updated>2026-04-22T08:30:03.571Z</updated>
    
    <content type="html"><![CDATA[<p>最近秋招尾声，对于职场小白的我来说有许许多多不懂、迷茫得地方。<strong>幸运的</strong>是我有着几个愿意听我埋怨，在我面试紧张时听我话痨，在我迷茫时给予我中肯建议的朋友，万分感谢！</p><p>过去的已无法改变，总结一下过去四年发生的点滴无论好坏，也叫作给自己一个交待吧。毕竟我们不也天天code-review么</p><p>不知不觉四年就过去了，最近经历了秋招见识到了找工作的艰辛，也有收获到了offer的喜悦，同时也有着选offer的纠结。找工作的艰辛源于自身实力不足，收获的offer是对我四年努力的肯定，选offer的纠结是出于对自身现状的不满。Anyway, dont stop learning.</p><h2 id="过去"><a href="#过去" class="headerlink" title="过去"></a>过去</h2><p>回顾过去，一个小白从网络入坑计算机到逐步转型web狗🐕再到现在慢慢靠近云原生、微服务相关领域。经历的点点滴滴，让人不禁感叹如果能remake该多好😶。</p><p>如果将大学四年放到整个计算机生涯中，那我会比作从婴儿蹒跚学步到一个学徒入门的水平。还记得刚入学的时候怀揣着一颗敬畏的心去面试部门，希望在这里能找到志同道合的人。现在看来这个决定是十分正确的。还记得大一下学期的时候，忐忑的去面试当时垄断校园公众号的小喵团队。二面时那“艰巨”的任务让许多人望而却步，庆幸的是我没有放弃。</p><ol><li>掌握python语法</li><li>学会使用git进行版本管理、提pr</li><li>学会flask框架，并开发一个简单的应用</li><li>一个星期内完成上述任务</li></ol><p>虽然这一个星期我活得很狼狈，但也正是这一个星期让我正式的踏上了开发这条路。大一如果说是跌跌撞撞的入门，那么大二可以说是渐入佳境、步入了快车道了。</p><p>大二一年可以说是完成了web前后端的一个蜕变。从与同学打闹的一个爬虫小程序到为了偷懒不跑<code>i广科</code>而写的一个定时爬虫小jio本，从<code>flask框架</code>与<code>Jinja2</code>模板的传统web开发到<code>flask</code>与<code>Vue</code>的前后端分离，以及不知何时学会的<code>JavaScript</code>。</p><p>大三是一个比较<em>卷</em>的一段时间，从<strong>手撸http协议</strong>到<strong>手撸玩具分布式</strong>再到前端Vue的一些底层api、原理的学习。也是从大三这个阶段，开始有了考研的想法，虽然这个想法动摇的十分厉害，就像大海上的小舟一样随波漂流。经过思考后我决定两个都要(小孩子才做选择)🤣不过此时的我还纯纯的不知道春招、秋招、提前批这些东西是啥，回过头来看真的挺呆的。</p><h2 id="现在"><a href="#现在" class="headerlink" title="现在"></a>现在</h2><p>到了大四了，无论是不是想找工作。在周围人的影响下我了解到了秋招、春招等信息，一开始还不是很上心，毕竟学校还有课程，考研408也还没看多少，八股文就更不用说了比408有过之而无不及。</p><p>大四是一个毕业的季节，同时也是考研的冲刺阶段。无论如何我都应该做出一个抉择了，要么考研要么秋招。对于一个末流三本的菜狗来说无论哪一条路都不好走，对于一个选择困难症晚期患者来说这是十分折磨人的。在一众吃瓜群友的吐苦水后，我选择了秋招这条路，现在看来这个决定还不错。秋招拿到了5份offer，选offer也不是件省心的事儿。因为无论是<strong>北上广深</strong>于我而言都是离家，别人也给不了太多的意见终归还是要自己做决定。从公司的待遇与职位的匹配度到所在城市的风俗习惯再到相关的入户政策等等诸多细枝末节的考虑，真让人头昏脑胀。</p><p>秋招也结束了，就等三方的流程走完就尘埃落定。虽然明年春招或许会再尝试，不过躺平又未尝不是一种选择？</p><h2 id="Remake"><a href="#Remake" class="headerlink" title="Remake"></a>Remake</h2><p>如果让我现在回到大一，我有什么是比较后悔的？我会怎么重新选择学习路线？我又会怎么弥补这些遗憾？</p><p>后悔事值得总结的大概就两件吧。其一是没有好好准备求职，比如没有参与春招实习、秋招导致错过了求职的黄金时间。另一件则是知乎上的热门话题了：<strong>为什么大学生都喜欢翘课</strong>，大一大二的公共课浪费了许许多多的精力与时间。如果给我现在回到大一，我会勇敢的翘了。如果你问我学校的课程有没有用？答案是不太有用。真实的情况往往是学校教学的内容早已掌握甚至比老师还会，这不是狂而是三本无奈的现状。教学的老师往往是开学前的一个星期临时自学然后就参与教学活动中来，上课讲错基础概念更是常有的事，这就是我为什么说<strong>上课浪费了我大量的精力与时间</strong>。第二个原因则是因为我已掌握的知识能反哺课程。以web开发为例，不外乎就是操作<code>http协议</code>与<code>数据库的CRUD</code>。熟悉了这个其实换框架只需几个小时，或者通过代码提示甚至可以直接上手完成实训、考试。往往裸考的成绩都能排到全班前三，真是讽刺啊~</p><h2 id="the-end"><a href="#the-end" class="headerlink" title="the end"></a>the end</h2><p>骂骂咧咧的写了篇流水账，过段时间再看回来或许会直接社死了吧。 Anyway, lifelong learning. 学习永远不晚，别停下学习的步伐就好啦。</p><h3 id="关于学校3-1-创新班"><a href="#关于学校3-1-创新班" class="headerlink" title="关于学校3+1(创新班)"></a>关于学校3+1(创新班)</h3><p>很多人都问过我3+1是否能去，废话不多说结论放在前头：</p><ol><li><strong>不要去3+1</strong></li><li><strong>不要去创新班!</strong></li></ol><p>当然事无绝对，觉得合适就好~</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近秋招尾声，对于职场小白的我来说有许许多多不懂、迷茫得地方。&lt;strong&gt;幸运的&lt;/strong&gt;是我有着几个愿意听我埋怨，在我面试紧张时听我话痨，在我迷茫时给予我中肯建议的朋友，万分感谢！&lt;/p&gt;
&lt;p&gt;过去的已无法改变，总结一下过去四年发生的点滴无论好坏，也叫作给自</summary>
      
    
    
    
    <category term="胡思乱语" scheme="https://blog.farmer233.top/categories/%E8%83%A1%E6%80%9D%E4%B9%B1%E8%AF%AD/"/>
    
    
    <category term="胡言乱语" scheme="https://blog.farmer233.top/tags/%E8%83%A1%E8%A8%80%E4%B9%B1%E8%AF%AD/"/>
    
  </entry>
  
  <entry>
    <title>A distributed system with Go</title>
    <link href="https://blog.farmer233.top/2021/09/05/A-distributed-with-Go/"/>
    <id>https://blog.farmer233.top/2021/09/05/A-distributed-with-Go/</id>
    <published>2021-09-04T16:21:43.000Z</published>
    <updated>2026-04-22T08:30:03.534Z</updated>
    
    <content type="html"><![CDATA[<p>从上学期开始，学校的教务系统更新了。新版的正方多了一个负载均衡的功能，但无奈实在做的太差了！为什么这么说尼？<br>因为在上学期抢课的时候入口网关依旧拉闸，而真正的后端服务器访问速度比德芙还丝滑。因此暂将锅甩给负载均衡服务器。既然学校的负载均衡拉闸，那么就有了 <strong>“帮学校做负载均衡”</strong> 的想法。<br>大体的思路就是，通过扫描内网网段发现隐藏的地址，然后负载均衡这些隐藏的地址。<br>既然是负载均衡，第一时间想到的就是<code>Nginx</code>了，通过资料搜集，大致确定了基本的流程，因此有了这么的这个流程图:</p><img src="/2021/09/05/A-distributed-with-Go/floatChart.dio.png" class="" title="floatChart"><span id="more"></span><h2 id="需要解决的问题"><a href="#需要解决的问题" class="headerlink" title="需要解决的问题"></a>需要解决的问题</h2><p>由于非官方的代理，因此我们无法知道教务系统部署在内网的哪些机器上，而校园网一般使用B类网，因此需要扫描大量的ip地址。<br>但这样无疑会触发学校网管的报警，正常情况下会导致ip短时间内被封。导致扫描质量低（由于被禁网，导致程序误认为扫描超时从而导致目标未被发现）。<br>基于这个问题，随之而生的想法就是分布式扫描，将扫描的工作打散到用户中，服务器只负责扫描几个主干网段。<br>一句话概括就是: <strong>将扫描工作分散到多台机器上</strong>，流程图如下：</p><img src="/2021/09/05/A-distributed-with-Go/microService.dio.png" class="" title="microService"><h2 id="Todo"><a href="#Todo" class="headerlink" title="Todo"></a>Todo</h2><blockquote><p>前情提示: 本文通信均采用HTTP、代码部分存在伪代码<br>代码仓库: (暂不开源)</p></blockquote><ol><li>注册中心 -&gt; <strong>服务注册</strong>与<strong>服务发现</strong></li><li>日志服务 -&gt; 记录日志</li><li>数据库服务 -&gt; 数据库操作(CRUD)</li><li>扫描器服务 -&gt; 发现教务系统地址</li><li>测试器服务 -&gt; 测试数据库中地址的<strong>健康度</strong></li><li>Api服务 -&gt; 向<code>consul</code>发送负载信息</li></ol><p>大致结构如下：</p><img src="/2021/09/05/A-distributed-with-Go/scanner.dio.png" class="" title="scanner"><h2 id="注册中心"><a href="#注册中心" class="headerlink" title="注册中心"></a>注册中心</h2><p>作为一个分布式系统，不可避免地需要<strong>服务注册</strong>与<strong>服务发现</strong>。因此需要一个<code>注册中心</code>来处理各个<code>服务</code>之间的依赖关系，在服务上线后通知依赖这个服务的服务(这里有点绕)<br><strong>举个例子</strong>:<br><code>数据库</code>的每个操作都需要记录日志，日志为了统一管理所以有一个<code>日志服务</code>专门处理日志信息。此时，<code>日志服务</code>因某些原因(可能是人为、也可能是网络掉线等)在<code>数据库服务</code>注册之后才注册，这是<code>注册中心</code>就需要通知<code>数据库</code>，让<code>数据库</code>的日志记录转成使用<code>日志服务</code>。</p><h3 id="功能与实现"><a href="#功能与实现" class="headerlink" title="功能与实现"></a>功能与实现</h3><p>作为一个注册中心，首先我们需要一个web服务来接收服务发送的信息(注册、依赖更新、注销等等)，但在这之前我们先来定义一下我们要用到的结构(在面向对象中为类)</p><ol><li><code>registry</code>来表示注册操作<ul><li><code>registrations</code>来存放注册的服务</li><li><code>add</code> 注册服务</li><li><code>notify</code> 事件通知</li><li><code>sendRequiredServices</code> 发送依赖的服务</li><li><code>sendPatch</code> 发送依赖项</li><li><code>remove</code> 移除(注销)服务</li><li><code>Heartbeat</code> 心跳包</li></ul></li><li><code>Registration</code>表示服务注册结构体<ul><li><code>ServiceName</code> 服务名</li><li><code>ServiceURL</code> 服务地址</li><li><code>RequiredServices</code>[数组]服务依赖项</li><li><code>ServiceUpdateURL</code> 服务与注册中心沟通的URL</li><li><code>HeartbeatURL</code> 心跳检测地址</li></ul></li></ol><figure class="highlight go"><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">type</span> registry <span class="keyword">struct</span> &#123;</span><br><span class="line">registrations []Registration</span><br><span class="line">mutex         *sync.RWMutex</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 服务注册结构体</span></span><br><span class="line"><span class="keyword">type</span> Registration <span class="keyword">struct</span> &#123;</span><br><span class="line">ServiceName ServiceName</span><br><span class="line">ServiceURL  <span class="type">string</span></span><br><span class="line">RequiredServices []ServiceName</span><br><span class="line">ServiceUpdateURL <span class="type">string</span></span><br><span class="line">HeartbeatURL     <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">type</span> ServiceName <span class="type">string</span></span><br><span class="line"><span class="comment">// 添加注册</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(r *registry)</span></span> add(reg Registration) <span class="type">error</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">func</span> <span class="params">(r registry)</span></span> notify(fullpatch patch) </span><br><span class="line"><span class="comment">// 注册中心向服务发送依赖相关内容</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(r registry)</span></span> sendRequiredServices(reg Registration) <span class="type">error</span></span><br><span class="line"><span class="comment">// 发送依赖项</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(r registry)</span></span> sendPatch(p patch, url <span class="type">string</span>) <span class="type">error</span></span><br><span class="line"><span class="comment">// 移除注册（注销）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(r *registry)</span></span> remove(url <span class="type">string</span>) <span class="type">error</span></span><br><span class="line"><span class="comment">// 心跳包</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(r *registry)</span></span> Heartbeat(freq time.Duration) </span><br></pre></td></tr></table></figure><p>既然注册中心是作为一个web服务实现的，那么肯定是需要一个<code>web server</code>的，由于项目属于玩票性质，也不算大因此使用Go内置的<code>net/http</code>来实现，代码如下：</p><figure class="highlight go"><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="keyword">type</span> RegistryService <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s RegistryService)</span></span> ServeHTTP(w http.ResponseWriter, r *http.Request) &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;Request received&quot;</span>)</span><br><span class="line"><span class="keyword">switch</span> r.Method &#123;</span><br><span class="line"><span class="keyword">case</span> http.MethodPost:</span><br><span class="line">decoder := json.NewDecoder(r.Body)</span><br><span class="line"><span class="keyword">var</span> r Registration</span><br><span class="line">err := decoder.Decode(&amp;r)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Println(err)</span><br><span class="line">w.WriteHeader(http.StatusBadRequest)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line">log.Printf(<span class="string">&quot;Adding service: %v with URL: %s\n&quot;</span>, r.ServiceName, r.ServiceURL)</span><br><span class="line">err = reg.add(r)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Println(err)</span><br><span class="line">w.WriteHeader(http.StatusBadRequest)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">case</span> http.MethodDelete:</span><br><span class="line">payload, err := ioutil.ReadAll(r.Body)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Println(err)</span><br><span class="line">w.WriteHeader(http.StatusInternalServerError)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line">url := <span class="type">string</span>(payload)</span><br><span class="line">log.Printf(<span class="string">&quot;Removing service at URL: %s&quot;</span>, url)</span><br><span class="line">err = reg.remove(url)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Println(err)</span><br><span class="line">w.WriteHeader(http.StatusInternalServerError)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line">w.WriteHeader(http.StatusMethodNotAllowed)</span><br><span class="line"><span class="keyword">return</span></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>对服务来说，就是根据注册中心定好的规则来注册服务，然后根据自身的依赖来处理对应的功能。<br>因为要处理相应的依赖，因此除了<code>Registration</code>外，再定义一个处理服务依赖的结构体及方法: <code>providers</code>.</p><figure class="highlight go"><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="keyword">type</span> providers <span class="keyword">struct</span> &#123;</span><br><span class="line">services <span class="keyword">map</span>[ServiceName][]<span class="type">string</span>   <span class="comment">// 服务名-&gt;服务url</span></span><br><span class="line">mutex    *sync.RWMutex</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 更新依赖</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p *providers)</span></span> Update(pat patch)</span><br><span class="line"><span class="comment">// // 通过服务名称，找到依赖的urls，从依赖项里面随机返回一个url</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p providers)</span></span> get(name ServiceName) (<span class="type">string</span>, <span class="type">error</span>)</span><br><span class="line"><span class="comment">// 导出给外部使用</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">GetProvide</span><span class="params">(name ServiceName)</span></span> (<span class="type">string</span>, <span class="type">error</span>)</span><br></pre></td></tr></table></figure><h3 id="组件的web服务"><a href="#组件的web服务" class="headerlink" title="组件的web服务"></a>组件的web服务</h3><p>因为每个服务都需要使用<strong>注册</strong>这些通用的功能，且这部分的工作都是重复的，因此将web抽出来公用.</p><figure class="highlight go"><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Start</span><span class="params">(ctx context.Context, host, port <span class="type">string</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">reg registry.Registration,</span></span></span><br><span class="line"><span class="params"><span class="function">RegisterHandlersFunc <span class="keyword">func</span>()</span></span>) (context.Context, <span class="type">error</span>) &#123;</span><br><span class="line"></span><br><span class="line">RegisterHandlersFunc()</span><br><span class="line">ctx = startService(ctx, reg.ServiceName, host, port)</span><br><span class="line">err := registry.RegisterService(reg)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> ctx, err</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> ctx, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">startService</span><span class="params">(ctx context.Context, ServiceName registry.ServiceName,</span></span></span><br><span class="line"><span class="params"><span class="function">host, port <span class="type">string</span>)</span></span> context.Context &#123;</span><br><span class="line">ctx, cancel := context.WithCancel(ctx)</span><br><span class="line"><span class="keyword">var</span> srv http.Server</span><br><span class="line">srv.Addr = <span class="string">&quot;:&quot;</span> + port</span><br><span class="line">address := fmt.Sprintf(<span class="string">&quot;http://%s:%s&quot;</span>, host, port)</span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">log.Println(srv.ListenAndServe())</span><br><span class="line"><span class="comment">// 关闭的时候要取消注册</span></span><br><span class="line"><span class="comment">// ... todo</span></span><br><span class="line">err := registry.ShutdownService(address)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Println(err)</span><br><span class="line">&#125;</span><br><span class="line">cancel()</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;Service is running in %v\n&quot;</span>, address)</span><br><span class="line">fmt.Println(<span class="string">&quot;Registry service started. Press any key to stop.&quot;</span>)</span><br><span class="line"><span class="keyword">var</span> s <span class="type">string</span></span><br><span class="line">fmt.Scanln(&amp;s)</span><br><span class="line">srv.Shutdown(ctx)</span><br><span class="line">err := registry.ShutdownService(address)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Println(err)</span><br><span class="line">&#125;</span><br><span class="line">cancel()</span><br><span class="line">&#125;()</span><br><span class="line"><span class="keyword">return</span> ctx</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>至此，可以开始专心的写业务了</p></blockquote><h3 id="扫描器"><a href="#扫描器" class="headerlink" title="扫描器"></a>扫描器</h3><p>得益于Go高并发的优势，开启数百万个的<code>goroutine</code>的开销也不会很大，非常的轻量！🛫<br>因此代码实现起来很轻松，大体思路和<strong>端口扫描器</strong>类似，在此基础上根据目标特征添加判断条件即可<br>端口扫描器代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">worker</span><span class="params">(ports <span class="keyword">chan</span> <span class="type">int</span>, results <span class="keyword">chan</span> <span class="type">int</span>)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> p := <span class="keyword">range</span> ports &#123;</span><br><span class="line">address := fmt.Sprintf(<span class="string">&quot;192.168.2.122:%d&quot;</span>, p)</span><br><span class="line">fmt.Printf(<span class="string">&quot;start %s\n&quot;</span>, address)</span><br><span class="line">conn, err := net.DialTimeout(<span class="string">&quot;tcp&quot;</span>, address, time.Second*<span class="number">5</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">results &lt;- <span class="number">0</span></span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line">conn.Close()</span><br><span class="line">results &lt;- p</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">ports := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">30000</span>)</span><br><span class="line">results := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">100</span>)</span><br><span class="line">scanEndPort := <span class="number">65535</span></span><br><span class="line"><span class="keyword">var</span> openPorts []<span class="type">int</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">cap</span>(ports); i++ &#123;</span><br><span class="line"><span class="keyword">go</span> worker(ports, results)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// push port to channel</span></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= scanEndPort; i++ &#123;</span><br><span class="line">ports &lt;- i</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">close</span>(ports)</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= scanEndPort; i++ &#123;</span><br><span class="line">res := &lt;-results</span><br><span class="line"><span class="keyword">if</span> res != <span class="number">0</span> &#123;</span><br><span class="line">openPorts = <span class="built_in">append</span>(openPorts, res)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">close</span>(results)</span><br><span class="line">sort.Ints(openPorts)</span><br><span class="line"><span class="keyword">for</span> _, port := <span class="keyword">range</span> openPorts &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;%d opend\n&quot;</span>, port)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="测试器"><a href="#测试器" class="headerlink" title="测试器"></a>测试器</h3><p>测试器主要功能是从数据库中取出教务系统地址，然后测试。与扫描器不同仅在于扫描器是写，测试器是读。因此这部分内容和扫描器实际上是在同个包内的，只是逻辑上将它分离了出来。   </p><p>这部分其实和<code>consul</code>的功能是重复的，因此代码不过多赘述</p><h3 id="数据库"><a href="#数据库" class="headerlink" title="数据库"></a>数据库</h3><p>由于校园网中，教务系统的地址不会太多，因此数据库的选择十分的随意(不存在性能方面的要求),所以这里使用自己熟悉的<code>redis</code>作为数据库。<br><strong>由于本系统是和官方的负载均衡并行的</strong> 因此存在某些结点用于两者共同访问导致压力上涨，响应不及时，因此利用<code>redis</code>的<code>sorted-set</code>在测试的时候，将响应快的地址设置高分数，使用<code>sorted-sets</code>的好处还有一个就是，集合的元素都是不重复的！，对于代理池来说，这个分数代表着教务系统地址稳定性的重要标准，因此设置分数的规则如下：</p><ul><li>基本分：10分，最高分：20</li><li>测试器设置一个更严格的超时时间来判断结点是否流畅</li><li>分数<strong>基本分</strong>为可用，检测器会定时循环检测每个代理可用情况，一旦检测到有可用的代理则立即置为<strong>最高分</strong></li><li>新获取的代理分数设置为<strong>基本分</strong>，如果测试可行(流畅)则置为满分，不可行(超时)则分数减一</li><li>分数减到0后代理移除</li></ul><p>由于是内网环境，分数与超时时间可以根据实际情况设置更严格</p><p>主要功能就是简单的CRUD啦，本文只讲逻辑与伪代码，实现部分就不多说了。</p><figure class="highlight go"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// set jwglxt to max score</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">max</span><span class="params">(addr discover.Addr)</span></span> (err <span class="type">error</span>) &#123;</span><br><span class="line">member, err := addr.MarshalBinary()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line">err = rdb.ZAdd(ctx, redisConfig.Key, &amp;redis.Z&#123;</span><br><span class="line">Score:  SCORE_MAX,</span><br><span class="line">Member: member,</span><br><span class="line">&#125;).Err()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// decrease proxy score</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">decrease</span><span class="params">(addr discover.Addr)</span></span> (err <span class="type">error</span>) &#123;</span><br><span class="line">member, err := addr.MarshalBinary()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line">err = rdb.ZIncrBy(ctx, redisConfig.Key, <span class="number">-1</span>, <span class="type">string</span>(member)).Err()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line">score := rdb.ZScore(ctx, redisConfig.Key, <span class="type">string</span>(member))</span><br><span class="line"><span class="keyword">if</span> score.Val() &lt;= <span class="number">0.00</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;%v current score %v, remove.\n&quot;</span>, addr, score.Val())</span><br><span class="line">err := rdb.ZRem(ctx, redisConfig.Key, member).Err()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// add new proxy to redis</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">add</span><span class="params">(addrs discover.Addrs)</span></span> (err <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">for</span> _, addr := <span class="keyword">range</span> addrs &#123;</span><br><span class="line">buf, err := addr.MarshalBinary()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line">err = rdb.ZAdd(ctx, redisConfig.Key, &amp;redis.Z&#123;</span><br><span class="line">Score:  SCORE_DEFAULT,</span><br><span class="line">Member: buf,</span><br><span class="line">&#125;).Err()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="consul-upsync-nginx实现动态负载均衡"><a href="#consul-upsync-nginx实现动态负载均衡" class="headerlink" title="consul-upsync-nginx实现动态负载均衡"></a>consul-upsync-nginx实现动态负载均衡</h2><p>该部分可以说是系统实现的关键了，因为<code>nginx</code>自带的负载均衡是写死的，不能根据后端情况动态调整，通过一番搜索对比，最终决定了<code>consul</code> + <code>upsync</code>方案。   </p><ol><li><code>upsync</code> 一个Nginx的模块(扩展)</li><li><code>consul</code> 一个分布式高可用的系统</li></ol><p>这部分仅限于“能用”阶段，笔者也不太懂，就不乱说啦！<br>主要就是<strong>Api服务</strong>定时的获取数据库内容(分数作为权重)，然后推送到<strong>consul</strong>中</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>该项目是学习Go时的一个练手项目，很多地方都不太好，因此仓库就不开源了🐕</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;从上学期开始，学校的教务系统更新了。新版的正方多了一个负载均衡的功能，但无奈实在做的太差了！为什么这么说尼？&lt;br&gt;因为在上学期抢课的时候入口网关依旧拉闸，而真正的后端服务器访问速度比德芙还丝滑。因此暂将锅甩给负载均衡服务器。既然学校的负载均衡拉闸，那么就有了 &lt;strong&gt;“帮学校做负载均衡”&lt;/strong&gt; 的想法。&lt;br&gt;大体的思路就是，通过扫描内网网段发现隐藏的地址，然后负载均衡这些隐藏的地址。&lt;br&gt;既然是负载均衡，第一时间想到的就是&lt;code&gt;Nginx&lt;/code&gt;了，通过资料搜集，大致确定了基本的流程，因此有了这么的这个流程图:&lt;/p&gt;
&lt;img src=&quot;/2021/09/05/A-distributed-with-Go/floatChart.dio.png&quot; class=&quot;&quot; title=&quot;floatChart&quot;&gt;</summary>
    
    
    
    <category term="搞事情" scheme="https://blog.farmer233.top/categories/%E6%90%9E%E4%BA%8B%E6%83%85/"/>
    
    
    <category term="Golang" scheme="https://blog.farmer233.top/tags/Golang/"/>
    
    <category term="爬虫" scheme="https://blog.farmer233.top/tags/%E7%88%AC%E8%99%AB/"/>
    
    <category term="正方教务系统" scheme="https://blog.farmer233.top/tags/%E6%AD%A3%E6%96%B9%E6%95%99%E5%8A%A1%E7%B3%BB%E7%BB%9F/"/>
    
    <category term="nginx" scheme="https://blog.farmer233.top/tags/nginx/"/>
    
  </entry>
  
</feed>
