<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
    <channel>
            <title>LeonardWang</title>
            <link>https://blog.leonard.wang</link>
        <generator>Halo 1.4.17</generator>
        <lastBuildDate>Mon, 10 Jun 2024 22:27:38 CST</lastBuildDate>
                <item>
                    <title>
                        <![CDATA[[译]Go 垃圾回收指南]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/gc-guide</link>
                    <description>
                            <![CDATA[<h2 id="简介">简介</h2><p>本指南旨在通过深入了解 Go 垃圾收集器，帮助高级 Go 用户更好地了解应用程序的成本。它还提供了Go用户如何利用这些知识来提高应用程序的资源利用率的指导。 它并不假设你了解垃圾回收，但假设你熟悉Go语言。</p><p>Go语言负责Go中 “值”的存储。在大多数情况下，Go语言开发人员根本不需要关心这些值存储在哪里，或者为什么要存储。 然而，在实践中，这些值通常需要存储在计算机<strong>物理内存</strong>中，而物理内存是有限的资源。 因为内存是有限的，所以必须小心地管理和回收内存，以避免在执行Go语言程序时耗尽内存。 根据需要分配和回收内存是 Go 负责实现的。</p><p>自动回收内存的另一个说法是<strong>垃圾回收</strong>(garbage collection) 。 从较高的层次上讲，垃圾回收器（简称GC）是通过识别不再需要使用的内存来对应用程序进行内存回收的系统。Go 标准工具链提供了一个随每个应用程序一起提供的运行时库，并且这个运行时库包含一个垃圾收集器。</p><p>请注意，Go语言规范并不能保证本指南所描述的垃圾回收器的存在，只不过Go语言值的底层存储由Go语言本身负责管理。 这一省略是有意的，它允许使用完全不同的内存管理技术。</p><p>因此，本指南是关于 Go 编程语言的特定实现，可能不适用于其他实现。 具体来说，本指南适用于标准工具链（gc (Go compiler)和工具）。 Gccgo和Gollvm都使用非常相似的GC实现，因此许多相同的概念都适用，但细节可能会有所不同。</p><p>此外，这是一个一直在修正的文档，随着时间的推移而变化，以最好地反映Go语言的最新版本。 本文档目前描述的是Go语言1.19中的垃圾回收器。</p><h3 id="go中的值存活在哪里">Go中的值存活在哪里</h3><p>在深入研究GC之前，让我们首先讨论一下不需要由GC管理的内存。</p><p>例如，存储在局部变量中的非指针值可能根本不会被Go语言的GC管理，Go语言会安排内存的分配，并将其绑定到创建它的<a href="https://go.dev/ref/spec#Declarations_and_scope">词法作用域</a>中。 一般来说，这比依赖GC更有效率，因为Go语言编译器能够预先确定何时释放内存，并发出清理内存的机器指令。 通常，我们把这种为Go语言的值分配内存的方式称为“<strong>栈分配</strong>”，因为空间存储在goroutine栈中。</p><p>由于 Go 编译器无法确定其生命周期，导致无法以这种方式分配内存的 Go 值被称为“<strong>逃逸到堆</strong>”。 “堆”可以被认为是内存分配的一个大杂烩，Go语言的值需要被放置在堆的某个地方。 在堆上分配内存的操作通常称为“动态内存分配”，因为编译器和运行时都可以对如何使用该内存以及何时可以清理它做出很少的假设。这就是GC的用武之地：它是一个专门标识和清理动态内存分配的系统。</p><p>Go语言的值需要逃逸到堆中的原因有很多。 一个原因可能是其大小是动态确定的。 例如，考虑一个slice对应的底层数组，它的初始大小由一个变量而不是一个常量确定。 请注意，逃逸到堆也必须是可传递的：如果一个Go值的引用被写入到另一个已经被确定为逃逸的Go值中，那么这个值也必须逃逸。</p><p>Go语言的值是否逃逸取决于使用它的上下文和Go语言编译器的逃逸分析算法。 当值逃逸时，试图准确地列举它将是脆弱和困难的：算法本身相当复杂，并且在不同的Go语言版本中会有所变化。 有关如何识别哪些值逃逸而哪些值不逃逸的详细信息，请参阅<a href="https://tip.golang.org/doc/gc-guide#Eliminating_heap_allocations">消除堆分配</a>一节。</p><h3 id="跟踪垃圾回收">跟踪垃圾回收</h3><p>垃圾回收可能指自动回收内存的众多实现方法，例如引用计数。 在本文档的上下文中，垃圾回收指的是跟踪垃圾回收，其通过跟踪指针传递来识别正在使用的、所谓的活动对象。</p><p>让我们更严格地定义这些术语:</p><ul><li><strong>对象</strong> - 对象是一个动态分配的内存块，包含一个或多个Go值。</li><li><strong>指针</strong> - 指向对象内任何值的内存地址。 这自然包括 <code>*T</code> 形式的Go语言值，但也包括部分内建的Go语言值： strings, slices, channels, maps和 intercace 值都包含GC必须跟踪的内存地址。</li></ul><p>对象和指向其他对象的指针一起形成<strong>对象图</strong>。 为了识别活动内存，GC从程序的<strong>根</strong>开始遍历对象图，这些指针标识了程序明确在使用的对象。 根的两个例子是局部变量和全局变量。 遍历对象图的过程被称为<strong>扫描</strong>。</p><p>此基本算法对所有跟踪GC通用。 跟踪GC的不同之处在于，它们对于发现的存活内存会做什么。 Go语言的GC使用了<strong>标记(mark)</strong>—<strong>清扫(sweep)<strong>技术，这意味着为了跟踪它的过程，GC也会将它遇到的值</strong>标记</strong>为存活。 跟踪完成后，GC将遍历堆中的所有内存，并将所有未标记的对象的内存设置为可用于分配的内存。 此过程称为<strong>扫描(scanning)</strong>。</p><p>您可能熟悉的另一种技术是将存活对象移动到另一部分新内存中，并留下一个转发指针，以后将使用该指针更新应用程序的所有指针。 我们称以这种方式移动对象的GC为<strong>移动GC</strong>; Go的GC不是这样子的，它是<strong>非移动GC</strong>。</p><h2 id="gc的周期">GC的周期</h2><p>由于Go GC是一个标记 - 清扫GC，因此它大致分为两个阶段：<strong>标记阶段</strong>和<strong>清扫阶段</strong>。 虽然这句话似乎是重复的，但它包含了一个重要的见解：在跟踪完所有内存之前，不可能释放内存以供分配，因为可能仍有未扫描的指针使对象保持活动状态。 因此，清扫动作必须与标记动作完全分开。 此外，当没有与GC相关的工作要做时，GC也可能根本不活动。 GC在清扫、关闭、标记这三种状态之间不断循环，这就是所谓的GC周期。在本文档中，将清扫作为GC周期的开始，然后是关闭，标记。</p><p>接下来的几个章节我们将集中讨论如何直观地了解GC的成本，以帮助用户调整GC参数，从而提升程序的性能。</p><h2 id="了解成本">了解成本</h2><p>GC本质上是一个构建在更复杂系统上的复杂软件。 当试图理解GC并调整其行为时，很容易陷入细节的泥潭。 本节旨在提供一个框架，用于说明Go GC的开销和调优参数。</p><p>开始讨论前，先了解基于三个简单公理的GC成本模型。</p><ol><li><p>GC只涉及两种资源：CPU时间和物理内存。</p></li><li><p>GC的内存开销包括存活的堆内存、标记阶段之前分配的新堆内存，以及元数据空间（即使与前两个的开销成比例，但相比之下元数据空间开销也很小）。</p><blockquote><p>注意：存活的堆内存是由上一个GC周期确定为存活的内存，而新堆内存是在当前周期中分配的任何内存，在标记结束时可能是存活的，也可能不是存活的。</p></blockquote></li><li><p>GC的CPU成本被建模分为每个周期的固定成本，以及与存活堆的大小成比例的边际成本(marginal cost)。</p><blockquote><p>注意：从渐进的角度来说，清扫比标记和扫描要难衡量，因为它必须执行与整个堆的大小成比例的工作，包括被确定为非存活（即“死”）的内存。 然而，在当前的实现中，清扫操作比标记和扫描快得多，因此在本讨论中可以忽略其相关成本。</p></blockquote></li></ol><p>这种模型简单而有效：它准确地对GC的主要成本进行了分类。 然而，这个模型没有说明这些成本的规模，也没有说明它们是如何相互作用的。 为了对此建模，考虑以下情况，我们称这种场景为<strong>稳态</strong>(steady-stat)。</p><ul><li><p>应用程序分配新内存的速率（以字节/秒为单位）是恒定的。</p><blockquote><p>注意：重要的是要理解这个分配率与这个新内存是否是存活是完全无关的。 没有一个是活的，所有的都是活的，或者一部分是活的都有可能。 (除此之外，一些旧的堆内存也可能死亡，因此，如果该内存是存活的，存活的堆大小不一定会增长。）<br />更具体地说，假设有一个web服务为它处理的每个请求分配2 MiB的总堆内存。 在请求过程中，2 MiB中最多有512 KiB在请求进行期间保持存活状态，当服务完成对请求的处理时，所有这些内存都会死亡。 稳定的请求流（比如每秒100个请求）会产生200 MiB/s的分配率和50 MiB的峰值存活堆。</p></blockquote></li><li><p>应用程序的对象图每次看起来都大致相同（对象的大小相似，指针的数量大致恒定，图的最大深度大致恒定）。<br />另一种思考方式是GC的边际成本是恒定的。</p></li></ul><blockquote><p>注意：稳态可能看起来是人为的，但它的确代表了应用程序在某个恒定工作负载下的行为。 当然，在应用程序运行时，工作负载也可能发生变化，但通常应用程序行为看起来总体上像是一串稳定状态，中间穿插着一些瞬态行为。</p></blockquote><blockquote><p>注意：稳定状态对存活堆没有任何假设。 它可能会随着每个后续GC周期而增长，可能会缩小，也可能会保持不变。 然而，试图在下面的解释中包含所有这些情况很无聊乏味，而且不是很有说明性，所以本指南将重点放在存活堆保持不变的示例上。 GOGC一节会更详细地探讨了非常量存活堆的场景。</p></blockquote><p>在存活堆大小不变的稳定状态下，只要GC在经过相同的时间后执行，每个GC周期在成本模型中看起来都是相同的。 这是因为在固定的时间内，如果应用程序的分配速率是固定的，则将分配固定数量的新堆内存。 因此，在存活堆大小和新堆内存保持不变的情况下，内存使用量将始终保持不变。 而且因为存活堆的大小相同，所以GC CPU的边际成本也相同，并且固定成本将以某个固定间隔发生。</p><p>现在考虑如果延迟 GC 的触发点， 那么将分配更多的内存，但每个GC周期仍将导致相同的CPU开销。 但是，在其他固定的时间窗口中，完成的GC周期会更少，从而降低了总体CPU成本。 如果GC提前启动，则情况正好相反：将分配较少的内存并且将更频繁地引起CPU成本。</p><p>这种情况代表了GC可以在CPU时间和内存之间进行的基本权衡，由GC实际执行的频率来控制。 换句话说，权衡完全由GC的频率定义。</p><p>还有一个细节需要定义，那就是GC应该决定何时开始。 注意，这直接设置了任何特定稳态下的GC频率，从而定义了权衡。 在Go语言中，决定GC何时启动是用户可以控制的主要参数。</p><h2 id="gogc">GOGC</h2><p>在高层次上说，GOGC 决定了 GC CPU 和内存之间的权衡。</p><p>它通过在每个 GC 周期后确定目标堆大小来工作，这是下一个周期中总堆大小的目标值。 GC 的目标是在总堆大小超过目标堆大小之前完成一个收集周期。 总堆大小定义为上一个周期结束时的活动堆大小，加上自上一个周期以来应用程序分配的任何新堆内存。 同时，目标堆内存定义为：</p><pre><code>Target heap memory = Live heap + (Live heap + GC roots) * GOGC / 100</code></pre><p>举个例子，假设一个Go语言程序，它的存活堆大小为8 MiB，goroutine栈为1 MiB，全局变量中的指针为 1 MiB。 如果GOGC值为100，则在下一次GC运行之前将分配的新内存量将为10 MiB（即 Live heap + GC roots = 10MiB工作量的100%），总堆占用量为18 MiB。 如果GOGC值为50，则它将为50%，即分配的新内存量为5 MiB。 如果GOGC值为200，则为200%，即分配的新内存量20 MiB。</p><blockquote><p>注意：通过GOGC目标堆，仅在Go 1.18以后包含GC roots。 以前，它只会计算存活堆。 通常，goroutine 堆栈中的内存量非常小，并且存活堆大小支配着所有其他 GC 工作源，但是在程序有数十万个 goroutine 的情况下，GC会做出错误的判断。</p></blockquote><p>通过堆目标控制 GC 频率：目标越大，GC 等待开始另一个标记阶段的时间越长，反之亦然。 虽然精确的公式对于进行估计很有用，但最好根据其基本目的来考虑 GOGC：一个在 GC CPU 和内存权衡中选择一个合适的参数。 关键的一点是，将 GOGC 翻倍将使堆内存开销翻倍，并将 GC CPU 成本大致减半，反之亦然。 （要查看有关原因的完整解释，请参阅<a href="https://tip.golang.org/doc/gc-guide#Additional_notes_on_GOGC">附录</a>。）</p><blockquote><p>注意：目标堆大小只是一个目标，GC 周期可能无法在该目标处完成的原因有很多。 一方面，足够大的堆分配可以简单地超过目标。 同时，GC 实现中出现的其他原因超出了本指南迄今为止使用的 <a href="https://tip.golang.org/doc/gc-guide#Understanding_costs">GC 模型</a>。 有关更多详细信息，请参阅<a href="https://tip.golang.org/doc/gc-guide#Latency">时延部分</a>，也可以在<a href="https://tip.golang.org/doc/gc-guide#Additional_resources">其他资源</a>中找到完整的详细信息。</p></blockquote><p>GOGC可以通过GOGC环境变量（所有Go语言程序都能识别）或者<code>runtime/debug</code>包中的<code>SetGCPercent</code> API来配置。</p><blockquote><p>注意：GOGC也可用于通过设置<code>GOGC=off</code>或调用<code>SetGCPercent(-1)</code>来完全关闭GC（前提是memory limit没有使用）。 从概念上讲，此设置等效于将GOGC设置为无穷大值，因为在触发GC之前新内存的数量是无限的。</p></blockquote><p>为了更好地理解我们到目前为止讨论的所有内容，请尝试下面的交互式可视化，它是基于前面讨论的GC成本模型构建的。 该可视化描述了某个程序的执行，该程序的非GC工作需要10秒的CPU时间才能完成。 在进入稳定状态之前的第一秒，它执行一些初始化步骤（增长其存活堆）。 应用程序总共分配200 MiB，每次存活的内存为20MiB。 它假设要完成的唯一相关GC工作来自存活堆，并且（不现实地）应用程序不使用额外的内存。</p><p>使用滑块调整GOGC的值，以查看应用程序在总持续时间和GC开销方面的响应情况。 每次GC循环都会在新堆降为零时发生。 X轴移动以始终显示程序的完整CPU持续时间。 请注意，GC使用的额外CPU时间会增加总持续时间。</p><blockquote><p>注 ：请移步<a href="https://tip.golang.org/doc/gc-guide">原文</a>进行体验</p></blockquote><p><a href="https://colobu.com/2022/07/16/A-Guide-to-the-Go-Garbage-Collector/1.png"><img src="https://imghost.leonard.wang//uPic/image-2022100621410195z2tU.png" alt="img" /></a></p><p>请注意，GC总是会导致一些CPU和峰值内存开销。 随着GOGC的增加，这些CPU开销降低，但峰值内存与活动堆大小成比例增加。 随着GOGC的减小，峰值内存需求也会减少，但会增加额外的CPU开销。</p><blockquote><p>注意：图形显示的是CPU时间，而不是完成程序所需的挂钟时间(wall-clock time)。 如果程序在1个CPU上运行并充分利用其资源，则它们是等效的。 真实的的程序可能运行在多核系统上，并且不会始终100%地利用CPU。 在这些情况下，GC的挂钟时间影响会比较低。</p></blockquote><blockquote><p>注意：Go GC的最小总堆大小为4 MiB，因此如果GOGC设置的目标值低于该值，则会取整。 这个图形展示反映此细节。</p></blockquote><p>这里有一个动态的和更有真实感的例子。 同样，在没有GC的情况下，应用程序需要10个CPU秒才能完成，但在中途，稳态分配率急剧增加，并且活动堆大小在第一阶段发生了一些变化。 这个示例演示了当活动堆大小实际上发生变化时，稳定状态可能是什么样子的，以及更高的分配率如何导致更频繁的GC周期。</p><p><a href="https://colobu.com/2022/07/16/A-Guide-to-the-Go-Garbage-Collector/2.png"><img src="https://imghost.leonard.wang//uPic/image-20221006214103dXh6YC.png" alt="img" /></a></p><h2 id="内存限制-memory-limit">内存限制 （Memory limit）</h2><p>在Go 1.19之前，GOGC是唯一一个可以用来修改GC行为的参数。 虽然它作为一种设置权衡(trade-off)的方式非常有效，但它没有考虑到可用内存是有限的。 考虑当活动堆大小出现短暂峰值时会发生什么情况：因为GC将选择与活动堆大小成比例的总堆大小，所以GOGC必须被配置为峰值活动堆大小相匹配的值，即使在通常情况下，较高的GOGC值会提供了更好的权衡效果。</p><p>下面的可视化演示了这种瞬态堆峰值情况。</p><p><a href="https://colobu.com/2022/07/16/A-Guide-to-the-Go-Garbage-Collector/3.png"><img src="https://imghost.leonard.wang//uPic/image-202210062141061oAiQN.png" alt="img" /></a></p><p>如果示例工作负载在可用内存略高于60 MiB的容器中运行，则GOGC不能增加到100以上，即使其余GC周期有可用内存来使用该额外内存。 此外，在一些应用中，这些瞬时峰值可能是罕见的并且难以预测，从而导致偶然的、不可避免的并且可能代价高昂的内存不足情况。</p><p>这就是为什么在1.19版本中，Go语言增加了对设置运行时内存限制的支持。 内存限制可以通过所有Go语言程序都能识别的<strong>GOMEMLIMIT</strong>环境变量来配置，也可以通过<code>runtime/debug</code>包中的<code>SetMemoryLimit</code>函数来配置。</p><p>这个内存限制设置了Go语言运行时可以使用的最大内存总量。 包含的特定内存集是<code>runtime.MemStats</code>的<code>Sys - HeapReleased</code>的值，或者等价于<code>runtime/metrics</code>的公式<code>/memory/classes/total:bytes - /memory/classes/heap/released:bytes</code>。</p><p>因为Go GC可以显式控制它使用多少堆内存，所以它会根据这个内存限制和Go运行时使用的其他内存来设置总的堆大小。</p><p>下面的可视化描述了来自GOGC部分的相同的单阶段稳态工作负载，但这次Go运行时额外增加了10 MiB的开销，并且内存限制可调。 尝试在GOGC和内存限制之间移动，看看会发生什么。</p><p><a href="https://colobu.com/2022/07/16/A-Guide-to-the-Go-Garbage-Collector/4.png"><img src="https://imghost.leonard.wang//uPic/image-202210062141082qA6Mn.png" alt="img" /></a></p><p>请注意，当内存限制降低到GOGC确定的峰值内存（GOGC为100时为42 MiB）以下时，GC会更频繁地运行，以将峰值内存保持在限制的内存之下。</p><p>回到我们前面的瞬态堆峰值的例子，通过设置内存限制并打开GOGC，我们可以获得两全其美的结果：不违反内存限制，且更好地节约资源。 请尝试以下交互式可视化。</p><p><a href="https://colobu.com/2022/07/16/A-Guide-to-the-Go-Garbage-Collector/5.png"><img src="https://imghost.leonard.wang//uPic/image-202210062141108ZVEPr.png" alt="img" /></a></p><p>请注意，对于GOGC的某些值和内存限制，峰值内存使用在内存限制为多少时停止，但程序执行的其余部分仍然遵守GOGC设置的总堆大小规则。</p><p>这一观察引出了另一个有趣的细节：即使GOGC设置为关闭，内存限制仍然有效! 实际上，这种特定的配置代表了资源经济的最大化，因为它设置了维持某个内存限制所需的最小GC频率。 在这种情况下，所有程序的执行都会使堆大小增加以满足内存限制。</p><p>现在，虽然内存限制显然是一个强大的工具，<strong>但使用内存限制并不是没有代价的</strong>，当然也不会使GOGC的实用性失效。</p><p>请考虑当活动堆增长到足以使总内存使用量接近内存限制时会发生什么。 在上面的稳定状态可视化中，尝试关闭GOGC，然后慢慢地进一步降低内存限制，看看会发生什么。 请注意，应用程序花费的总时间将开始以无限制的方式增长，因为GC不断地执行以维持不可能的内存限制。</p><p>这种情况，即程序不断进行GC，存活内存较多，无法进行预期的内存清理，称为系统颠簸(thrashing)。 这是特别危险的，因为它严重地拖延了程序，可能会导致用户程序无法执行。 更糟糕的是，它可能会发生在我们试图避免使用GOGC的情况下：一个足够大临时堆尖峰会导致程序无限期地停止! 尝试在瞬态堆峰值可视化中降低内存限制（大约30 MiB或更低），并注意最坏的行为是如何从堆峰值开始的。</p><p>在许多情况下，无限期暂停比内存不足情况更糟，因为后者往往会导致更快的失败以便我们发现和处理。</p><p>因此，内存限制被定义为软限制。 Go语言运行时并不保证在任何情况下都能保持这个内存限制;它只承诺了一些合理的努力。 内存限制的放宽对于避免系统颠簸行为至关重要，因为它为GC提供了一条出路：让内存使用超过限制以避免在GC中花费太多时间。</p><p>这在内部是如何工作的？GC mitigates 设置了一个在某个时间窗口内可以使用的CPU时间量的上限（对于CPU使用中非常短的瞬时峰值，有一些滞后）。 此限制当前设置为大约50%，具有<code>2 * GOMAXPROCS CPU-second</code>窗口。 限制GC CPU时间的结果是GC的工作被延迟，同时Go程序可能会继续分配新的堆内存，甚至超过内存限制。</p><p>50% GC CPU限制背后的直觉是基于对具有充足可用内存的程序的最坏情况影响。 在内存限制配置错误的情况下，它被错误地设置得太低，程序最多会慢2倍，因为GC占用的CPU时间不能超过50%。</p><p>注意：此页上的可视化不会模拟GC CPU限制。</p><h3 id="建议用法">建议用法</h3><p>虽然内存限制是一个强大的工具，Go语言运行时也会采取措施来减少误用造成的最坏行为，但谨慎使用它仍然很重要。 下面是一些关于内存限制在哪些地方最有用，以及在哪些地方可能弊大于利的建议。</p><ul><li>当Go语言程序的执行环境完全在你的控制之下，并且Go语言程序是唯一可以访问某些资源的程序时（也就是说，某种内存预留，就像容器内存限制一样），一定要利用内存限制。<br />一个很好的示例是将web服务部署到具有固定可用内存量的容器中。<br /><strong>在这种情况下，一个很好的经验法则是，留出额外的5-10%的空间来处理Go语言运行时不知道的内存资源。</strong></li><li>请随时调整内存限制，以适应不断变化的条件。<br />一个很好的例子是cgo程序，其中C库暂时需要使用更多的内存。</li><li>如果Go语言程序可能会与其他程序共享有限的内存，那么不要将GOGC设置为off，因为这些程序通常与Go语言程序是解耦的。 相反，保留内存限制，因为它可能有助于抑制不需要的瞬态行为，但将GOGC设置为某个较小的、对于一般情况而言合理的值。</li></ul><p>虽然尝试为共享程序“保留”内存是很诱人的，但除非程序完全同步（例如，Go程序在被调用程序执行时调用某些子进程和阻塞），否则结果将不太可靠，因为两个程序都不可避免地需要更多内存。 让Go程序在不需要内存的时候使用更少的内存，总体上会产生更可靠的结果。 此建议也适用于过量使用的情况，在这种情况下，在一台计算机上运行的容器的内存限制之和可能会超过该计算机可用的实际物理内存。</p><ul><li>当部署到您无法控制的执行环境时，不要使用内存限制，特别是当程序的内存使用与其输入成比例时。<br />CLI工具或桌面应用程序就是一个很好的例子。 在不清楚可能输入什么类型的输入，或者系统上可能有多少可用内存时，将内存限制写入程序可能会导致混乱的崩溃和性能下降。 此外，高级最终用户可以根据需要设置内存限制。</li><li>当程序已经接近其环境的内存限制时，不要设置内存限制以避免内存不足的情况。</li></ul><p>这有效地将内存不足的风险替换为严重的应用程序速度减慢的风险，这通常不是一个有利的交易，即使Go语言努力减轻系统颠簸。 在这种情况下，提高环境的内存限制（然后可能设置内存限制）或降低GOGC（这提供了比系统颠簸缓解更干净的权衡）将更加有效。</p><h3 id="延迟时间">延迟时间</h3><p>到目前为止，本文将应用程序建模在在GC执行时会暂停这一公理上。 确实存在这样的GC实现，它们被称为<strong>stop-the-world</strong> GC。</p><p>然而，Go GC并不是完全停止世界，实际上它的大部分工作都是与应用程序同时进行的。 这样做的主要原因是它减少了应用程序延迟。 具体来说，延迟是指单个计算单元（例如，web请求）的端到端持续时间。</p><p>到目前为止，本文主要考虑应用程序吞吐量，或这些操作的聚合（例如，每秒处理的web请求）。 请注意，GC周期部分中的每个示例都侧重于执行程序的总CPU持续时间。 然而，这样的持续时间对于例如web服务来说意义要小得多。 虽然吞吐量（即每秒的查询数）对于web服务仍然很重要，但通常每个单独请求的延迟甚至更重要。</p><p>就延迟而言，stop-the-world GC可能需要相当长的时间来执行其标记和扫描阶段，在此期间，应用程序以及在web服务的上下文中的任何正在进行的请求都无法取得进一步的进展。 相反，Go GC确保了任何全局应用程序暂停的时间都不会以任何形式与堆的大小成比例，并且在应用程序主动执行的同时执行核心跟踪算法（暂停在算法上与 GOMAXPROCS 成比例更大，但最常见的是停止运行 goroutine 所需的时间）。 这种选择并非没有成本，因为在实践中，它往往会导致吞吐量较低的设计，但需要注意的是，较低的延迟并不一定意味着较低的吞吐量，并且随着时间的推移，Go 垃圾收集器的性能在延迟和吞吐量方面都在稳步提高。</p><p>到目前为止，Go 的当前 GC 的并发性质并未使本文档中讨论的任何内容无效：没有任何陈述依赖于这种设计选择。 GC 频率仍然是 GC 在 CPU 时间和内存之间权衡吞吐量的主要方式，事实上，它也扮演着延迟的角色。 这是因为 GC 的大部分成本都是在标记阶段处于活动状态时产生的。</p><p>那么关键的一点是，降低 GC 频率也可能会改善延迟。 这不仅适用于通过修改调整参数来降低 GC 频率，例如增加 GOGC 和/或内存限制，还适用于<a href="https://tip.golang.org/doc/gc-guide#Optimization_guide">优化指南</a>中描述的优化。</p><p>然而，理解延迟通常比理解吞吐量更复杂，因为它是程序即时执行的产物，而不仅仅是成本的聚合之物。 因此，延迟和GC频率之间的联系更加脆弱，可能不那么直接。 下面是一个可能导致延迟的来源列表，供那些倾向于深入研究的人使用。</p><ul><li>当 GC 在标记和扫描阶段之间转换时，短暂的 stop-the-world 暂停</li><li>调度延迟是因为GC在标记阶段占用了25%的CPU资源</li><li>用户goroutine在高内存分配速率下的辅助标记</li><li>当GC处于标记阶段时，指针写入需要额外的处理（write barrier）</li><li>运行中的goroutine必须被暂停，以便扫描它们的根。</li></ul><p>这些延迟源在<a href="https://tip.golang.org/doc/diagnostics#execution-tracer">trace</a>中可见，除了需要额外工作的指针写入。</p><h3 id="其他资源">其他资源</h3><p>虽然上面提供的信息是准确的，但它缺乏充分理解Go GC设计中的成本和权衡的细节。 有关详细信息，请参阅以下其他资源。</p><ul><li>[The GC Handbook](<a href="https://tip.golang.org/doc/gc-guide#:~:text=following">https://tip.golang.org/doc/gc-guide#:~:text=following</a> additional resources.-,The GC Handbook,-—An excellent general) — 一个垃圾收集器设计的优秀通用资源和参考资料。</li><li><a href="https://google.github.io/tcmalloc/design.html">TCMalloc</a> — C/C++内存分配器TCMalloc的设计文档，Go内存分配器就是基于此。</li><li><a href="https://go.dev/blog/go15gc">Go 1.5 GC announcement</a> — 官方介绍Go 1.5并发GC的博客文章，其中更详细地描述了算法。</li><li><a href="https://go.dev/blog/ismmkeynote">Getting to Go</a> — 深入介绍Go GC设计到2018年的演变</li><li><a href="https://docs.google.com/document/d/1wmjrocXIWTr1JxU-3EQBI6BK6KgtiFArkG47XK73xIQ/edit">Go 1.5 concurrent GC pacing</a> — 确定何时开始并发标记阶段的设计文档</li><li><a href="https://github.com/golang/go/issues/30333">Smarter scavenging</a> — 订正Go运行时向操作系统返回内存的方式的设计文档</li><li><a href="https://github.com/golang/go/issues/35112">Scalable page allocator</a> — 订正Go运行时管理其从操作系统获得的内存的方式的设计文档</li><li><a href="https://github.com/golang/go/issues/44167">GC pacer redesign (Go 1.18)</a> — 用于修改算法以确定何时开始并发标记阶段的设计文件</li><li><a href="https://github.com/golang/go/issues/48409">Soft memory limit (Go 1.19)</a> — 软内存限制的设计文件</li></ul><h2 id="关于虚拟内存注意事项">关于虚拟内存注意事项</h2><p>本指南主要关注GC的物理内存使用，但经常出现的一个问题是这到底意味着什么，以及它与虚拟内存的比较（通常在像top这样的程序中表示为“VSS”）。</p><p>物理内存是大多数计算机中实际物理RAM芯片中的内存。 虚拟内存是由操作系统提供的物理内存上的抽象，用于将程序彼此隔离。 程序保留完全不映射到任何物理地址的虚拟地址空间通常也是可以接受的。</p><p><strong>由于虚拟内存只是操作系统维护的映射，因此保留不映射到物理内存的大型虚拟内存的成本通常非常小。</strong></p><p>Go语言运行时通常在以下几个方面依赖于这种虚拟内存开销视图：</p><ul><li>Go语言运行时不会删除它所映射的虚拟内存。 相反，它使用大多数操作系统提供的特殊操作来显式释放与某个虚拟内存范围相关联的任何物理内存资源。<br />该技术被显式地用于管理内存限制，并将Go语言运行时不再需要的内存返回给操作系统。 Go运行时也会在后台连续释放不再需要的内存。 有关详细信息，请参阅其他资源。</li><li>在32位平台上，Go运行时会为堆预留128 MiB到512 MiB的地址空间，以限制碎片问题。</li><li>Go语言运行时在实现几个内部数据结构时使用了大量的虚拟内存地址空间预留。 在64位平台上，它们通常具有大约700 MiB的最小虚拟内存占用量。 在32位平台上，它们的占用空间可以忽略不计。</li></ul><p>因此，虚拟内存指标，比如top中的“VSS”，在理解Go语言程序的内存占用方面通常不是很有用。 相反，应该关注“RSS”和类似的度量，它们更直接地反映了物理内存的使用情况。</p><h2 id="优化指南">优化指南</h2><h3 id="确定成本">确定成本</h3><p>在尝试优化Go语言应用程序与GC的交互方式之前，首先确定GC是一个主要的开销，这一点很重要。</p><p>Go生态系统提供了大量的工具来识别成本和优化Go应用程序。 有关这些工具的简要概述，请参阅<a href="https://tip.golang.org/doc/diagnostics">诊断指南</a>。 在这里，我们将重点讨论这些工具的一个子集，以及应用它们的合理顺序，以便理解GC的影响和行为。</p><p><strong>1、CPU profile</strong></p><p>优化程序的一个很好的起点是<a href="https://pkg.go.dev/runtime/pprof#hdr-Profiling_a_Go_program">CPU profiling</a>。 CPU profiling提供了CPU时间花费在何处的概述，尽管对于没有经验的人来说，可能很难确定 GC 在特定应用中的开销。 幸运的是，理解profile的GC主要归结为了解<code>runtime</code>包中不同函数的含义即可。 以下是这些函数中用于解释CPU profile文件的有用子集。</p><blockquote><p>注意：下面列出的函数不是叶子函数，因此它们可能不会显示在pprof工具为top命令提供的默认值中。 相反，使用<code>top cum</code>命令或直接对这些函数使用<code>list</code>命令，并将注意力集中在累计百分比列上。</p></blockquote><ul><li><strong>runtime.gcBgMarkWorker</strong>: 专用标记工作goroutine的入口点。 这里花费的时间与GC频率以及对象图的复杂性和大小成比例。 它表示应用程序标记和扫描所用时间的基准。</li></ul><p>注意：在一个大部分时间都处于空闲状态的Go应用程序中，Go GC会消耗额外的（空闲的）CPU资源来更快地完成任务。 结果，该函数可以表示它认为是免费采样部分。 发生这种情况的一个常见原因是，应用程序仅在一个 goroutine 中运行，但 GOMAXPROCS 大于 1。</p><ul><li><strong>runtime.mallocgc</strong>:堆内存的内存分配器的入口点。 此处花费的大量累积时间（&gt; 15%）通常表示分配了大量内存。</li><li><strong>runtime.gcAssistAlloc</strong>: goroutine进入这个函数是为了腾出一些时间来帮助GC进行扫描和标记。 这里花费的大量累积时间（&gt; 5%）表明应用程序在分配速度方面可能超过了GC标记速度。 它表示GC的影响程度特别高，并且还表示应用程序在标记和扫描上花费的时间。 请注意，它包含在<code>runtime.mallocgc</code>调用树中，因此它也会使该调用树累计时间增加。</li></ul><p><strong>2、trace</strong></p><p>虽然CPU profile文件非常适合用于确定时间在聚合中的花费点，但对于指示更细微、更罕见或与延迟具体相关的性能成本，它们的用处不大。 另一方面，trace提供了Go语言程序执行的一个短窗口的丰富而深入的视图。 它们包含了与Go GC相关的各种事件，可以直接观察到具体的执行路径，沿着应用程序与Go GC的交互方式。 所有被跟踪的GC事件都在跟踪查看器中被方便地标记为GC事件。</p><p>有关如何开始使用执行trace的信息，请参阅 <a href="https://pkg.go.dev/runtime/trace">runtime/trace</a> 的文档。</p><p><strong>3、GC Trace</strong></p><p>当所有其他方法都失败时，Go GC还提供了一些不同的特定跟踪，这些跟踪提供了对GC行为的更深入的了解。 这些踪迹总是被直接打印到 STDERR 中，每个GC周期打印一行，并且通过所有Go语言程序都能识别的 GODEBUG 环境变量来配置。 它们主要用于调试Go GC本身，因为它们需要对GC实现的细节有一定的了解，但是偶尔也可以用于更好地理解GC的行为。</p><p>通过设置<code>GODEBUG=gctrace=1</code>，可以启用GC Trace。 此跟踪生成的输出记录在<a href="https://pkg.go.dev/runtime#hdr-Environment_Variables">runtime</a>包文档的环境变量部分中。</p><p>一个称为<code>pacer trace</code>的技术用来补充GC跟踪，提供了更深入的见解，它通过设置<code>GODEBUG=gcpacertrace=1</code>来启用。 解释这个输出需要理解GC的<code>pacer</code>（参见<a href="https://tip.golang.org/doc/gc-guide#Additional_resources">其他参考资料</a>），这超出了本指南的范围。</p><h3 id="消除堆分配">消除堆分配</h3><p>降低GC成本的一种方法是让GC管理较少的值。 下面描述的技术可以带来一些最大的性能改进，因为正如GOGC部分所展示的，Go语言程序的分配率是GC频率的一个主要因素，GC频率是本指南使用的关键成本度量。</p><h4 id="heap-profiling">Heap profiling</h4><p>在确定GC是一个巨大开销的来源之后，消除堆分配的下一步是找出它们中的大多数来自哪里。 为此，memory profiles 文件（实际上是堆内存 profile 文件）非常有用。 请查看<a href="https://pkg.go.dev/runtime/pprof#hdr-Profiling_a_Go_program">文档</a>以了解如何开始使用它们。</p><p>内存 profile 文件描述了程序堆中分配的来源，并通过分配时的堆栈跟踪来标识它们。 每个内存 profile 文件可以按四种方式分析：</p><ul><li>inuse_objects - 存活对象的数量</li><li>inuse_space - 存活对象使用的内存量，以字节为单位</li><li>alloc_objects - 自Go程序开始执行以来已经分配的对象数</li><li>alloc_space - 自Go程序开始执行以来所分配的内存总量</li></ul><p>在这些不同的堆内存视图之间切换可以通过pprof工具的 <code>-sample_index</code>标志来完成，或者在交互式使用该工具时通过<code>sample_index</code>选项来完成。</p><blockquote><p>注意：默认情况下，内存 profile 文件只对堆对象的子集进行采样（间隔采样），因此它们不会包含有关每个堆分配的信息。 但是，这足以找到热点。 若要更改采样率，请参见<a href="https://pkg.go.dev/runtime#pkg-variables">runtime.MemProfileRate</a>。</p></blockquote><p>为了降低GC成本，alloc_space通常是最有用的视图，因为它直接对应于分配率。 此视图将指示可提供最大益处的分配热点。</p><h4 id="逃逸分析">逃逸分析</h4><p>一旦在<a href="https://tip.golang.org/doc/Heap_profiling">Heap  profile 文件</a>的帮助下确定了堆分配热点，如何消除它们？ 关键是要利用Go语言编译器的逃逸分析，让Go语言编译器为这个内存找到替代的、更有效的存储空间，比如在goroutine栈中。 幸运的是，Go语言编译器能够描述为什么要将Go语言的值逃逸到堆中。 有了这些知识，就变成了重新组织源代码以改变分析结果的问题（这通常是最困难的部分，但超出了本指南的范围）。</p><p>至于如何从Go语言编译器的逃逸分析中获取信息，最简单的方法是通过Go语言编译器支持的调试标志，该标志以文本格式描述了对某个包应用或未应用的所有优化。 这包括值是否逃逸。 尝试下面的命令，其中<code>package</code>是Go语言包的路径:<br /><code>$go build -gcflags=-m=3 [package]</code></p><p>此信息也可以在 VS Code 中可视化为覆盖图。 此覆盖在VS Code Go插件设置中配置和启用:</p><ul><li>设置<a href="https://github.com/golang/vscode-go/wiki/settings#uicodelenses">ui.codelenses设置以包括gc_details</a></li><li>通过<a href="https://github.com/golang/vscode-go/wiki/settings#uidiagnosticannotations">将ui.diagnostic.annotations设置为包括逃逸，启用逃逸分析的覆盖</a></li></ul><p>最后，Go编译器以机器可读（JSON）格式提供了这些信息，可以用来构建其他定制工具。 有关这方面的更多信息，请参见<a href="https://cs.opensource.google/go/go/+/master:src/cmd/compile/internal/logopt/log_opts.go;l=25;drc=351e0f4083779d8ac91c05afebded42a302a6893">Go语言源代码中的文档</a>。</p><h3 id="基于特定实现的优化">基于特定实现的优化</h3><p>Go GC对存活内存的统计很敏感，因为对象和指针的复杂图既限制了并行性，又为GC生成了更多的工作。 因此，GC包含了一些针对特定公共结构的优化。 下面列出了对性能优化最直接有用的方法。</p><blockquote><p>注意：应用下面的优化可能会因为混淆意图而降低代码的可读性，并且可能无法在Go语言的各个版本中保持。 希望只在最重要的地方应用这些优化。 可以使用确定成本一节中列出的工具来确定这些地点。</p></blockquote><ul><li>无指针值与其他值分开。</li></ul><p>因此，依赖于索引而不是指针值的数据结构虽然类型不太好，但性能可能会更好。 仅当对象图很复杂并且 GC 花费大量时间标记和扫描时，才值得这样做。</p><ul><li>GC将在对象中的最后一个指针处停止扫描。</li></ul><p>因此，将结构体类型中的指针字段放在开头可能是有利的。 只有当应用程序花费大量时间进行标记和扫描时，才值得这样做。 (理论上，编译器可以自动执行此操作，但尚未实现，并且结构字段的排列方式与源代码中所写的相同。）</p><p>此外，GC必须与它所看到的几乎每个指针交互，因此，例如，使用切片中的索引而不是指针，可以帮助降低GC成本。</p><h2 id="附录">附录</h2><h3 id="关于-gogc-的附加说明">关于 GOGC 的附加说明</h3><p><a href="https://tip.golang.org/doc/gc-guide#GOGC">GOGC 部分</a>声称，将 GOGC 翻倍会使堆内存开销翻倍，并将 GC CPU 成本减半。 要了解原因，让我们在数学上对其进行分解。</p><p>首先，堆目标(<em>Target heap memory</em> )为总堆大小设置一个目标。 然而，这个目标主要影响新的堆内存，因为存活堆(<em>Live heap</em> )是应用程序的基础。</p><pre><code class="language-bash">Target heap memory = Live heap + (Live heap + GC roots) * GOGC / 100Total heap memory = Live heap + New heap memory⇒New heap memory = (Live heap + GC roots) * GOGC / 100</code></pre><p>由此我们可以看到，将 GOGC 翻倍也会使应用程序在每个周期分配的新堆内存量(New heap memory)翻倍，这会加大堆内存开销。 请注意，Live heap + GC roots 是 GC 需要扫描的内存量的近似值。</p><p>接下来，我们来看看 GC CPU 开销。 总成本可以分解为每个周期的成本乘以一段时间 T 内的 GC 频率。</p><pre><code class="language-bash">Total GC CPU cost = (GC CPU cost per cycle) * (GC frequency) * T总开销 = 单次GC开销 * 频率 * 时间</code></pre><p>每个周期的 GC CPU 成本可以从 GC 模型中得出：</p><pre><code class="language-bash">GC CPU cost per cycle = (Live heap + GC roots) * (Cost per byte) + Fixed cost存活对象数 * 扫描每字节开销 + 固定开销</code></pre><p>请注意，此处忽略清扫阶段成本，因为标记和扫描成本占主导地位。</p><p>稳态由恒定的分配率和恒定的每字节成本定义，因此在稳态下，我们可以从这个新的堆内存中推导出 GC 频率：</p><pre><code class="language-bash">GC frequency = (Allocation rate) / (New heap memory) = (Allocation rate) / ((Live heap + GC roots) * GOGC / 100)</code></pre><p>把这些放在一起，我们得到了总成本的完整方程：</p><pre><code class="language-bash">Total GC CPU cost = (Allocation rate) / ((Live heap + GC roots) * GOGC / 100) * ((Live heap + GC roots) * (Cost per byte) + Fixed cost) * T</code></pre><p>对于足够大的堆（代表大多数情况），GC 周期的边际成本支配着固定成本。 这可以显着简化 GC CPU 总成本公式。</p><pre><code class="language-bash">Total GC CPU cost = (Allocation rate) / (GOGC / 100) * (Cost per byte) * T</code></pre><p>从这个简化的公式中，我们可以看到，如果我们将 GOGC 翻倍，我们将总 GC CPU 成本减半。 （请注意，本指南中的可视化确实模拟了固定成本，因此当 GOGC 翻倍时，它们报告的 GC CPU 开销不会完全减半。）此外，GC CPU 成本在很大程度上取决于分配率和扫描内存的每字节成本。 有关如何具体降低这些成本的更多信息，请参阅优化指南。</p><p>注意：存活堆的大小和 GC 实际需要扫描的内存量之间存在差异：相同大小的活动堆但具有不同的结构会导致不同的 CPU 成本，但内存成本相同 ，可能导致不同的权衡。 这就是为什么堆的结构是稳态定义的一部分。 堆目标可以说应该只包括可扫描的活动堆，作为 GC 需要扫描的内存的更接近的近似值，但是当可扫描的活动堆数量非常少但活动堆很大时，这会导致退化行为。</p><h2 id="参考文章">参考文章</h2><p><a href="https://tip.golang.org/doc/gc-guide">https://tip.golang.org/doc/gc-guide</a></p><p><a href="https://colobu.com/2022/07/16/A-Guide-to-the-Go-Garbage-Collector/">https://colobu.com/2022/07/16/A-Guide-to-the-Go-Garbage-Collector/</a></p>]]>
                    </description>
                    <pubDate>Fri, 07 Oct 2022 23:50:45 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[2021年终总结]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/2021年终总结</link>
                    <description>
                            <![CDATA[<h2 id="2021年终总结">2021年终总结</h2><p>FYI：博客地址已由 <a href="https://blog.leonardwang.cn/">https://blog.leonardwang.cn/</a>  更换为 <a href="https://blog.leonard.wang/">https://blog.leonard.wang/</a> ，当前做了重定向，惠存，谢谢~</p><p>终于在假期最后一天写完了<strong>简短</strong>的年终总结。</p><h3 id="技术">技术</h3><p>从0到1实现了协程调度器</p><p>了解了解释器的实现原理</p><p>From Huawei To Bytedance，告别了一些可爱的人遇见了一些新的可爱的人</p><p>其实我写了离职感言的，没找到合适的时机来发，就发到博客吧</p><blockquote><p>2018.07.09-2021.10.18 结束了校招的第一段旅程<br />感恩各位大佬的谆谆教诲，从懵懵懂懂到可以独立开发特性独立负责模块…<br />感谢各位可爱的同事在工作生活中给予的诸多帮助</p><p>还记得第一个MR被合入时的兴奋<br />还记得大家集思广益解决性能比拼中瓶颈问题时，被证明方案效果显著时的称赞<br />还记得特性实际商用中遇到重大缺陷时绞尽脑汁的想方案做实验，当程序终于不panic时的如释重负…<br />有幸为自研编程语言贡献了自己的一份力量，还记得它可以并发打印1 2 3时的喜形于色…</p><p>也体验过定位一线问题时凌晨5点的园区<br />也体验过联调被拉着电话定位问题到凌晨3点<br />也……</p><p>祝好！</p><p>休假开始<br />期待下一段旅程～</p></blockquote><p>和 <a href="https://gin-vue-admin.com/">gin-vue-admin</a>的小伙伴搞了些事情</p><p>加入了 <a href="https://golangcn.org/">Golang China Club</a></p><p>嗯，，从10月份开始鸽了博客 :)</p><h3 id="娱乐">娱乐</h3><p>入了一些好玩的东西，比如说龙芯3A5000、静电容键盘、新的软路由（使用PonStick 抛弃了光猫）、新的群晖（Ds1821）、Apple TV 等。。。</p><h2 id="2022年计划">2022年计划</h2><ul><li>从“零”开始实现Go调度器系列，估计要跟随一波1.18版本了。</li><li>Go调测特性（profile、trace等）的源码分析，因为近期工作内容需要这些数据来定位问题，遇到一些细节总是不清楚这个数据具体是采集的从哪个时间点到哪个时间点。</li><li>Linux 内存管理和线程调度相关代码的学习。</li><li>ebpf 尝试编写一些小程序。</li><li>可能会从技术学习的角度开始阅读<a href="https://github.com/cloudwego">cloudwego</a>、<a href="https://github.com/bytedance/sonic">sonic</a> 的源码。</li><li>可能会开始更新一些Go编译器相关代码分析。</li><li>可能会更新一些家庭网络和智能家居相关的方案，目前在组 主Homekit，辅米家 的智能家居系统。</li></ul>]]>
                    </description>
                    <pubDate>Sun, 06 Feb 2022 22:12:31 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Go的Finalizer]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/finalizer</link>
                    <description>
                            <![CDATA[<h2 id="什么是finalizer">什么是Finalizer</h2><blockquote><p>In <a href="https://en.wikipedia.org/wiki/Computer_science">computer science</a>, a <strong>finalizer</strong> or <strong>finalize method</strong> is a special <a href="https://en.wikipedia.org/wiki/Method_(computer_science)">method</a> that performs <strong>finalization</strong>, generally some form of cleanup.</p></blockquote><p>Finalizer通常用来执行一些清理操作。</p><h2 id="finalizer终结器-和--destructor析构函数">finalizer（终结器） 和  destructor（析构函数）</h2><blockquote><p>The terminology of &quot;finalizer&quot; and &quot;finalization&quot; versus &quot;destructor&quot; and &quot;destruction&quot; varies between authors and is sometimes unclear.</p><p>In common usage, a <em>destructor</em> is a method called deterministically on object destruction, and the archetype is C++ destructors; while a finalizer is called non-deterministically by the garbage collector, and the archetype is Java <code>finalize</code> methods.</p></blockquote><p>析构函数是在对象销毁时确定性调用的方法，原型是 C++ 析构函数；而垃圾收集器不确定地调用终结器，并且原型是 Java<code>finalize</code>方法。</p><p>析构函数的调用时间点是确定的，拿C++举例，当跳出class声明作用域或者显式调用delete执行清理时，如果该class存在析构函数，即会在当前线程执行。</p><p>终结器的调用时间不是确定的，由垃圾回收期决定何时执行，可以保证的是在该对象真正被回收之前会执行其注册的<code>finalize</code>，在哪个线程上执行也是不确定的。</p><h2 id="go中的finalizer">Go中的Finalizer</h2><h3 id="如何使用">如何使用</h3><p>推荐用法</p><pre><code class="language-go">package mainimport (&quot;log&quot;&quot;runtime&quot;&quot;time&quot;)type test intfunc findRoad(t *test) {// 一般在这里进行资源回收log.Println(&quot;test:&quot;, *t)}func entry() {var rd test = test(1111)r := &amp;rd// 解除绑定并执行对应函数下一次gc在进行清理runtime.SetFinalizer(r, findRoad)}func main() {entry()for i := 0; i &lt; 5; i++ {time.Sleep(time.Second)runtime.GC()}}// OUTPUT:// 2021/08/26 23:22:29 test: 1111</code></pre><p><code>runtime</code>中提供了<code>runtime.SetFinalizer</code>来为对象注册<code>Finalizer</code>函数</p><p>该函数有两个入参</p><ul><li>入参一为注册Finalizer的对象地址</li><li>入参二为 被&quot;终结&quot;时要执行的回调函数，该函数正常情况下应接受一个与注册对象类型相同的指针，如上方例子中的findRoad</li></ul><p>当然也可以用它来做一些骚操作</p><p>不太推荐的用法</p><p>如上方所述，Finalizer是在已经确定该对象不再存活的情况下，被回收之前执行的，那么我们可以在Finalizer中让这个本该被回收的对象，再“活过来”</p><pre><code class="language-go">package mainimport (&quot;log&quot;&quot;runtime&quot;&quot;time&quot;)type test intfunc findRoad(t *test) {log.Println(&quot;test:&quot;, *t)*t = 3global = t}var global interface{}func entry() {var rd test = test(1)r := &amp;rdgo func() {rd = 2}()runtime.SetFinalizer(r, findRoad)}func main() {entry()for i := 0; i &lt; 5; i++ {time.Sleep(time.Second)runtime.GC()}log.Println(&quot;main:&quot;, *(global).(*test))}// OUTPUT:// 2021/08/26 23:28:57 test: 2// 2021/08/26 23:29:00 main: 3</code></pre><p>禁止使用的用法</p><p>当然也可以在Finalizer函数中调用会导致协程阻塞的方法，比如说<code>Sleep()</code>、<code>Mutex.lock()</code></p><p>注：这个例子仅用来说明在Go中 Finalizer是在协程环境中执行的，实际使用中Finalizer中仅应该做一些快速简单的清理操作，否则会阻塞其他Finalizer的执行（源码中可以证明这一点）。</p><pre><code class="language-go">package mainimport (&quot;log&quot;&quot;runtime&quot;&quot;sync&quot;&quot;time&quot;)type test intfunc findRoad(t *test) {log.Println(&quot;test:&quot;, *t)time.Sleep(time.Second)log.Println(&quot;Sleep Done&quot;)}func entry() {var rd test = test(1)r := &amp;rdruntime.SetFinalizer(r, findRoad)}func main() {entry()for i := 0; i &lt; 5; i++ {time.Sleep(time.Second)runtime.GC()}}// OUTPUT:// 2021/08/26 23:42:35 test: 1// 2021/08/26 23:42:36 Sleep Done</code></pre><h3 id="标准库中的使用">标准库中的使用?</h3><h3 id="实现原理">实现原理</h3><p>TODO：总结</p><h4 id="设置">设置</h4><p>TODO:mspan简单介绍</p><pre><code class="language-go">// $GOROOT/src/runtime/mfinal.gofunc SetFinalizer(obj interface{}, finalizer interface{}) {e := efaceOf(&amp;obj)// 去除参数校验逻辑f := efaceOf(&amp;finalizer)ftyp := f._typeokarg:// compute size needed for return parametersnret := uintptr(0)for _, t := range ft.out() {nret = alignUp(nret, uintptr(t.align)) + uintptr(t.size)}nret = alignUp(nret, sys.PtrSize)// 确保已创建finalizer goroutinecreatefing()// 切换到系统栈执行addfinalizersystemstack(func() {if !addfinalizer(e.data, (*funcval)(f.data), nret, fint, ot) {throw(&quot;runtime.SetFinalizer: finalizer already set&quot;)}})}func createfing() {// 使用CAS来拿到执行权，保证只创建一个goroutineif fingCreate == 0 &amp;&amp; atomic.Cas(&amp;fingCreate, 0, 1) {go runfinq()}}</code></pre><pre><code class="language-go">// $GOROOT/src/runtime/mheap.go// Adds a finalizer to the object p. Returns true if it succeeded.func addfinalizer(p unsafe.Pointer, f *funcval, nret uintptr, fint *_type, ot *ptrtype) bool {lock(&amp;mheap_.speciallock)// 申请一个 specialfinalizers := (*specialfinalizer)(mheap_.specialfinalizeralloc.alloc())unlock(&amp;mheap_.speciallock)// 初始化s.special.kind = _KindSpecialFinalizers.fn = fs.nret = nrets.fint = fints.ot = otif addspecial(p, &amp;s.special) {// GC期间调用SetFinalizer，会继续标记它的Filedif gcphase != _GCoff {base, _, _ := findObject(uintptr(p), 0, 0)mp := acquirem()gcw := &amp;mp.p.ptr().gcw// Mark everything reachable from the object// so it's retained for the finalizer.scanobject(base, gcw)// Mark the finalizer itself, since the// special isn't part of the GC'd heap.scanblock(uintptr(unsafe.Pointer(&amp;s.fn)), sys.PtrSize, &amp;oneptrmask[0], gcw, nil)releasem(mp)}return true}// There was an old finalizerlock(&amp;mheap_.speciallock)mheap_.specialfinalizeralloc.free(unsafe.Pointer(s))unlock(&amp;mheap_.speciallock)return false}func addspecial(p unsafe.Pointer, s *special) bool {// 根据p找到对应的mspanspan := spanOfHeap(uintptr(p))mp := acquirem()span.ensureSwept()// 计算在对应mspan中的偏移，可以根据这个信息反查到对应的对象offset := uintptr(p) - span.base()kind := s.kindlock(&amp;span.speciallock)// Find splice point, check for existing record.// 添加到span.specials链表中t := &amp;span.specialsfor {x := *tif x == nil {break}if offset == uintptr(x.offset) &amp;&amp; kind == x.kind {unlock(&amp;span.speciallock)releasem(mp)return false // already exists}if offset &lt; uintptr(x.offset) || (offset == uintptr(x.offset) &amp;&amp; kind &lt; x.kind) {break}t = &amp;x.next}// Splice in record, fill in offset.s.offset = uint16(offset)s.next = *t*t = sspanHasSpecials(span)unlock(&amp;span.speciallock)releasem(mp)return true}</code></pre><h4 id="触发">触发</h4><p>在GC mark和sweep期间都需要进行对应的处理</p><p>mark：每个mspan的specials链表是GC中的根节点，对设置了Finalizer的对象的子field进行扫描标记，但自身不会在扫描根节点过程中被标记，自身是否存活是根据从其他root节点是否可达来确定的。</p><p>标记其子field是因为，如果本轮GC过程中设置了Finalizer的对象不可达，即会执行所设置的Finalizer回调函数，并且该对象自身会被作为入参传入。而执行回调的时机是在sweep清扫阶段，所以需要对其子field进行保活，这也就导致了后续的“循环引用和Finalizer一起使用会导致内存泄漏”的问题</p><pre><code class="language-go">// $GOROOT/src/runtime/mgcmark.gofunc markroot(gcw *gcWork, i uint32) {// Note: if you add a case here, please also update heapdump.go:dumproots.switch {case work.baseSpans &lt;= i &amp;&amp; i &lt; work.baseStacks:// mark mspan.specialsmarkrootSpans(gcw, int(i-work.baseSpans))default:}}func markrootSpans(gcw *gcWork, shard int) {// 找到对应mspan// Construct slice of bitmap which we'll iterate over.specialsbits := ha.pageSpecials[arenaPage/8:]specialsbits = specialsbits[:pagesPerSpanRoot/8]for i := range specialsbits {// Find set bits, which correspond to spans with specials.specials := atomic.Load8(&amp;specialsbits[i])if specials == 0 {continue}for j := uint(0); j &lt; 8; j++ {// Lock the specials to prevent a special from being// removed from the list while we're traversing it.lock(&amp;s.speciallock)// 遍历mspan中的specialsfor sp := s.specials; sp != nil; sp = sp.next {// 只关心_KindSpecialFinalizerif sp.kind != _KindSpecialFinalizer {continue}// don't mark finalized object, but scan it so we// retain everything it points to.spf := (*specialfinalizer)(unsafe.Pointer(sp))// A finalizer can be set for an inner byte of an object, find object beginning.p := s.base() + uintptr(spf.special.offset)/s.elemsize*s.elemsize// Mark everything that can be reached from// the object (but *not* the object itself or// we'll never collect it).scanobject(p, gcw)// The special itself is a root.scanblock(uintptr(unsafe.Pointer(&amp;spf.fn)), sys.PtrSize, &amp;oneptrmask[0], gcw, nil)}unlock(&amp;s.speciallock)}}}</code></pre><pre><code class="language-go">func (sl *sweepLocked) sweep(preserve bool) bool {// It's critical that we enter this function with preemption disabled,// GC must not start while we are in the middle of this function._g_ := getg()s := sl.mspanhadSpecials := s.specials != nilsiter := newSpecialsIter(s)// 遍历specials链表for siter.valid() {// A finalizer can be set for an inner byte of an object, find object beginning.// 根据offset反查对应的object在mspan中的位置objIndex := uintptr(siter.s.offset) / sizep := s.base() + objIndex*sizembits := s.markBitsForIndex(objIndex)// 如果该object没有被标记，说明在这轮GC中是不可以对象，可以被回收// 触发Finalizer机制if !mbits.isMarked() {// This object is not marked and has at least one special record.// Pass 1: see if it has at least one finalizer.hasFin := falseendOffset := p - s.base() + sizefor tmp := siter.s; tmp != nil &amp;&amp; uintptr(tmp.offset) &lt; endOffset; tmp = tmp.next {if tmp.kind == _KindSpecialFinalizer {// Stop freeing of object if it has a finalizer.// 标记该对象，使其多存活一周期mbits.setMarkedNonAtomic()hasFin = truebreak}}// Pass 2: queue all finalizers _or_ handle profile record.// 从special列表移除该记录，下轮GC时，该object已经不再拥有Finalizerfor siter.valid() &amp;&amp; uintptr(siter.s.offset) &lt; endOffset {// Find the exact byte for which the special was setup// (as opposed to object beginning).special := siter.sp := s.base() + uintptr(special.offset)if special.kind == _KindSpecialFinalizer || !hasFin {siter.unlinkAndNext()freeSpecial(special, unsafe.Pointer(p), size)} else {// The object has finalizers, so we're keeping it alive.// All other specials only apply when an object is freed,// so just keep the special record.siter.next()}}} else {// object is still liveif siter.s.kind == _KindSpecialReachable {// 测试用special := siter.unlinkAndNext()(*specialReachable)(unsafe.Pointer(special)).reachable = truefreeSpecial(special, unsafe.Pointer(p), size)} else {// 对象存活，保留该Finalizer，继续遍历// keep special recordsiter.next()}}}if hadSpecials &amp;&amp; s.specials == nil {spanHasNoSpecials(s)}}// freeSpecial performs any cleanup on special s and deallocates it.// s must already be unlinked from the specials list.func freeSpecial(s *special, p unsafe.Pointer, size uintptr) {switch s.kind {case _KindSpecialFinalizer:sf := (*specialfinalizer)(unsafe.Pointer(s))queuefinalizer(p, sf.fn, sf.nret, sf.fint, sf.ot)lock(&amp;mheap_.speciallock)mheap_.specialfinalizeralloc.free(unsafe.Pointer(sf))unlock(&amp;mheap_.speciallock)case _KindSpecialProfile:sp := (*specialprofile)(unsafe.Pointer(s))mProf_Free(sp.b, size)lock(&amp;mheap_.speciallock)mheap_.specialprofilealloc.free(unsafe.Pointer(sp))unlock(&amp;mheap_.speciallock)case _KindSpecialReachable:sp := (*specialReachable)(unsafe.Pointer(s))sp.done = true// The creator frees these.default:throw(&quot;bad special kind&quot;)panic(&quot;not reached&quot;)}}// 提交到finq队列，并设置fingwakefunc queuefinalizer(p unsafe.Pointer, fn *funcval, nret uintptr, fint *_type, ot *ptrtype) {if gcphase != _GCoff {// Currently we assume that the finalizer queue won't// grow during marking so we don't have to rescan it// during mark termination. If we ever need to lift// this assumption, we can do it by adding the// necessary barriers to queuefinalizer (which it may// have automatically).throw(&quot;queuefinalizer during GC&quot;)}lock(&amp;finlock)if finq == nil || finq.cnt == uint32(len(finq.fin)) {if finc == nil {finc = (*finblock)(persistentalloc(_FinBlockSize, 0, &amp;memstats.gcMiscSys))finc.alllink = allfinallfin = fincif finptrmask[0] == 0 {// Build pointer mask for Finalizer array in block.// Check assumptions made in finalizer1 array above.if (unsafe.Sizeof(finalizer{}) != 5*sys.PtrSize ||unsafe.Offsetof(finalizer{}.fn) != 0 ||unsafe.Offsetof(finalizer{}.arg) != sys.PtrSize ||unsafe.Offsetof(finalizer{}.nret) != 2*sys.PtrSize ||unsafe.Offsetof(finalizer{}.fint) != 3*sys.PtrSize ||unsafe.Offsetof(finalizer{}.ot) != 4*sys.PtrSize) {throw(&quot;finalizer out of sync&quot;)}for i := range finptrmask {finptrmask[i] = finalizer1[i%len(finalizer1)]}}}block := fincfinc = block.nextblock.next = finqfinq = block}f := &amp;finq.fin[finq.cnt]atomic.Xadd(&amp;finq.cnt, +1) // Sync with markrootsf.fn = fnf.nret = nretf.fint = fintf.ot = otf.arg = pfingwake = trueunlock(&amp;finlock)}</code></pre><h4 id="执行">执行</h4><pre><code class="language-go">// Finds a runnable goroutine to execute.// Tries to steal from other P's, get g from local or global queue, poll network.func findrunnable() (gp *g, inheritTime bool) {_g_ := getg()// The conditions here and in handoffp must agree: if// findrunnable would return a G to run, handoffp must start// an M.top:_p_ := _g_.m.p.ptr()// 调度过程中发现需要执行Finalizer，唤醒之前创建的runfinq()if fingwait &amp;&amp; fingwake {if gp := wakefing(); gp != nil {ready(gp, 0, true)}}}// This is the goroutine that runs all of the finalizersfunc runfinq() {var (frame    unsafe.Pointerframecap uintptrargRegs  int)for {lock(&amp;finlock)fb := finqfinq = nilif fb == nil {gp := getg()fing = gpfingwait = truegoparkunlock(&amp;finlock, waitReasonFinalizerWait, traceEvGoBlock, 1)continue}argRegs = intArgRegsunlock(&amp;finlock)if raceenabled {racefingo()}for fb != nil {for i := fb.cnt; i &gt; 0; i-- {// 从队列中取出待执行的任务f := &amp;fb.fin[i-1]var regs abi.RegArgsvar framesz uintptrif argRegs &gt; 0 {// The args can always be passed in registers if they're// available, because platforms we support always have no// argument registers available, or more than 2.//// But unfortunately because we can have an arbitrary// amount of returns and it would be complex to try and// figure out how many of those can get passed in registers,// just conservatively assume none of them do.framesz = f.nret} else {// Need to pass arguments on the stack too.framesz = unsafe.Sizeof((interface{})(nil)) + f.nret}if framecap &lt; framesz {// The frame does not contain pointers interesting for GC,// all not yet finalized objects are stored in finq.// If we do not mark it as FlagNoScan,// the last finalized object is not collected.frame = mallocgc(framesz, nil, true)framecap = framesz}if f.fint == nil {throw(&quot;missing type in runfinq&quot;)}r := frameif argRegs &gt; 0 {r = unsafe.Pointer(&amp;regs.Ints)} else {// frame is effectively uninitialized// memory. That means we have to clear// it before writing to it to avoid// confusing the write barrier.*(*[2]uintptr)(frame) = [2]uintptr{}}switch f.fint.kind &amp; kindMask {case kindPtr:// direct use of pointer*(*unsafe.Pointer)(r) = f.argcase kindInterface:ityp := (*interfacetype)(unsafe.Pointer(f.fint))// set up with empty interface(*eface)(r)._type = &amp;f.ot.typ(*eface)(r).data = f.argif len(ityp.mhdr) != 0 {// convert to interface with methods// this conversion is guaranteed to succeed - we checked in SetFinalizer(*iface)(r).tab = assertE2I(ityp, (*eface)(r)._type)}default:throw(&quot;bad kind in runfinq&quot;)}fingRunning = true// 使用反射的方式调用注册的回调函数// 可以看到后台只有这一个goroutine在勤勤恳恳按顺序的执行Finalizer回调// 所以在Finalizer中调用阻塞函数是一件非常危险的事情reflectcall(nil, unsafe.Pointer(f.fn), frame, uint32(framesz), uint32(framesz), uint32(framesz), &amp;regs)fingRunning = false// Drop finalizer queue heap references// before hiding them from markroot.// This also ensures these will be// clear if we reuse the finalizer.f.fn = nilf.arg = nilf.ot = nilatomic.Store(&amp;fb.cnt, i-1)}next := fb.nextlock(&amp;finlock)fb.next = fincfinc = fbunlock(&amp;finlock)fb = next}}}</code></pre><h3 id="为什么和循环引用一起使用会导致内存泄漏">为什么和循环引用一起使用会导致内存泄漏？</h3><p>这个应该也属于禁止使用的用法</p><pre><code class="language-go">package mainimport (&quot;log&quot;&quot;runtime&quot;&quot;time&quot;)type X struct {data [1 &lt;&lt; 20][10]byte // 构造大对象，使其逃逸到堆上ptr  *X}func test() {var a, b Xa.ptr = &amp;bb.ptr = &amp;aruntime.SetFinalizer(&amp;a, func(*X) { log.Println(&quot;Finalizer a&quot;) })runtime.SetFinalizer(&amp;b, func(*X) { log.Println(&quot;Finalizer b&quot;) })}func main() {for i := 0; i &lt; 10; i++ {test()runtime.GC()time.Sleep(time.Second)}}// OUTPUT:// gc 1 @0.001s 6%: 0.009+1.3+0.002 ms clock, 0.072+0.10/1.3/1.2+0.018 ms cpu, 10-&gt;10-&gt;10 MB, 11 MB goal, 8 P// gc 2 @0.003s 15%: 0.007+3.8+0.002 ms clock, 0.062+0/7.4/18+0.016 ms cpu, 20-&gt;20-&gt;60 MB, 21 MB goal, 8 P// gc 3 @0.007s 17%: 0.015+2.7+0.002 ms clock, 0.12+0/5.4/5.3+0.021 ms cpu, 60-&gt;60-&gt;40 MB, 120 MB goal, 8 P (forced)// gc 4 @1.021s 0%: 0.068+5.6+0.002 ms clock, 0.54+0/11/16+0.019 ms cpu, 60-&gt;60-&gt;80 MB, 80 MB goal, 8 P (forced)// gc 5 @2.032s 0%: 0.054+10+0.002 ms clock, 0.43+0/21/61+0.021 ms cpu, 100-&gt;100-&gt;120 MB, 160 MB goal, 8 P (forced)</code></pre><p>可以看出，Finalizer并没有被执行，并且GC无法回收掉函数中的局部变量。</p><p>注：解除a和b的循环引用关系，或者不对其SetFinalizer都可以正常回收，这并不是GC的问题。</p><blockquote><p>Finalizers are run in dependency order: if A points at B, both have finalizers, and they are otherwise unreachable, only the finalizer for A runs; once A is freed, the finalizer for B can run. If a cyclic structure includes a block with a finalizer, that cycle is not guaranteed to be garbage collected and the finalizer is not guaranteed to run, because there is no ordering that respects the dependencies.</p></blockquote><p>其实主要是因为Finalizers也会作为GC的根节点，会进行标记它的子Field，所以在这种场景下，a和b在每轮GC中都是存活的(a是通过b的Finalizers标记存活，b是通过a的Finalizers标记存活)，也就导致了内存泄漏，在sweep期间，对于存活对象也不会调用其Finalizers函数，所以Finalizers回调也并不会被执行。</p><h2 id="参考链接">参考链接</h2><p><a href="https://juejin.cn/post/6844903937649147912">深入理解Go-runtime.SetFinalizer原理剖析</a></p><p><a href="https://www.cnblogs.com/binHome/p/12901392.html">Go如何巧妙使用runtime.SetFinalizer</a></p><p><a href="https://segmentfault.com/a/1190000005066283">Go 性能优化技巧 10/10</a></p>]]>
                    </description>
                    <pubDate>Fri, 27 Aug 2021 01:11:48 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[从“零”开始实现Go1.17调度器]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/implement-scheduler</link>
                    <description>
                            <![CDATA[<h2 id="并发整体设计">并发整体设计</h2><p>Go调度器的主要函数为 <a href="https://github.com/golang/go/blob/72ab3ff68b1ec894fe5599ec82b8849f3baa9d94/src/runtime/proc.go#L3291">schedule</a>，但是由于版本迭代新功能的不断增加，该函数的流程也越来越复杂，本系列计划从一个最简单的调度器开始，一步一步添加功能，来实现完整的Go调度器，也便于大家理解每部分的作用。</p><p>简单来说，Go从语言层面支持了协程，使用go 关键字创建协程；Go的协程（goroutine）是轻量级的线程，是“<strong>有栈</strong>”的，每个Goroutine默认的栈大小为2KB；支持自动扩缩容；由Go的调度器提供了一种<strong>M：N的调度模型</strong>，在用户态而非内核态进行调度。</p><p>TODO: 1:1、1:N、M:N 调度模型</p><p>除了创建协程，还提供了协程级别的sleep、mutex、signal等。</p><p>为了保障并发安全，提供了原子操作、mutex、channel、waitGroup等等并发原语。</p><p>注：协程中的“协”，本意为协作式，但是从实现上来说，Goroutine的调度方式并不完全是协作式的。如果每个Goroutine只有主动调用runtime.Gosched()才会释放执行权的话，是协作式的，但是Go还会在安全点抢占运行时间过长的Goroutine，以及go1.14中加入的基于信号的抢占式调度，都使得Goroutine可能会在程序员不感知的情况下被切换。</p><h2 id="调度器与操作系统的交互">调度器与操作系统的交互</h2><p>TODO: 补充</p><p>或者说需要操作系统提供的能力</p><ul><li><p>多线程</p><p>clone 系统调用</p><p>pthread库</p></li><li><p>原子操作</p><p>x86对应的汇编指令</p></li><li><p>线程锁</p><p>futex信号量 + 原子操作</p></li></ul><h2 id="朴素调度器">朴素调度器</h2><p>注：环境信息 x86 Linux/Macos</p><p>源码地址：<a href="https://github.com/WangLeonard/go/commits/dev-schedule">https://github.com/WangLeonard/go/commits/dev-schedule</a></p><p>作者认为，调度器其实只需要保证所有的<strong>可运行</strong>的Goroutine<strong>终将被执行</strong>，就足够了。当然也应该考虑调度时延，内存占用，调度效率，低负载时的Cpu使用率等等，但这些并不影响程序或者说协程定义的正确性。</p><p>那么如何实现最低标准的M:N调度模型的协程调度器呢？</p><p>先来看一个简单的例子，该例子中使用go关键字创建了一个Goroutine。</p><p>对于go func的行为预期应该是什么？是否需要等待对应的协程开始运行或是执行完成才返回，或者说是否main函数在调用go func之后是否会阻塞等待其被执行或是执行完成，答案是否定的。从执行结果来看，该程序只有小概率会打印出&quot;Hello Goroutine&quot;，大部分情况下打印&quot;Hello Main&quot;之后便退出了。</p><pre><code class="language-go">package mainimport (&quot;fmt&quot;)func main() {go func() {fmt.Println(&quot;Hello Goroutine&quot;)}()fmt.Println(&quot;Hello Main&quot;)}</code></pre><p>所以这里需要将提交任务和执行任务进行“解耦”，可以将其想象为“生产者-消费者”模型，需要一个“容器”来存储待运行的Goroutine，可以选择一个无界、线程安全的队列来存储提交给调度器的Goroutine。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210810004738by1MCd.png" alt="image-20210810004738091" /></p><p>朴素调度器逻辑伪代码如下</p><pre><code class="language-go">func schedule() {for {gp := globrunqget()if gp != nil {run(gp)}}}</code></pre><h3 id="创建协程">创建协程</h3><p>继续分析go func的行为</p><p><img src="https://imghost.leonard.wang//uPic/image-20210811000139wS4HAH.png" alt="image-20210811000139738" /></p><p>go关键字和其后跟随的func会被转换为对<a href="https://github.com/golang/go/blob/72ab3ff68b1ec894fe5599ec82b8849f3baa9d94/src/runtime/proc.go#L4250">runtime.newproc</a>函数的调用。</p><h3 id="在何时启动多线程">在何时启动多线程</h3><p>继续分析<code>runtime.newproc</code>函数需要做的事情，分为以下几个部分</p><ul><li><p>初始化Goroutine</p></li><li><p>提交Goroutine到任务队列</p></li><li><p>“按需”创建操作系统线程</p><p>调度器需要在一个合适的时间点来创建新的线程，来达到多线程并行调度的目的，在向调度器提交任务时可以作为一个合适的时间点，在<strong>朴素调度器</strong>中由于操作系统线程被创建后会一直从任务队列查询是否有需要运行的Goroutine，不会进入空闲等待的状态，所以有需要时，直接创建新的操作系统线程，线程的入口函数会做一些初始化之后进入调度逻辑寻找Goroutine来执行。</p><p>那么创建线程的数量也应该有一个限制，否则每次提交Goroutine都创建一个新的操作系统线程，就无法达到轻量的目的了，在<strong>朴素调度器</strong>中可以简单的将线程最大数量限制为cpu核心数（最终版本中是根据P的数量进行限制）。所以下图中的“需要创建线程”条件具体为“当前线程数量是否到达了CPU核心数”。</p></li></ul><p><img src="https://imghost.leonard.wang//uPic/image-20210811002033OG3Fiv.png" alt="image-20210811002033348" /></p><h3 id="任务队列">任务队列</h3><p>这里就是一个需要<strong>线程锁</strong>来保证线程安全的、使用<strong>链表</strong>实现的<strong>无界</strong>队列，提供了<code>put</code>和<code>get</code>方法</p><h3 id="协程运行完成如何回到调度器">协程运行完成如何回到调度器</h3><p>调度器找到一个可运行的Goroutine后，切换到它的栈进行运行，那么协程执行结束后如何再回到调度器，使得调度器可以继续执行任务呢？</p><p><strong>简单来说</strong>，实际提交到调度器的任务，是被“包裹”了一层，在新函数中调用要执行的任务，并在后方插入回到调度器的函数。</p><p>比如说<code>go Hello()</code></p><pre><code class="language-go">func Hello() {fmt.Println(&quot;Hello World&quot;)}func main() {go Hello()}</code></pre><p>实际执行的任务其实是：（伪代码）</p><pre><code class="language-go">func goexit(){Hello()  switchToSchedule()  // mcall(schedule)}</code></pre><p>所以在Hello执行完成后会回到goexit函数，并执行后续的<code>switchToSchedule</code>，进而重新回到调度器。</p><p>注：mcall的实现在栈切换中进行详解</p><p><img src="https://imghost.leonard.wang//uPic/image-20210811235156qr4Una.png" alt="image-20210811235156697" /></p><h3 id="栈切换">栈切换</h3><p>Cpu如何认为这是一个栈？</p><blockquote><p>SP、BP寄存器的作用</p><p>SP:堆栈寄存器SP(stack pointer)存放栈的偏移地址;</p><p>BP: 基数指针寄存器BP(base pointer)是一个寄存器，它的用途有点特殊，是和堆栈指针SP联合使用的，作为SP校准使用的，只有在寻找堆栈里的数据和使用个别的寻址方式时候才能用到</p><p>SP用以指示栈顶的偏移地址，**而BP可 作为堆栈区中的一个基地址，用以确定在堆栈中的操作数地址。</p></blockquote><p>按照这个规则，从CPU的角度来说，<strong>SP寄存器</strong>指向的位置就是<strong>栈</strong>空间，我们可以通过汇编指令的方式来直接操作SP物理寄存器，我们可以通过这种方式来“欺骗”CPU将Goroutine的栈空间认为是栈。</p><p>如前方所述，Go的协程是“有栈”的，即每个Goroutine有自己独立的栈空间。</p><p>再来看一个老生常谈的问题，堆和栈的区别</p><blockquote><p><a href="https://www.zhihu.com/question/19729973">https://www.zhihu.com/question/19729973</a></p><p>C++ 中堆和栈的区别</p><p>管理方式：栈由编译器自动管理，无需人为控制。而堆释放工作由程序员控制，容易产生内存泄漏（memory leak）。</p><p>空间大小：在32位系统下，堆内存可以达到4G的空间，但对于栈来说，一般都是有一定的空间大小的（在VC6默认的栈空间大小是1M，也有默认2M的）。</p><p>生长方向：堆生长（扩展）方向是向上的，也就是向着内存地址增加的方向；栈生长（扩展）方向是向下的，是向着内存地址减小的方向增长。</p><p>分配方式：堆都是动态分配的，没有静态分配的堆。而栈有2种分配方式：静态分配和动态分配。静态分配是编译器完成的，如局部变量分配。动态分配由alloca函数进行分配，但是栈的动态分配和堆是不同的，它的动态分配是由编译器进行释放，无需我们手工实现。</p><p>效率：栈是机器系统提供的数据结构，计算机会在底层对栈提供支持（有专门的寄存器存放栈的地址，压栈出栈都有专门的机器指令执行），这就决定了栈的效率比较高。堆则是C/C++函数库提供的，它的机制是很复杂的。例如分配一块内存，堆会按照一定的算法，在堆内存中搜索可用的足够大小的空间，如果没有（可能是由于内存碎片太多），就有可能调用系统功能去增加程序数据段的内存空间，这样就有机会分到足够大小的内存，然后进行返回。总之，堆的效率比栈要低得多。</p></blockquote><p>这些规则在Go中不完全适用，首先，堆和栈其实都是内存，调用sysmap从操作系统申请一块内存在Runtime内部手动管理，这些内存既可以作为堆内存也可以<strong>使其作为栈</strong>。</p><p>Go中的栈分为两种，g0的栈（线程栈），用来运行runtime内部逻辑，一般来说比较大，在没有cgo的情况下，栈空间为8KB（使用clone系统调用，并传入栈空间地址），有cgo的情况下，调用pthread直接创建线程，Linux中默认为8MB？</p><p>栈切换，其实就是在<strong>g0</strong>和不同的<strong>g</strong>栈中互相切换，切换包括保存当前的SP、BP、调用者压栈的reture address 等物理寄存器到g 结构体的sched字段，然后开始恢复要切换到的栈的信息，恢复完成后调用  jmp return address 开始执行。</p><p>由于每个G有独立的栈空间，所以在栈切换的过程中无需单独保存局部变量等在栈上的信息。</p><p>TODO:如何理解g0？</p><p>如何理解栈的分配和释放是由编译器进行管理的？</p><p>这里涉及call指令（push return address 并跳转到 具体地址继续执行）和ret指令（pop return address  并跳回return address ）的实现；另外栈的申请，其实是对SP寄存器的值进行减法操作（栈的生长方式是向下的），释放栈，是对SP寄存器进行加法操作；编译器也会自动保存和恢复BP的值。</p><pre><code class="language-go">func Test() {a := 1b := 2c := a + bfmt.Println(c)}func main() {Test()}</code></pre><p><code>go tool compile -S ./main.go</code></p><pre><code class="language-bash">&quot;&quot;.Test STEXT size=103 args=0x0 locals=0x40 funcid=0x00x0000 00000 (./main.go:9)TEXT&quot;&quot;.Test(SB), ABIInternal, $64-00x0000 00000 (./main.go:9)CMPQSP, 16(R14)0x0004 00004 (./main.go:9)PCDATA$0, $-20x0004 00004 (./main.go:9)JLS950x0006 00006 (./main.go:9)PCDATA$0, $-10x0006 00006 (./main.go:9)SUBQ$64, SP      # 申请栈空间0x000a 00010 (./main.go:9)MOVQBP, 56(SP)   # 在栈上保存当前BP0x000f 00015 (./main.go:9)LEAQ56(SP), BP   # 更新BP0x0014 00020 (./main.go:9)FUNCDATA$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0014 00020 (./main.go:9)FUNCDATA$1, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)0x0014 00020 (./main.go:9)FUNCDATA$2, &quot;&quot;.Test.stkobj(SB)0x0014 00020 (./main.go:14)MOVL$3, AX0x0019 00025 (./main.go:14)PCDATA$1, $00x0019 00025 (./main.go:14)CALLruntime.convT64(SB)0x001e 00030 (./main.go:14)MOVUPSX15, &quot;&quot;..autotmp_13+40(SP)0x0024 00036 (./main.go:14)LEAQtype.int(SB), CX0x002b 00043 (./main.go:14)MOVQCX, &quot;&quot;..autotmp_13+40(SP)0x0030 00048 (./main.go:14)MOVQAX, &quot;&quot;..autotmp_13+48(SP)0x0035 00053 (&lt;unknown line number&gt;)NOP0x0035 00053 ($GOROOT/src/fmt/print.go:274)MOVQos.Stdout(SB), BX0x003c 00060 ($GOROOT/src/fmt/print.go:274)LEAQgo.itab.*os.File,io.Writer(SB), AX0x0043 00067 ($GOROOT/src/fmt/print.go:274)LEAQ&quot;&quot;..autotmp_13+40(SP), CX0x0048 00072 ($GOROOT/src/fmt/print.go:274)MOVL$1, DI0x004d 00077 ($GOROOT/src/fmt/print.go:274)MOVQDI, SI0x0050 00080 ($GOROOT/src/fmt/print.go:274)CALLfmt.Fprintln(SB)0x0055 00085 (./main.go:15)MOVQ56(SP), BP    # 恢复BP0x005a 00090 (./main.go:15)ADDQ$64, SP       # 释放栈空间0x005e 00094 (./main.go:15)RET&quot;&quot;.main STEXT size=40 args=0x0 locals=0x8 funcid=0x00x0000 00000 (./main.go:17)TEXT&quot;&quot;.main(SB), ABIInternal, $8-00x0000 00000 (./main.go:17)CMPQSP, 16(R14)0x0004 00004 (./main.go:17)PCDATA$0, $-20x0004 00004 (./main.go:17)JLS330x0006 00006 (./main.go:17)PCDATA$0, $-10x0006 00006 (./main.go:17)SUBQ$8, SP0x000a 00010 (./main.go:17)MOVQBP, (SP)0x000e 00014 (./main.go:17)LEAQ(SP), BP0x0012 00018 (./main.go:17)FUNCDATA$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0012 00018 (./main.go:17)FUNCDATA$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0012 00018 (./main.go:18)PCDATA$1, $00x0012 00018 (./main.go:18)CALL&quot;&quot;.Test(SB)0x0017 00023 (./main.go:19)MOVQ(SP), BP0x001b 00027 (./main.go:19)ADDQ$8, SP0x001f 00031 (./main.go:19)NOP0x0020 00032 (./main.go:19)RET</code></pre><p>g0 -&gt; g栈的切换</p><p><img src="https://imghost.leonard.wang//uPic/image-20210815230834xUgi1V.png" alt="image-20210815230834251" /></p><p>g-&gt;g0栈的切换</p><p>这里考虑在Goroutine执行结束时的情况</p><p>main.Hello 执行结束时，回到runtime.exit函数中，进而调用mcall(schedule)</p><p><img src="https://imghost.leonard.wang//uPic/image-20210815233337D1Xf3D.png" alt="image-20210815233337010" /></p><p>这里有个细节，为什么切换回g0时的SP指向了mstart，而不是g0-&gt;g切换时的runtime.gogo</p><p>这里要看<code>g0</code>的<code>sched gobuf</code>中存储的<code>SP</code>和<code>PC</code>信息</p><p>创建线程时指定的mstart会调用到mstart1函数，在这个函数中存储，调用mstart1(mstart)时的PC和SP到<code>gobuf</code>中，<code>g-&gt;g0</code>时根据这个信息来切栈后，使用<code>CALL</code>执行调用具体要在<code>g0</code>栈中执行的<code>schedule</code>函数。</p><pre><code class="language-go">func mstart1() {_g_ := getg()if _g_ != _g_.m.g0 {throw(&quot;bad runtime·mstart&quot;)}// Set up m.g0.sched as a label returning to just// after the mstart1 call in mstart0 above, for use by goexit0 and mcall.// We're never coming back to mstart1 after we call schedule,// so other calls can reuse the current frame.// And goexit0 does a gogo that needs to return from mstart1// and let mstart0 exit the thread._g_.sched.g = guintptr(unsafe.Pointer(_g_))_g_.sched.pc = getcallerpc()_g_.sched.sp = getcallersp()// ……}</code></pre><p>实际上在不考虑cgo回调go的场景下时，g0的栈中，在mstart下面不应该再存在任何有价值的栈空间，g0栈只用来临时执行一些栈空间消耗较大的runtime函数。</p><p>这里有两个“新名词”<code>getg</code>和<code>gobuf</code></p><p>对于切栈，有几个被封装过的函数</p><p>systemstack</p><p>mcall</p><p>gogo</p><p>TODO:为什么不需要保存寄存器？</p><h3 id="正确性验证">正确性验证</h3><pre><code class="language-go">package mainimport (&quot;sync/atomic&quot;)var flag = falsevar atomicValue int64const goroutineCount = 100func main() {println(&quot;main&quot;)for i := 0; i &lt; goroutineCount; i++ {go func() {atomic.AddInt64(&amp;atomicValue, 1)}()}go func() {println(&quot;goroitine begin&quot;)flag = trueprintln(&quot;goroitine done&quot;)}()for atomic.LoadInt64(&amp;atomicValue) != goroutineCount || flag != true {// Just like sleep}println(&quot;Done&quot;)println(atomic.LoadInt64(&amp;atomicValue))}</code></pre><p><code>go build</code></p><p><code>GODEBUG=newschedule=1 ./schedule</code></p><p>除去日志打印，该程序终将打印如下信息，并可以正常退出</p><pre><code>Done100</code></pre><h3 id="源码实现详解">源码实现详解</h3><h4 id="创建协程-1">创建协程</h4><pre><code class="language-go">func newproc(siz int32, fn *funcval) {argp := add(unsafe.Pointer(&amp;fn), sys.PtrSize)gp := getg()pc := getcallerpc()systemstack(func() { // 切换到g0栈执行newg := newproc1(fn, argp, siz, gp, pc) // 创建g的数据结构以及构造初始栈if debug.newschedule == 1 {lock(&amp;sched.lock)globrunqput(newg) // 加入全局队列unlock(&amp;sched.lock)}if mainStarted {wakep() // 如果main Goroutine已启动，尝试唤醒新的线程}})}func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g {_g_ := getg()acquirem() // 关闭抢占siz := nargsiz = (siz + 7) &amp;^ 7if newg == nil {newg = malg(_StackMin) // 创建一个G的结构体casgstatus(newg, _Gidle, _Gdead) // G的状态变化  _Gidle -&gt; _Gdeadallgadd(newg) // 添加到全局Goroutine列表}totalSize := 4*sys.PtrSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frametotalSize += -totalSize &amp; (sys.StackAlign - 1)               // align to StackAlignsp := newg.stack.hi - totalSizespArg := spif usesLR {// caller's LR*(*uintptr)(unsafe.Pointer(sp)) = 0prepGoExitFrame(sp)spArg += sys.MinFrameSize}if narg &gt; 0 {// 拷贝参数到Goroutine的栈上memmove(unsafe.Pointer(spArg), argp, uintptr(narg))}memclrNoHeapPointers(unsafe.Pointer(&amp;newg.sched), unsafe.Sizeof(newg.sched))newg.sched.sp = spnewg.stktopsp = sp// 在栈底埋入goexitnewg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same functionnewg.sched.g = guintptr(unsafe.Pointer(newg))gostartcallfn(&amp;newg.sched, fn)newg.gopc = callerpcnewg.ancestors = saveAncestors(callergp)newg.startpc = fn.fn  // G的状态变化  _Gdead -&gt; _Grunnablecasgstatus(newg, _Gdead, _Grunnable)// 为每个Goroutine分配唯一IDnewg.goid = atomic.Xaddint64(&amp;sched.goidCount, 1) - 1releasem(_g_.m) // 开启抢占return newg}</code></pre><h4 id="唤醒线程">唤醒线程</h4><p>准确的说，应该是按需唤醒或创建操作系统线程。</p><pre><code class="language-go">func wakep() {startm(nil, true)}func startm(_p_ *p, spinning bool) {if debug.newschedule == 1 {if atomic.Loadint64(&amp;sched.mCount) &lt; int64(ncpu) &amp;&amp; atomic.Xaddint64(&amp;sched.mCount, 1) &lt; int64(ncpu) {newStartm()}}}//go:nowritebarrierrecfunc newStartm() {mp := acquirem()lock(&amp;sched.lock)nmp := mget() // 获取空闲的Mif nmp == nil {// 如果没有空闲的M 则创建新的 M / 操作系统线程id := mReserveID()unlock(&amp;sched.lock)newm(nil, nil, id)// Ownership transfer of _p_ committed by start in newm.// Preemption is now safe.releasem(mp)return}unlock(&amp;sched.lock)notewakeup(&amp;nmp.park)// Ownership transfer of _p_ committed by wakeup. Preemption is now// safe.releasem(mp)}</code></pre><p>newm会进一步调用newm1、newosproc</p><pre><code class="language-go">func newosproc(mp *m) {stk := unsafe.Pointer(mp.g0.stack.hi)// Disable signals during clone, so that the new thread starts// with signals disabled. It will enable them in minit.var oset sigsetsigprocmask(_SIG_SETMASK, &amp;sigset_all, &amp;oset)// 调用clone系统调用创建线程，并指定线程入口函数为mstartret := clone(cloneFlags, stk, unsafe.Pointer(mp), unsafe.Pointer(mp.g0), unsafe.Pointer(funcPC(mstart)))sigprocmask(_SIG_SETMASK, &amp;oset, nil)if ret &lt; 0 {print(&quot;runtime: failed to create new OS thread (have &quot;, mcount(), &quot; already; errno=&quot;, -ret, &quot;)\n&quot;)if ret == -_EAGAIN {println(&quot;runtime: may need to increase max user processes (ulimit -u)&quot;)}throw(&quot;newosproc&quot;)}}</code></pre><h4 id="进入调度逻辑">进入调度逻辑</h4><p>上方已指定线程入口函数为mstart，所以在新线程启动时，会执行mstart</p><p>该函数使用汇编实现，直接调用到mstart0，进一步调用到mstart1</p><pre><code class="language-go">TEXT runtime·mstart(SB),NOSPLIT|TOPFRAME,$0   CALL   runtime·mstart0(SB)   RET // not reached</code></pre><pre><code class="language-go">func mstart1() {_g_ := getg()if _g_ != _g_.m.g0 {throw(&quot;bad runtime·mstart&quot;)}// Set up m.g0.sched as a label returning to just// after the mstart1 call in mstart0 above, for use by goexit0 and mcall.// We're never coming back to mstart1 after we call schedule,// so other calls can reuse the current frame.// And goexit0 does a gogo that needs to return from mstart1// and let mstart0 exit the thread._g_.sched.g = guintptr(unsafe.Pointer(_g_))// 初始化g0的原始栈信息，g-&gt;g0时切换到这里保存的PC和SP_g_.sched.pc = getcallerpc()_g_.sched.sp = getcallersp()asminit()minit()// 如果调用newm时的第一个入参不为nil，在这里会执行这个入参所指定的函数// Tips: sysmon的实现即使在newm时传入了一个永远不会退出的函数，会一直在这个位置运行，而不会进入到下方的schedule，进而陷入调度循环。if fn := _g_.m.mstartfn; fn != nil {fn()}// 进入调度循环schedule()}</code></pre><h4 id="寻找任务执行">寻找任务执行</h4><pre><code class="language-go">func schedule() {  _g_ := getg()  println(&quot;enter newschedule,m id:&quot;, _g_.m.id)  if _g_.m.p != 0 {    println(&quot;m have p&quot;)  }  var gp *g  for {    // 加锁从全局队列寻找Goroutine    lock(&amp;sched.lock)    gp = globrunqget(_g_.m.p.ptr(), 1)    unlock(&amp;sched.lock)    if gp == nil {      // 没有可运行Goroutine时，挂起      newStopm()    } else {      break    }  }  // 运行寻找到的Goroutine  println(&quot;go to execute goroutine&quot;, gp.goid)  newEexecute(gp)}//go:yeswritebarrierrecfunc newEexecute(gp *g) {_g_ := getg()// Assign gp.m before entering _Grunning so running Gs have an// M._g_.m.curg = gpgp.m = _g_.m// G的状态变化  _Grunnable -&gt; _Grunningcasgstatus(gp, _Grunnable, _Grunning)gp.stackguard0 = gp.stack.lo + _StackGuard// 切栈g0-&gt;ggogo(&amp;gp.sched)}</code></pre><h1 id="后续计划">后续计划</h1><h2 id="gm---gmp">GM -&gt; GMP</h2><h3 id="控制程序并发度">控制程序并发度</h3><h3 id="减小锁冲突">减小锁冲突</h3><p>LocalRunQueue</p><h2 id="减小调度时延">减小调度时延</h2><h3 id="任务窃取">任务窃取</h3><h3 id="抢占调度">抢占调度</h3><p>协作式抢占 信号抢占</p><h3 id="自旋">自旋</h3><h3 id="防饿死">防饿死</h3><h2 id="栈溢出检查">栈溢出检查</h2><h2 id="优先级调度">优先级调度</h2><h2 id="支持gc">支持GC</h2><h2 id="支持cgo">支持Cgo</h2><h2 id="支持lockosthread">支持LockOSThread</h2><h2 id="支持timer">支持Timer</h2><h2 id="支持epoll">支持epoll</h2><h2 id="支持cpuprofile">支持cpuprofile</h2><h2 id="支持trace">支持trace</h2>]]>
                    </description>
                    <pubDate>Wed, 11 Aug 2021 01:01:09 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[STW实现原理]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/stw</link>
                    <description>
                            <![CDATA[<h1 id="stw实现原理">STW实现原理</h1><p>STW(Stop The World)是Go Runtime内部的一个方法，它的作用如它的名字所言，停止世界，在stopTheWorld调用返回后，<strong>有且仅有</strong>调用该方法的goroutine可以运行。</p><h2 id="用途">用途</h2><p>主要在GC（垃圾回收）和<code>runtime.GOMAXPROCS()</code>中使用</p><p>当然你可以可以通过<code>//go:linkname</code> 的方式来使用，进行些hack操作</p><h2 id="实现方式">实现方式</h2><h3 id="go调度模型">Go调度模型</h3><p>Go的调度模型分为G  M  P 三部分</p><p>这里简单说下，详细讲解Go调度模型的文章有很多，读者有兴趣可以学习下。</p><p>注：本文需要对Go调度器流程和实现有些基本的了解</p><p>G： Goroutine，使用go 关键字创建的协程，代表要执行的任务，在Go中，main函数也是一个goroutine。</p><p>M：Machine，对应操作系统线程，提供cpu执行资源，M的目的是获取一个P，并找到一个可运行的G去执行。</p><p>P：Processor，中间层，包含Goroutine执行所需的资源，比如说内存和待执行的Goroutine等，也可以用来控制程序并发度，<strong>M可以运行G的前提是获得到一个P</strong>。</p><h3 id="分析">分析</h3><p>那么如果让你来实现Stop The World功能，应该对调度器中的哪个部分进行干预呢？</p><p>首先再进一步明确一下，Stop The World的目的，程序中所有goroutine（除去调用方）停止运行即可。</p><p>另外Stop The World还有一个前置依赖功能，抢占调度，需要依赖于这个功能来让Goroutine可以在一个合适的安全点停止运行。对于协作式抢占和信号抢占的调度方式其实不care。</p><ul><li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" readonly="readonly" />&nbsp;G：抢占所有运行的G，并且禁止新创建和唤醒的G运行，听起来可行，但是需要修改限制的地方较多，而且要关注的G的数量可能较多。</li><li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" readonly="readonly" />&nbsp;M：抢占正在运行的G后，停止M，使其不再执行新的G，看起开可行，但是Go中还有跨语言调用，在跨C调用时，由于没有抢占点（信号抢占中对于这种情况也决定不进行抢占），所以没有办法抢占所有的M。</li><li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" checked="checked" disabled="disabled" readonly="readonly" />&nbsp;P：抢占所有运行的P，对于idel闲置状态的P，保证其不再被M获取到，跨C调用的M是不持有P的（其实是可以认为不持有的，可能会保存之前的P，但是超时会被抢占，而且跨C调用结束回来时，也只是尝试获取之前的P）</li></ul><h3 id="源码解析">源码解析</h3><p>Go是选择了抢占P的方案，直接来看源码</p><pre><code class="language-go">// stopTheWorld stops all P's from executing goroutines, interrupting// all goroutines at GC safe points and records reason as the reason// for the stop. On return, only the current goroutine's P is running.// stopTheWorld must not be called from a system stack and the caller// must not hold worldsema. The caller must call startTheWorld when// other P's should resume execution.//// stopTheWorld is safe for multiple goroutines to call at the// same time. Each will execute its own stop, and the stops will// be serialized.//// This is also used by routines that do stack dumps. If the system is// in panic or being exited, this may not reliably stop all// goroutines.func stopTheWorld(reason string) {semacquire(&amp;worldsema)gp := getg()gp.m.preemptoff = reason// 切换到g0系统栈上执行该函数systemstack(func() {// 先切换调用方goroutine到_Gwaiting状态casgstatus(gp, _Grunning, _Gwaiting)// stopTheWorldstopTheWorldWithSema()// 切换调用方goroutine到_Grunning状态，返回，此时 World 已经Stopcasgstatus(gp, _Gwaiting, _Grunning)})}</code></pre><pre><code class="language-go">// stopTheWorldWithSema是stopTheWorld的核心实现.// The caller is responsible for acquiring worldsema and disabling// preemption first and then should stopTheWorldWithSema on the system// stack:////semacquire(&amp;worldsema, 0)//m.preemptoff = &quot;reason&quot;//systemstack(stopTheWorldWithSema)//// When finished, the caller must either call startTheWorld or undo// these three operations separately:////m.preemptoff = &quot;&quot;//systemstack(startTheWorldWithSema)//semrelease(&amp;worldsema)//// It is allowed to acquire worldsema once and then execute multiple// startTheWorldWithSema/stopTheWorldWithSema pairs.// Other P's are able to execute between successive calls to// startTheWorldWithSema and stopTheWorldWithSema.// Holding worldsema causes any other goroutines invoking// stopTheWorld to block.func stopTheWorldWithSema() {_g_ := getg() // 获取当前g，这个已经变为g0lock(&amp;sched.lock)sched.stopwait = gomaxprocs // P的总数量(需要停止的P的数量)atomic.Store(&amp;sched.gcwaiting, 1) // 设置sched.gcwaiting为1preemptall() // 对所有处于运行状态的P执行抢占操作// 停止当前的P_g_.m.p.ptr().status = _Pgcstop // Pgcstop is only diagnostic.sched.stopwait--// 尝试抢占处于syscall状态的P，其实仅修改了P的状态for _, p := range allp {s := p.statusif s == _Psyscall &amp;&amp; atomic.Cas(&amp;p.status, s, _Pgcstop) {if trace.enabled {traceGoSysBlock(p)traceProcStop(p)}p.syscalltick++sched.stopwait-- // 如果状态修改成功，计数减一}}// 处于闲置的P，由于闲置的P没有在运行，仅需从闲置列表中取出，避免其他M获取到即可，并设置对应状态，计数减一for {p := pidleget()if p == nil {break}p.status = _Pgcstopsched.stopwait--}wait := sched.stopwait &gt; 0 // 是有还有需要等待停止的Punlock(&amp;sched.lock)// wait for remaining P's to stop voluntarily// 等待剩余的P停止（上方preemptall已经尝试抢占所有的运行的P）if wait {for {// wait for 100us, then try to re-preempt in case of any races// 等待，底层为futex系统调用，信号量机制，超时时间为100us// 如果是因为超时而唤醒，则返回false// 如果是因为其他线程调用notetwakeup而唤醒，则返回trueif notetsleep(&amp;sched.stopnote, 100*1000) {// 如果因为notetwakeup而唤醒，清除该标志，并退出for循环noteclear(&amp;sched.stopnote)break}preemptall()}}// 正常情况下，这里应该没有需要等待被停止的P了，再检查一下bad := &quot;&quot;if sched.stopwait != 0 {bad = &quot;stopTheWorld: not stopped (stopwait != 0)&quot;} else {for _, p := range allp {if p.status != _Pgcstop {bad = &quot;stopTheWorld: not stopped (status != _Pgcstop)&quot;}}}if atomic.Load(&amp;freezing) != 0 {// Some other thread is panicking. This can cause the// sanity checks above to fail if the panic happens in// the signal handler on a stopped thread. Either way,// we should halt this thread.lock(&amp;deadlock)lock(&amp;deadlock)}if bad != &quot;&quot; {throw(bad)}worldStopped()}</code></pre><p>如何停止其他P，或者说其他P是如何停止的</p><p>G处理抢占信号后会将自身状态切换为<code>_Grunnable</code>，放入<code>global run queue</code>，并重新调用<code>schedule()</code></p><p>记住这个<code>schedule()</code></p><pre><code class="language-go">// Tell all goroutines that they have been preempted and they should stop.// This function is purely best-effort. It can fail to inform a goroutine if a// processor just started running it.// No locks need to be held.// Returns true if preemption request was issued to at least one goroutine.func preemptall() bool {res := false// 遍历所有的Pfor _, _p_ := range allp {// 只关心处于_Prunning状态的Pif _p_.status != _Prunning {continue}// 对每个处于_Prunning状态的P调用 preemptoneif preemptone(_p_) {res = true}}return res}// 抢占入参指定的P，这里分为协作式抢占和信号抢占// 协作式抢占是设置G的preempt和stackguard0字段值，等待函数调用时在函数头插入的more stack检查，停止运行的G，切换G的状态为_Grunnable，放入global run queue，并重新调用schedule// 信号抢占，是向当前P锁绑定的线程 M 发送信号，使得线程触发信号处理函数，在满足安全点的前提下响应该抢占信号，停止运行的G// 信号处理函数响应后，会在该G的调用栈中强行插入asyncPreempt，然后return// 等到G继续运行时，会先运行asyncPreempt，进而执行asyncPreempt2-&gt;g0-&gt;gopreempt_m-&gt;goschedImpl-&gt;切换G的状态为_Grunnable，放入global run queue，并重新调用schedulefunc preemptone(_p_ *p) bool {mp := _p_.m.ptr()if mp == nil || mp == getg().m {return false}gp := mp.curgif gp == nil || gp == mp.g0 {return false}gp.preempt = true// Every call in a goroutine checks for stack overflow by// comparing the current stack pointer to gp-&gt;stackguard0.// Setting gp-&gt;stackguard0 to StackPreempt folds// preemption into the normal stack overflow check.gp.stackguard0 = stackPreempt// Request an async preemption of this P.if preemptMSupported &amp;&amp; debug.asyncpreemptoff == 0 {_p_.preempt = truepreemptM(mp)}return true}</code></pre><p>谁会调用<code>notewakeup</code> ？</p><pre><code class="language-go">// Stops the current m for stopTheWorld.// Returns when the world is restarted.func gcstopm() {_g_ := getg()if sched.gcwaiting == 0 {throw(&quot;gcstopm: not waiting for gc&quot;)}if _g_.m.spinning {_g_.m.spinning = false// OK to just drop nmspinning here,// startTheWorld will unpark threads as necessary.if int32(atomic.Xadd(&amp;sched.nmspinning, -1)) &lt; 0 {throw(&quot;gcstopm: negative nmspinning&quot;)}}_p_ := releasep()lock(&amp;sched.lock)_p_.status = _Pgcstopsched.stopwait--if sched.stopwait == 0 {// 当所有需要停止的 P 已经被停止时，调用 notewakeup 唤醒等待该信号量的Gnotewakeup(&amp;sched.stopnote)}unlock(&amp;sched.lock)stopm()}</code></pre><p>进一步 <code>stopm</code>会做什么？</p><pre><code class="language-go">// Stops execution of the current m until new work is available.// Returns with acquired P.func stopm() {_g_ := getg()lock(&amp;sched.lock)mput(_g_.m) // 将当前 M 放入idel空闲列表unlock(&amp;sched.lock)mPark()     // 会在这里睡眠等待被唤醒acquirep(_g_.m.nextp.ptr()) // 被唤醒后从_g_.m.nextp重新获得P_g_.m.nextp = 0}func mPark() {g := getg()for {notesleep(&amp;g.m.park) // 调用futex进行睡眠，在g.m.park信号量上等待，被唤醒后返回// Note, because of signal handling by this parked m,// a preemptive mDoFixup() may actually occur via// mDoFixupAndOSYield(). (See golang.org/issue/44193)noteclear(&amp;g.m.park)if !mDoFixup() {return}}}</code></pre><p>M如何被唤醒在番外篇中进行讲述</p><p>进一步 谁会调用<code>gcstopm</code>？</p><p><code>schedule</code>的<code>Top</code>和<code>findrunnable</code>函数中</p><p>在调度循环中，会检查当前是否处于STW的过程(<code>sched.gcwaiting != 0</code>)，如果是的话，则会调用<code>gcstopm</code>。</p><pre><code class="language-go">// One round of scheduler: find a runnable goroutine and execute it.// Never returns.func schedule() {_g_ := getg()if _g_.m.locks != 0 {throw(&quot;schedule: holding locks&quot;)}if _g_.m.lockedg != 0 {stoplockedm()execute(_g_.m.lockedg.ptr(), false) // Never returns.}// We should not schedule away from a g that is executing a cgo call,// since the cgo call is using the m's g0 stack.if _g_.m.incgo {throw(&quot;schedule: in cgo&quot;)}top:pp := _g_.m.p.ptr()pp.preempt = falseif sched.gcwaiting != 0 {gcstopm()goto top}if pp.runSafePointFn != 0 {runSafePointFn()}// Sanity check: if we are spinning, the run queue should be empty.// Check this before calling checkTimers, as that might call// goready to put a ready goroutine on the local run queue.if _g_.m.spinning &amp;&amp; (pp.runnext != 0 || pp.runqhead != pp.runqtail) {throw(&quot;schedule: spinning with local work&quot;)}checkTimers(pp, 0)var gp *gvar inheritTime bool// Normal goroutines will check for need to wakeP in ready,// but GCworkers and tracereaders will not, so the check must// be done here instead.tryWakeP := falseif trace.enabled || trace.shutdown {gp = traceReader()if gp != nil {casgstatus(gp, _Gwaiting, _Grunnable)traceGoUnpark(gp, 0)tryWakeP = true}}if gp == nil &amp;&amp; gcBlackenEnabled != 0 {gp = gcController.findRunnableGCWorker(_g_.m.p.ptr())if gp != nil {tryWakeP = true}}if gp == nil {// Check the global runnable queue once in a while to ensure fairness.// Otherwise two goroutines can completely occupy the local runqueue// by constantly respawning each other.if _g_.m.p.ptr().schedtick%61 == 0 &amp;&amp; sched.runqsize &gt; 0 {lock(&amp;sched.lock)gp = globrunqget(_g_.m.p.ptr(), 1)unlock(&amp;sched.lock)}}if gp == nil {gp, inheritTime = runqget(_g_.m.p.ptr())// We can see gp != nil here even if the M is spinning,// if checkTimers added a local goroutine via goready.}if gp == nil {gp, inheritTime = findrunnable() // blocks until work is available}</code></pre><p>至此所有的信息都已经完美拼接上了。</p><h2 id="番外篇">番外篇</h2><p>如何开启世界（Start The Workd）</p><p>注：这里只关注GOMAXPROCS没有发生变更的情况，GOMAXPROCS的实现原理后续再详细分析。</p><pre><code class="language-go">// startTheWorld undoes the effects of stopTheWorld.func startTheWorld() {systemstack(func() { startTheWorldWithSema(false) })// worldsema must be held over startTheWorldWithSema to ensure// gomaxprocs cannot change while worldsema is held.//// Release worldsema with direct handoff to the next waiter, but// acquirem so that semrelease1 doesn't try to yield our time.//// Otherwise if e.g. ReadMemStats is being called in a loop,// it might stomp on other attempts to stop the world, such as// for starting or ending GC. The operation this blocks is// so heavy-weight that we should just try to be as fair as// possible here.//// We don't want to just allow us to get preempted between now// and releasing the semaphore because then we keep everyone// (including, for example, GCs) waiting longer.mp := acquirem()mp.preemptoff = &quot;&quot;semrelease1(&amp;worldsema, true, 0)releasem(mp)}</code></pre><p>核心实现</p><p>先假设proc没有发生变更</p><pre><code class="language-go">func startTheWorldWithSema(emitTraceEvent bool) int64 {assertWorldStopped()mp := acquirem() // disable preemption because it can be holding p in a local varif netpollinited() { // 检查netpoll中是否有已完成的任务list := netpoll(0) // non-blockinginjectglist(&amp;list)}lock(&amp;sched.lock)procs := gomaxprocsp1 := procresize(procs) // 获取所有需要继续运行的Psched.gcwaiting = 0 // 清空 sched.gcwaiting 标志位if sched.sysmonwait != 0 { // 唤醒sysmonsched.sysmonwait = 0notewakeup(&amp;sched.sysmonnote)}unlock(&amp;sched.lock)worldStarted()// 遍历可运行的P链表for p1 != nil {p := p1p1 = p1.link.ptr()if p.m != 0 { // 如果有已绑定的M，直接唤醒该Mmp := p.m.ptr()p.m = 0if mp.nextp != 0 {throw(&quot;startTheWorld: inconsistent mp-&gt;nextp&quot;)}mp.nextp.set(p) // 唤醒之前放置到m的nextp字段中notewakeup(&amp;mp.park) // 唤醒m，m在mPark()函数中等待} else {// 如果没有可用的M，创建一个新的M来绑定这个P// Start M to run P.  Do not start another M below.newm(nil, p, -1)}}// Capture start-the-world time before doing clean-up tasks.startTime := nanotime()if emitTraceEvent {traceGCSTWDone()}// Wakeup an additional proc in case we have excessive runnable goroutines// in local queues or in the global queue. If we don't, the proc will park itself.// If we have lots of excessive work, resetspinning will unpark additional procs as necessary.// 唤醒一个额外的P，以避免任务不均匀的分布在本地队列或全局队列wakep()releasem(mp)return startTime}</code></pre><pre><code class="language-go">func procresize(nprocs int32) *p {assertLockHeld(&amp;sched.lock)assertWorldStopped()old := gomaxprocs// update statisticsnow := nanotime()if sched.procresizetime != 0 {sched.totaltime += int64(old) * (now - sched.procresizetime)}sched.procresizetime = nowmaskWords := (nprocs + 31) / 32_g_ := getg()if _g_.m.p != 0 &amp;&amp; _g_.m.p.ptr().id &lt; nprocs {// continue to use the current P// 处理当前的P_g_.m.p.ptr().status = _Prunning_g_.m.p.ptr().mcache.prepareForSweep()}var runnablePs *p// 处理剩余的Pfor i := nprocs - 1; i &gt;= 0; i-- {p := allp[i]if _g_.m.p.ptr() == p {continue}// 设置P的状态 -&gt; _Pidlep.status = _Pidleif runqempty(p) {// 如果P的runque中没有待运行的任务，放置到idle队列中pidleput(p)} else {// P需要继续运行，放入到runnablePs链表中p.m.set(mget()) // 寻找一个可用的M与其绑定p.link.set(runnablePs)runnablePs = p}}stealOrder.reset(uint32(nprocs))var int32p *int32 = &amp;gomaxprocs // make compiler check that gomaxprocs is an int32atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))// 返回待运行链表return runnablePs}</code></pre><h2 id="总结">总结</h2><p>Stop The World会先计算所有P的数量，然后尝试停止所有的P</p><p>P的状态分为三种情况</p><ul><li>idle：从idle列表移除，避免其他M获取P</li><li>running：发出抢占信号</li><li>syscsll：切换状态，避免syscall结束时再次获取到P</li></ul><p>其中每停止成功一个P，会将需要等待的数量减一</p><p>当前Goroutine睡眠等待所有P结束</p><p>对于被抢占的P，被停止后也会重新计算需要等待的数量，当数量减为0后，唤醒睡眠等待的Goroutine</p><p>当睡眠的Goroutine被唤醒时，World已经完成Stop，函数返回。</p><p>另外这篇文章埋了三个坑，<code>go:linkname</code>(第二次埋坑)、<code>syscall</code>、<code>runtime.GOMAXPROCS()</code>有机会来填。</p>]]>
                    </description>
                    <pubDate>Thu, 22 Jul 2021 23:23:48 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[[转] 虚拟内存]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/virtualmemory</link>
                    <description>
                            <![CDATA[<p><a href="https://mp.weixin.qq.com/s?__biz=MzkwNTIwMjIwMg==&amp;mid=2247484936&amp;idx=1&amp;sn=6b995ba375374fde498bfa7b654c141a&amp;chksm=c0fa197df78d906b5bbf5dcb7637b319a65d0e040b62807cbe64bb330f6ba16106aabd6faa21&amp;mpshare=1&amp;scene=1&amp;srcid=06066TKIcw6b2te3251WPBE8&amp;sharer_sharetime=1622989843730&amp;sharer_shareid=278eeeabb74d9c6a29ae4f4e5e1e32f6#rd">原文地址</a></p><ul><li><p>导言</p></li><li><p>计算机存储器</p></li><li><p>主存</p></li><li><ul><li>物理内存</li><li>虚拟内存</li></ul></li><li><p>虚拟内存</p></li><li><ul><li>页表</li><li>地址翻译</li><li>虚拟内存和高速缓存</li><li>加速翻译&amp;优化页表</li></ul></li><li><p>总结</p></li><li><p>参考&amp;延伸阅读</p></li></ul><h2 id="导言">导言</h2><p>虚拟内存是当今计算机系统中最重要的抽象概念之一，它的提出是为了更加有效地管理内存并且降低内存出错的概率。虚拟内存影响着计算机的方方面面，包括硬件设计、文件系统、共享对象和进程/线程调度等等，每一个致力于编写高效且出错概率低的程序的程序员都应该深入学习虚拟内存。</p><p>本文全面而深入地剖析了虚拟内存的工作原理，帮助读者快速而深刻地理解这个重要的概念。</p><h2 id="计算机存储器">计算机存储器</h2><p>存储器是计算机的核心部件之一，在完全理想的状态下，存储器应该要同时具备以下三种特性：</p><ol><li>速度足够快：存储器的存取速度应当快于 CPU 执行一条指令，这样 CPU 的效率才不会受限于存储器</li><li>容量足够大：容量能够存储计算机所需的全部数据</li><li>价格足够便宜：价格低廉，所有类型的计算机都能配备</li></ol><p>但是现实往往是残酷的，我们目前的计算机技术无法同时满足上述的三个条件，于是现代计算机的存储器设计采用了一种分层次的结构：</p><p><img src="https://imghost.leonard.wang//uPic/image-20210606223200mXUlkv.png" alt="图片" /></p><p>从顶至底，现代计算机里的存储器类型分别有：寄存器、高速缓存、主存和磁盘，这些存储器的速度逐级递减而容量逐级递增。存取速度最快的是寄存器，因为寄存器的制作材料和 CPU 是相同的，所以速度和 CPU 一样快，CPU 访问寄存器是没有时延的，然而因为价格昂贵，因此容量也极小，一般 32 位的 CPU 配备的寄存器容量是 32✖️32 Bit，64 位的 CPU 则是 64✖️64 Bit，不管是 32 位还是 64 位，寄存器容量都小于 1 KB，且寄存器也必须通过软件自行管理。</p><p>第二层是高速缓存，也即我们平时了解的 CPU 高速缓存 L1、L2、L3，一般 L1 是每个 CPU 独享，L3 是全部 CPU 共享，而 L2 则根据不同的架构设计会被设计成独享或者共享两种模式之一，比如 Intel 的多核芯片采用的是共享 L2 模式而 AMD 的多核芯片则采用的是独享 L2 模式。</p><p>第三层则是主存，也即主内存，通常称作随机访问存储器（Random Access Memory, RAM）。是与 CPU 直接交换数据的内部存储器。它可以随时读写（刷新时除外），而且速度很快，通常作为操作系统或其他正在运行中的程序的临时资料存储介质。</p><p>最后则是磁盘，磁盘和主存相比，每个二进制位的成本低了两个数量级，因此容量比之会大得多，动辄上 GB、TB，而缺点则是访问速度则比主存慢了大概三个数量级。机械硬盘速度慢主要是因为机械臂需要不断在金属盘片之间移动，等待磁盘扇区旋转至磁头之下，然后才能进行读写操作，因此效率很低。</p><h2 id="主存">主存</h2><h3 id="物理内存">物理内存</h3><p>我们平时一直提及的物理内存就是上文中对应的第三种计算机存储器，RAM 主存，它在计算机中以内存条的形式存在，嵌在主板的内存槽上，用来加载各式各样的程序与数据以供 CPU 直接运行和使用。</p><h3 id="虚拟内存">虚拟内存</h3><p>在计算机领域有一句如同摩西十诫般神圣的哲言：&quot;<strong>计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决</strong>&quot;，从内存管理、网络模型、并发调度甚至是硬件架构，都能看到这句哲言在闪烁着光芒，而虚拟内存则是这一哲言的完美实践之一。</p><p>虚拟内存是现代计算机中的一个非常重要的存储器抽象，主要是用来解决应用程序日益增长的内存使用需求：现代物理内存的容量增长已经非常快速了，然而还是跟不上应用程序对主存需求的增长速度，对于应用程序来说内存还是可能会不够用，因此便需要一种方法来解决这两者之间的容量差矛盾。为了更高效地管理内存并尽可能消除程序错误，现代计算机系统对物理主存 RAM 进行抽象，实现了***虚拟内存 (Virtual Memory, VM)*** 技术。</p><h2 id="虚拟内存-1">虚拟内存</h2><p>虚拟内存的核心原理是：为每个程序设置一段&quot;连续&quot;的虚拟地址空间，把这个地址空间分割成多个具有连续地址范围的页 (Page)，并把这些页和物理内存做映射，在程序运行期间动态映射到物理内存。当程序引用到一段在物理内存的地址空间时，由硬件立刻执行必要的映射；而当程序引用到一段不在物理内存中的地址空间时，由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。</p><p>其实虚拟内存技术从某种角度来看的话，很像是糅合了基址寄存器和界限寄存器之后的新技术。它使得整个进程的地址空间可以通过较小的虚拟单元映射到物理内存，而不需要为程序的代码和数据地址进行重定位。</p><p><img src="https://imghost.leonard.wang//uPic/image-202106062246430o1etF.png" alt="图片" /></p><p>虚拟地址空间按照固定大小划分成被称为页（Page）的若干单元，物理内存中对应的则是页框（Page Frame）。这两者一般来说是一样的大小，如上图中的是 4KB，不过实际上计算机系统中一般是 512 字节到 1 GB，这就是虚拟内存的分页技术。因为是虚拟内存空间，每个进程分配的大小是 4GB (32 位架构)，而实际上当然不可能给所有在运行中的进程都分配 4GB 的物理内存，所以虚拟内存技术还需要利用到一种 <code>交换（swapping）</code>技术，也就是通常所说的页面置换算法，在进程运行期间只分配映射当前使用到的内存，暂时不使用的数据则写回磁盘作为副本保存，需要用的时候再读入内存，动态地在磁盘和内存之间交换数据。</p><h3 id="页表">页表</h3><p>页表（Page Table），每次进行虚拟地址到物理地址的映射之时，都需要读取页表，从数学角度来说页表就是一个函数，入参是虚拟页号（Virtual Page Number，简称 VPN），输出是物理页框号（Physical Page Number，简称 PPN，也就是物理地址的基址）。</p><p>页表由多个页表项（Page Table Entry, 简称 PTE）组成，页表项的结构取决于机器架构，不过基本上都大同小异。一般来说页表项中都会存储物理页框号、修改位、访问位、保护位和 &quot;在/不在&quot; 位（有效位）等信息。</p><ul><li>物理页框号：这是 PTE 中最重要的域值，毕竟页表存在的意义就是提供 VPN 到 PPN 的映射。</li><li>有效位：表示该页面当前是否存在于主存中，1 表示存在，0 表示缺失，当进程尝试访问一个有效位为 0 的页面时，就会引起一个缺页中断。</li><li>保护位：指示该页面所允许的访问类型，比如 0 表示可读写，1 表示只读。</li><li>修改位和访问位：为了记录页面使用情况而引入的，一般是页面置换算法会使用到。比如当一个内存页面被程序修改过之后，硬件会自动设置修改位，如果下次程序发生缺页中断需要运行页面置换算法把该页面调出以便为即将调入的页面腾出空间之时，就会先去访问修改位，从而得知该页面被修改过，也就是脏页 (Dirty Page)，则需要把最新的页面内容写回到磁盘保存，否则就表示内存和磁盘上的副本内容是同步的，无需写回磁盘；而访问位同样也是系统在程序访问页面时自动设置的，它也是页面置换算法会使用到的一个值，系统会根据页面是否正在被访问来觉得是否要淘汰掉这个页面，一般来说不再使用的页面更适合被淘汰掉。</li><li>高速缓存禁止位：用于禁止页面被放入 CPU 高速缓存，这个值主要适用于那些映射到寄存器等实时 I/O 设备而非普通主存的内存页面，这一类实时 I/O 设备需要拿到最新的数据，而 CPU 高速缓存中的数据可能是旧的拷贝。</li></ul><p><img src="https://imghost.leonard.wang//uPic/image-20210606224702p5rEea.png" alt="图片" /></p><h3 id="地址翻译">地址翻译</h3><p>进程在运行期间产生的内存地址都是虚拟地址，如果计算机没有引入虚拟内存这种存储器抽象技术的话，则 CPU 会把这些地址直接发送到内存地址总线上，然后访问和虚拟地址相同值的物理地址；如果使用虚拟内存技术的话，CPU 则是把这些虚拟地址通过地址总线送到内存管理单元（Memory Management Unit，简称 MMU），MMU 将虚拟地址翻译成物理地址之后再通过内存总线去访问物理内存：</p><p><img src="https://imghost.leonard.wang//uPic/image-20210606224716rQdPwT.png" alt="图片" /></p><p>虚拟地址（比如 16 位地址 8196=0010 000000000100）分为两部分：虚拟页号（Virtual Page Number，简称 VPN，这里是高 4 位部分）和偏移量（Virtual Page Offset，简称 VPO，这里是低 12 位部分），虚拟地址转换成物理地址是通过页表（page table）来实现的。</p><p>这里我们基于一个例子来分析当页面命中时，计算机各个硬件是如何交互的：</p><p><img src="https://imghost.leonard.wang//uPic/image-20210606224728Of4bNb.png" alt="图片" /></p><ul><li><strong>第 1 步</strong>：处理器生成一个虚拟地址 VA，通过总线发送到 MMU；</li><li><strong>第 2 步</strong>：MMU 通过虚拟页号得到页表项的地址 PTEA，通过内存总线从 CPU 高速缓存/主存读取这个页表项 PTE；</li><li><strong>第 3 步</strong>：CPU 高速缓存或者主存通过内存总线向 MMU 返回页表项 PTE；</li><li><strong>第 4 步</strong>：MMU 先把页表项中的物理页框号 PPN 复制到寄存器的高三位中，接着把 12 位的偏移量 VPO 复制到寄存器的末 12 位构成 15 位的物理地址，即可以把该寄存器存储的物理内存地址 PA 发送到内存总线，访问高速缓存/主存；</li><li><strong>第 5 步</strong>：CPU 高速缓存/主存返回该物理地址对应的数据给处理器。</li></ul><p><img src="https://imghost.leonard.wang//uPic/image-20210606224747JYbdB4.png" alt="图片" /></p><p>在 MMU 进行地址转换时，如果页表项的有效位是 0，则表示该页面并没有映射到真实的物理页框号 PPN，则会引发一个<strong>缺页中断</strong>，CPU 陷入操作系统内核，接着操作系统就会通过页面置换算法选择一个页面将其换出 (swap)，以便为即将调入的新页面腾出位置，如果要换出的页面的页表项里的修改位已经被设置过，也就是被更新过，则这是一个脏页 (Dirty Page)，需要写回磁盘更新该页面在磁盘上的副本，如果该页面是&quot;干净&quot;的，也就是没有被修改过，则直接用调入的新页面覆盖掉被换出的旧页面即可。</p><p>缺页中断的具体流程如下：</p><ul><li><strong>第 1 步到第 3 步</strong>：和前面的页面命中的前 3 步是一致的；</li><li><strong>第 4 步</strong>：检查返回的页表项 PTE 发现其有效位是 0，则 MMU 触发一次缺页中断异常，然后 CPU 转入到操作系统内核中的缺页中断处理器；</li><li><strong>第 5 步</strong>：缺页中断处理程序检查所需的虚拟地址是否合法，确认合法后系统则检查是否有空闲物理页框号 PPN 可以映射给该缺失的虚拟页面，如果没有空闲页框，则执行页面置换算法寻找一个现有的虚拟页面淘汰，如果该页面已经被修改过，则写回磁盘，更新该页面在磁盘上的副本；</li><li><strong>第 6 步</strong>：缺页中断处理程序从磁盘调入新的页面到内存，更新页表项 PTE；</li><li><strong>第 7 步</strong>：缺页中断程序返回到原先的进程，重新执行引起缺页中断的指令，CPU 将引起缺页中断的虚拟地址重新发送给 MMU，此时该虚拟地址已经有了映射的物理页框号 PPN，因此会按照前面『Page Hit』的流程走一遍，最后主存把请求的数据返回给处理器。</li></ul><p><img src="https://imghost.leonard.wang//uPic/image-20210606224801SFufxk.png" alt="图片" /></p><h3 id="虚拟内存和高速缓存">虚拟内存和高速缓存</h3><p>前面在分析虚拟内存的工作原理之时，谈到页表的存储位置，为了简化处理，都是默认把主存和高速缓存放在一起，而实际上更详细的流程应该是如下的原理图：</p><p><img src="https://imghost.leonard.wang//uPic/image-20210606223200MFm3lI.png" alt="图片" /></p><p>如果一台计算机同时配备了虚拟内存技术和 CPU 高速缓存，那么 MMU 每次都会优先尝试到高速缓存中进行寻址，如果缓存命中则会直接返回，只有当缓存不命中之后才去主存寻址。</p><p>通常来说，大多数系统都会选择利用物理内存地址去访问高速缓存，因为高速缓存相比于主存要小得多，所以使用物理寻址也不会太复杂；另外也因为高速缓存容量很小，所以系统需要尽量在多个进程之间共享数据块，而使用物理地址能够使得多进程同时在高速缓存中存储数据块以及共享来自相同虚拟内存页的数据块变得更加直观。</p><h3 id="加速翻译优化页表">加速翻译&amp;优化页表</h3><p>经过前面的剖析，相信读者们已经了解了虚拟内存及其分页&amp;地址翻译的基础和原理。现在我们可以引入虚拟内存中两个核心的需求，或者说瓶颈：</p><ul><li>虚拟地址到物理地址的映射过程必须要非常快，地址翻译如何加速。</li><li>虚拟地址范围的增大必然会导致页表的膨胀，形成大页表。</li></ul><p>这两个因素决定了虚拟内存这项技术能不能真正地广泛应用到计算机中，如何解决这两个问题呢？</p><p>正如文章开头所说：&quot;<strong>计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决</strong>&quot;。因此，虽然虚拟内存本身就已经是一个中间层了，但是中间层里的问题同样可以通过再引入一个中间层来解决。</p><p>加速地址翻译过程的方案目前是通过引入页表缓存模块 -- TLB，而大页表则是通过实现多级页表或倒排页表来解决。</p><h4 id="tlb-加速">TLB 加速</h4><p><strong>翻译后备缓冲器</strong>（Translation Lookaside Buffer，TLB），也叫快表，是用来加速虚拟地址翻译的，因为虚拟内存的分页机制，页表一般是保存在内存中的一块固定的存储区，而 MMU 每次翻译虚拟地址的时候都需要从页表中匹配一个对应的 PTE，导致进程通过 MMU 访问指定内存数据的时候比没有分页机制的系统多了一次内存访问，一般会多耗费几十到几百个 CPU 时钟周期，性能至少下降一半，如果 PTE 碰巧缓存在 CPU L1 高速缓存中，则开销可以降低到一两个周期，但是我们不能寄希望于每次要匹配的 PTE 都刚好在 L1 中，因此需要引入加速机制，即 TLB 快表。</p><p>TLB 可以简单地理解成页表的高速缓存，保存了最高频被访问的页表项 PTE。由于 TLB 一般是硬件实现的，因此速度极快，MMU 收到虚拟地址时一般会先通过硬件 TLB 并行地在页表中匹配对应的 PTE，若命中且该 PTE 的访问操作不违反保护位（比如尝试写一个只读的内存地址），则直接从 TLB 取出对应的物理页框号 PPN 返回，若不命中则会穿透到主存页表里查询，并且会在查询到最新页表项之后存入 TLB，以备下次缓存命中，如果 TLB 当前的存储空间不足则会替换掉现有的其中一个 PTE。</p><p>下面来具体分析一下 TLB 命中和不命中。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210606224823eSL2nw.png" alt="图片" /></p><p><strong>TLB 命中</strong>：</p><ul><li><strong>第 1 步</strong>：CPU 产生一个虚拟地址 VA；</li><li><strong>第 2 步和第 3 步</strong>：MMU 从 TLB 中取出对应的 PTE；</li><li><strong>第 4 步</strong>：MMU 将这个虚拟地址 VA 翻译成一个真实的物理地址 PA，通过地址总线发送到高速缓存/主存中去；</li><li><strong>第 5 步</strong>：高速缓存/主存将物理地址 PA 上的数据返回给 CPU。</li></ul><p><img src="https://imghost.leonard.wang//uPic/image-20210606223200U77pUu.jpg" alt="图片" /></p><p><strong>TLB 不命中</strong>：</p><ul><li><strong>第 1 步</strong>：CPU 产生一个虚拟地址 VA；</li><li><strong>第 2 步至第 4 步</strong>：查询 TLB 失败，走正常的主存页表查询流程拿到 PTE，然后把它放入 TLB 缓存，以备下次查询，如果 TLB 此时的存储空间不足，则这个操作会汰换掉 TLB 中另一个已存在的 PTE；</li><li><strong>第 5 步</strong>：MMU 将这个虚拟地址 VA 翻译成一个真实的物理地址 PA，通过地址总线发送到高速缓存/主存中去；</li><li><strong>第 6 步</strong>：高速缓存/主存将物理地址 PA 上的数据返回给 CPU。</li></ul><h4 id="多级页表">多级页表</h4><p>TLB 的引入可以一定程度上解决虚拟地址到物理地址翻译的开销问题，接下来还需要解决另一个问题：大页表。</p><p>理论上一台 32 位的计算机的寻址空间是 4GB，也就是说每一个运行在该计算机上的进程理论上的虚拟寻址范围是 4GB。到目前为止，我们一直在讨论的都是单页表的情形，如果每一个进程都把理论上可用的内存页都装载进一个页表里，但是实际上进程会真正使用到的内存其实可能只有很小的一部分，而我们也知道页表也是保存在计算机主存中的，那么势必会造成大量的内存浪费，甚至有可能导致计算机物理内存不足从而无法并行地运行更多进程。</p><p>这个问题一般通过<strong>多级页表</strong>（Multi-Level Page Tables）来解决，通过把一个大页表进行拆分，形成多级的页表，我们具体来看一个二级页表应该如何设计：假定一个虚拟地址是 32 位，由 10 位的一级页表索引、10 位的二级页表索引以及 12 位的地址偏移量，则 PTE 是 4 字节，页面 page 大小是 2<sup>12 = 4KB，总共需要 2</sup>20 个 PTE，一级页表中的每个 PTE 负责映射虚拟地址空间中的一个 4MB 的 chunk，每一个 chunk 都由 1024 个连续的页面 Page 组成，如果寻址空间是 4GB，那么一共只需要 1024 个 PTE 就足够覆盖整个进程地址空间。二级页表中的每一个 PTE 都负责映射到一个 4KB 的虚拟内存页面，和单页表的原理是一样的。</p><p>多级页表的关键在于，我们并不需要为一级页表中的每一个 PTE 都分配一个二级页表，而只需要为进程当前使用到的地址做相应的分配和映射。因此，对于大部分进程来说，它们的一级页表中有大量空置的 PTE，那么这部分 PTE 对应的二级页表也将无需存在，这是一个相当可观的内存节约，事实上对于一个典型的程序来说，理论上的 4GB 可用虚拟内存地址空间绝大部分都会处于这样一种未分配的状态；更进一步，在程序运行过程中，只需要把一级页表放在主存中，虚拟内存系统可以在实际需要的时候才去创建、调入和调出二级页表，这样就可以确保只有那些最频繁被使用的二级页表才会常驻在主存中，此举亦极大地缓解了主存的压力。</p><p><img src="https://imghost.leonard.wang//uPic/image-202106062232001GF8JI.jpg" alt="图片" /></p><p>多级页表的层级深度可以按照需求不断扩充，一般来说，级数越多，灵活性越高。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210606224857HT94f5.png" alt="图片" /></p><p>比如有个一个 k 级页表，虚拟地址由 k 个 VPN 和 1 个 VPO 组成，每一个 VPN i 都是一个到第 i 级页表的索引，其中 1 &lt;= i &lt;= k。第 j 级页表中的每一个 PTE（1 &lt;= j &lt;= k-1）都指向第 j+1 级页表的基址。第 k 级页表中的每一个 PTE 都包含一个物理地址的页框号 PPN，或者一个磁盘块的地址（该内存页已经被页面置换算法换出到磁盘中）。MMU 每次都需要访问 k 个 PTE 才能找到物理页框号 PPN 然后加上虚拟地址中的偏移量 VPO 从而生成一个物理地址。这里读者可能会对 MMU 每次都访问 k 个 PTE 表示性能上的担忧，此时就是 TLB 出场的时候了，计算机正是通过把每一级页表中的 PTE 缓存在 TLB 中从而让多级页表的性能不至于落后单页表太多。</p><h4 id="倒排页表">倒排页表</h4><p>另一种针对页式虚拟内存管理大页表问题的解决方案是<strong>倒排页表</strong>（Inverted Page Table，简称 IPT）。倒排页表的原理和搜索引擎的倒排索引相似，都是通过反转映射过程来实现。</p><p>在搜索引擎中，有两个概念：文档 doc 和 关键词 keyword，我们的需求是通过 keyword 快速找到对应的 doc 列表，如果搜索引擎的存储结构是正向索引，也即是通过 doc 映射到其中包含的所有 keyword 列表，那么我们要找到某一个指定的 keyword 所对应的 doc 列表，那么便需要扫描索引库中的所有 doc，找到包含该 keyword 的 doc，再根据打分模型进行打分，排序后返回，这种设计无疑是低效的；所以我们需要反转一下正向索引从而得到倒排索引，也即通过 keyword 映射到所有包含它的 doc 列表，这样当我们查询包含某个指定 keyword 的 doc 列表时，只需要利用倒排索引就可以快速定位到对应的结果，然后还是根据打分模型进行排序返回。</p><p>上面的描述只是搜索引擎倒排索引的简化原理，实际的倒排索引设计是要复杂很多的，有兴趣的读者可以自行查找资料学习，这里就不再展开。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210606224909PW22PA.png" alt="图片" /></p><p>回到虚拟内存的倒排页表，它正是采用了和倒排索引类似的思想，反转了映射过程：前面我们学习到的页表设计都是以虚拟地址页号 VPN 作为页表项 PTE 索引，映射到物理页框号 PPN，而在倒排页表中则是以 PPN 作为 PTE 索引，映射到 (进程号，虚拟页号 VPN)。</p><p>倒排页表在寻址空间更大的 CPU 架构下尤其高效，或者应该说更适合那些『虚拟内存空间/物理内存空间』比例非常大的场景，因为这种设计是以实际物理内存页框作为 PTE 索引，而不是以远超物理内存的虚拟内存作为索引。例如，以 64 位架构为例，如果是单页表结构，还是用 12 位作为页面地址偏移量，也就是 4KB 的内存页大小，那么以最理论化的方式来计算，则需要 2<sup>52 个 PTE，每个 PTE 占 8 个字节，那么整个页表需要 32PB 的内存空间，这完全是不可接受的，而如果采用倒排页表，假定使用 4GB 的 RAM，则只需要 2</sup>20 个 PTE，极大减少内存使用量。</p><p>倒排页表虽然在节省内存空间方面效果显著，但同时却引入了另一个重大的缺陷：地址翻译过程变得更加低效。我们都清楚 MMU 的工作就是要把虚拟内存地址翻译成物理内存地址，现在索引结构变了，物理页框号 PPN 作为索引，从原来的 VPN --&gt; PPN 变成了 PPN --&gt; VPN，那么当进程尝试访问一个虚拟内存地址之时，CPU 在通过地址总线把 VPN 发送到 MMU 之后，基于倒排页表的设计，MMU 并不知道这个 VPN 对应的是不是一个缺页，所以不得不扫描整个倒排页表来找到该 VPN，而最要命的是就算是一个非缺页的 VPN，每次内存访问还是需要执行这个全表扫描操作，假设是前面提到的 4GB RAM 的例子，那么相当于每次都要扫描 2^20 个 PTE，相当低效。</p><p>这时候又是我们的老朋友 -- TLB 出场的时候了，我们只需要把高频使用的页面缓存在 TLB 中，借助于硬件，在 TLB 缓存命中的情况下虚拟内存地址的翻译过程就可以像普通页表那样快速，然而当 TLB 失效的时候，则还是需要通过软件的方式去扫描整个倒排页表，线性扫描的方式非常低效，因此一般倒排页表会基于哈希表来实现，假设有 1G 的物理内存，那么这里就一共有 2^18 个 4KB 大小的页框，建立一张以 PPN 作为 key 的哈希表，每一个 key 值对应的 value 中存储的是 (VPN, PNN)，那么所有具有相同哈希值的 VPN 会被链接在一起形成一个冲突链，如果我们把哈希表的槽数设置成跟物理页框数量一致的话，那么这个倒排哈希表中的冲突链的平均长度将会是 1 个 PTE，可以大大提高查询速度。当 VPN 通过倒排页表匹配到 PPN 之后，这个 (VPN, PPN) 映射关系就会马上被缓存进 TLB，以加速下次虚拟地址翻译。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210606224925yVavzt.jpg" alt="图片" /></p><p>倒排页表在 64 位架构的计算机中很常见，因为在 64 位架构下，基于分页的虚拟内存中即便把页面 Page 的大小从一般的 4KB 提升至 4MB，依然需要一个拥有 2^42 个 PTE 的巨型页表放在主存中（理论上，实际上不会这么实现），极大地消耗内存。</p><h2 id="总结">总结</h2><p>现在让我们来回顾一下本文的核心内容：虚拟内存是存在于计算机 CPU 和物理内存之间一个中间层，主要作用是高效管理内存并减少内存出错。虚拟内存的几个核心概念有：</p><ol><li><strong>页表</strong>：从数学角度来说页表就是一个函数，入参是虚拟页号 VPN，输出是物理页框号 PPN，也就是物理地址的基址。页表由页表项组成，页表项中保存了所有用来进行地址翻译所需的信息，页表是虚拟内存得以正常运作的基础，每一个虚拟地址要翻译成物理地址都需要借助它来完成。</li><li><strong>TLB</strong>：计算机硬件，主要用来解决引入虚拟内存之后寻址的性能问题，加速地址翻译。如果没有 TLB 来解决虚拟内存的性能问题，那么虚拟内存将只可能是一个学术上的理论而无法真正广泛地应用在计算机中。</li><li><strong>多级页表和倒排页表</strong>：用来解决虚拟地址空间爆炸性膨胀而导致的大页表问题，多级页表通过将单页表进行分拆并按需分配虚拟内存页而倒排页表则是通过反转映射关系来实现节省内存的效果。</li></ol><p>最后，虚拟内存技术中还需要涉及到操作系统的页面置换机制，由于页面置换机制也是一个较为庞杂和复杂的概念，本文便不再继续剖析这一部分的原理，我们在以后的文章中再单独拿来讲解。</p><h2 id="参考延伸阅读">参考&amp;延伸阅读</h2><p>本文的主要参考资料是《现代操作系统》和《深入理解计算机系统》这两本书的英文原版，如果读者还想更加深入地学习虚拟内存，可以深入阅读这两本书并且搜寻其他的论文资料进行学习。</p>]]>
                    </description>
                    <pubDate>Sun, 06 Jun 2021 22:35:16 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Go iota实现原理]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/iota</link>
                    <description>
                            <![CDATA[<h2 id="编译器基础知识">编译器基础知识</h2><p>引用2020  Gopher China 史斌大佬的一张图</p><blockquote><p><img src="https://imghost.leonard.wang/uPic/image-20210520004100xoWxYO.png" alt="image-20210520003438552" /></p></blockquote><p>简化一下，当前文章只关注语法分析和类型检查即可</p><p>注：当前文章go版本 <a href="https://github.com/golang/go/tree/690a8c3fb136431d4f22894c545ea99278758570">master 690a8c3fb1</a> ，应该是go1.17左右的版本。</p><p><img src="https://imghost.leonard.wang//uPic/image-202105200040241I7QQF.png" alt="image-20210520004023873" /></p><h2 id="开始探究iota">开始探究iota</h2><p>参考杨文大佬的文章<a href="https://mp.weixin.qq.com/s/MwkeB-6OgoTw2JWFcu7bXA">带你开始探究 Go iota</a></p><blockquote><p><a href="https://golang.org/ref/spec#Iota">官方定义</a></p><p>Within a <a href="https://golang.org/ref/spec#Constant_declarations">constant declaration</a>, the predeclared identifier <code>iota</code> represents successive untyped integer <a href="https://golang.org/ref/spec#Constants">constants</a>. Its value is the index of the respective <a href="https://golang.org/ref/spec#ConstSpec">ConstSpec</a> in that constant declaration, starting at zero. It can be used to construct a set of related constants:</p></blockquote><p>在<strong>常量</strong>声明中，<code>iota</code>表示<strong>连续</strong>的<strong>无类型整数常量</strong>，它的值是<strong>从零开始</strong>的索引，可以用来<strong>构造一组</strong>相关的常量：</p><p>先看一个简单的例子</p><pre><code class="language-go">package mainimport &quot;fmt&quot;const (one   = iotatwo   = iotathree = iota)func main() {fmt.Println(one, two, three)}// Output:// 0 1 2</code></pre><p>先从这个例子入手，后续再分析复杂的场景</p><p>Note：例子中的one、two、three实际值为0、1、2，后续流程分析完才发现这个问题，不修改例子了。</p><p>先打印AST看一下，做一下大概的了解。</p><pre><code class="language-go">package mainimport (&quot;go/ast&quot;&quot;go/parser&quot;&quot;go/token&quot;&quot;log&quot;&quot;path/filepath&quot;)func main() {fset := token.NewFileSet()// 这里取绝对路径，方便打印出来的语法树可以转跳到编辑器path, _ := filepath.Abs(&quot;XXX/main.go&quot;)f, err := parser.ParseFile(fset, path, nil, parser.AllErrors)if err != nil {log.Println(err)return}// 打印语法树ast.Print(fset, f)}</code></pre><pre><code class="language-bash">0  *ast.File {1  .  Package: XXX/main.go:1:12  .  Name: *ast.Ident {3  .  .  NamePos: XXX/main.go:1:94  .  .  Name: &quot;main&quot;5  .  }6  .  Decls: []ast.Decl (len = 3) { // 共有3个Top Level Decl7  .  .  0: *ast.GenDecl { // import &quot;fmt&quot;8  .  .  .  TokPos: XXX/main.go:3:19  .  .  .  Tok: import10  .  .  .  Lparen: -11  .  .  .  Specs: []ast.Spec (len = 1) {12  .  .  .  .  0: *ast.ImportSpec {13  .  .  .  .  .  Path: *ast.BasicLit {14  .  .  .  .  .  .  ValuePos: XXX/main.go:3:815  .  .  .  .  .  .  Kind: STRING16  .  .  .  .  .  .  Value: &quot;\&quot;fmt\&quot;&quot;17  .  .  .  .  .  }18  .  .  .  .  .  EndPos: -19  .  .  .  .  }20  .  .  .  }21  .  .  .  Rparen: -22  .  .  }23  .  .  1: *ast.GenDecl {  // const ( XXX )24  .  .  .  TokPos: XXX/main.go:5:125  .  .  .  Tok: const26  .  .  .  Lparen: XXX/main.go:5:727  .  .  .  Specs: []ast.Spec (len = 3) { // 共声明了3个常量28  .  .  .  .  0: *ast.ValueSpec {29  .  .  .  .  .  Names: []*ast.Ident (len = 1) {30  .  .  .  .  .  .  0: *ast.Ident {31  .  .  .  .  .  .  .  NamePos: XXX/main.go:6:232  .  .  .  .  .  .  .  Name: &quot;one&quot;  // 常量名字33  .  .  .  .  .  .  .  Obj: *ast.Object {34  .  .  .  .  .  .  .  .  Kind: const35  .  .  .  .  .  .  .  .  Name: &quot;one&quot;36  .  .  .  .  .  .  .  .  Decl: *(obj @ 28)37  .  .  .  .  .  .  .  .  Data: 0  // “只是iota的值”，后续复杂场景可以理解这句话的意思38  .  .  .  .  .  .  .  }39  .  .  .  .  .  .  }40  .  .  .  .  .  }41  .  .  .  .  .  Values: []ast.Expr (len = 1) {42  .  .  .  .  .  .  0: *ast.Ident {43  .  .  .  .  .  .  .  NamePos: XXX/main.go:6:1044  .  .  .  .  .  .  .  Name: &quot;iota&quot;  // 常量对应的值类型为iota45  .  .  .  .  .  .  }46  .  .  .  .  .  }47  .  .  .  .  }48  .  .  .  .  1: *ast.ValueSpec { //省略部分字段49  .  .  .  .  .  Names: []*ast.Ident (len = 1) {50  .  .  .  .  .  .  0: *ast.Ident {52  .  .  .  .  .  .  .  Name: &quot;two&quot;53  .  .  .  .  .  .  .  Obj: *ast.Object {57  .  .  .  .  .  .  .  .  Data: 158  .  .  .  .  .  .  .  }59  .  .  .  .  .  .  }60  .  .  .  .  .  }61  .  .  .  .  .  Values: []ast.Expr (len = 1) {62  .  .  .  .  .  .  0: *ast.Ident {64  .  .  .  .  .  .  .  Name: &quot;iota&quot;65  .  .  .  .  .  .  }66  .  .  .  .  .  }67  .  .  .  .  }68  .  .  .  .  2: *ast.ValueSpec {69  .  .  .  .  .  Names: []*ast.Ident (len = 1) {70  .  .  .  .  .  .  0: *ast.Ident {72  .  .  .  .  .  .  .  Name: &quot;three&quot;73  .  .  .  .  .  .  .  Obj: *ast.Object {77  .  .  .  .  .  .  .  .  Data: 278  .  .  .  .  .  .  .  }79  .  .  .  .  .  .  }80  .  .  .  .  .  }81  .  .  .  .  .  Values: []ast.Expr (len = 1) {82  .  .  .  .  .  .  0: *ast.Ident {84  .  .  .  .  .  .  .  Name: &quot;iota&quot;85  .  .  .  .  .  .  }86  .  .  .  .  .  }87  .  .  .  .  }88  .  .  .  }89  .  .  .  Rparen: XXX/main.go:9:190  .  .  }91  .  .  2: *ast.FuncDecl { // func main92  .  .  .  Name: *ast.Ident {93  .  .  .  .  NamePos: XXX/main.go:11:694  .  .  .  .  Name: &quot;main&quot;95  .  .  .  .  Obj: *ast.Object {96  .  .  .  .  .  Kind: func97  .  .  .  .  .  Name: &quot;main&quot;98  .  .  .  .  .  Decl: *(obj @ 91)99  .  .  .  .  }100  .  .  .  }101  .  .  .  148  .  .  }149  .  }150  .  Scope: *ast.Scope {151  .  .  Objects: map[string]*ast.Object (len = 4) {152  .  .  .  &quot;three&quot;: *(obj @ 73)153  .  .  .  &quot;main&quot;: *(obj @ 95)154  .  .  .  &quot;one&quot;: *(obj @ 33)155  .  .  .  &quot;two&quot;: *(obj @ 53)156  .  .  }157  .  }158  .  Imports: []*ast.ImportSpec (len = 1) {159  .  .  0: *(obj @ 12)160  .  }161  .  Unresolved: []*ast.Ident (len = 4) {162  .  .  0: *(obj @ 42)163  .  .  1: *(obj @ 62)164  .  .  2: *(obj @ 82)165  .  .  3: *(obj @ 114)166  .  }167  }</code></pre><h3 id="源码剖析">源码剖析</h3><h4 id="调试go编译器代码">调试Go编译器代码</h4><p>使用Goland调试Go编译器代码</p><p><code>$GOPATH/src/cmd/compile/main.go</code></p><p><img src="https://imghost.leonard.wang//uPic/image-20210522095340PbZuhb.png" alt="image-20210522095340794" /></p><h4 id="iota值是如何确定的">iota值是如何确定的</h4><p>关注<code>ConstDecl</code>的处理</p><p>先在<code>$GOPATH/src/cmd/compile/internal/noder/noder.go:325</code>打断点观察一下</p><p><img src="https://imghost.leonard.wang//uPic/image-20210522101059HR1VwM.jpg" alt="1621648930041" /></p><p>上方说了，当前文件“共有3个Top Level Decl”，为什么这里的decls有5个？</p><p>实际上是把 Top Level Decl 中的<code>const(XXX)</code> 展平了</p><p>这里关注一个新的概念：<code>Group</code>，从调试信息中可以看出，<code>one</code>、<code>two</code>、<code>three</code>这三个<code>ConstDecl</code>指向了同一个<code>Group</code></p><p><img src="https://imghost.leonard.wang//uPic/image-202105221021174EY1oy.png" alt="WX20210522-101413@2x" /></p><p>前面埋了两个坑 <code>cs</code>、<code>Group</code></p><p>这三个<code>ConstDecl</code>会按顺序执行这一行代码，并传入<code>cs</code></p><pre><code class="language-go">l = append(l, p.constDecl(decl, &amp;cs)...)</code></pre><p>继续来看<code>p.constDecl</code>的实现（删除部分源码，仅关注当前场景的逻辑代码，后面会再解析剩余源码）</p><p>当前场景下，<code>names</code>和<code>values</code>的长度都为1</p><pre><code class="language-go">type constState struct {group  *syntax.Grouptyp    ir.Ntypevalues []ir.Nodeiota   int64         // 初始值为0}func (p *noder) constDecl(decl *syntax.ConstDecl, cs *constState) []ir.Node {// 当Group为nil 或者 当前处理的 Group 与 cs 缓存的 Group 不相同时，新建一个 cs// Group，顾名思义，“组”// 当前场景中 ，`one`、`two`、`three` 被同一个 const ()包裹，属于同一个组// cs是在外层循环开始时定义的，初始化的 cs 是一个空值，所以不属于任何Group// cs是通过指针传递的，所以在该函数中新建cs，外层是可以感知到的cs的变化的，并会传递给下一个ConstDecl的p.constDecl方法// 所以在当前场景中，在处理const one时，会进入该分支，更新Group信息// 在处理two、three时，由于归属同一Group，不会再进入该分支// 通过这种方法达到了同一个const Group中iota值自加的目的if decl.Group == nil || decl.Group != cs.group {*cs = constState{group: decl.Group,}}names := p.declNames(ir.OLITERAL, decl.NameList)typ := p.typeExprOrNil(decl.Type)var values []ir.Nodeif decl.Values != nil {values = p.exprList(decl.Values)}nn := make([]ir.Node, 0, len(names))for i, n := range names {if i &gt;= len(values) {base.Errorf(&quot;missing value in const declaration&quot;)break}v := values[i]typecheck.Declare(n, typecheck.DeclContext)n.Ntype = typn.Defn = v// 在处理one时，iota为初始值0n.SetIota(cs.iota) // 设置这个Node的iota值nn = append(nn, ir.NewDecl(p.pos(decl), ir.ODCLCONST, n))}if len(values) &gt; len(names) {base.Errorf(&quot;extra expression in const declaration&quot;)}cs.iota++ // iota值自增，所以在后续继续处理two、three时，对应的值已经更改为1,2return nn}</code></pre><p><code>SetIota</code>实际上就是更改了Node中Offset_字段的值，虽然这个<code>Offset</code>有些词不达意，其实对于不同的Decl类型，<code>Offset</code>字段的含义是不一样的，复用该字段，来节省空间。</p><pre><code class="language-go">func (n *Name) SetIota(x int64)        { n.Offset_ = x }</code></pre><p>继续查看一下返回值nn中的信息</p><p>可以简单认为，将该Decl节点换了一种形式，我们关注的信息都还在，这个符号的名字是<code>one</code>，值是<code>iota</code>，<code>iota</code>的真实值<code>Offset</code>为0。</p><p>另外注意下，<code>Defn</code>中的<code>sym</code>字段，该字段是个指针，它的<code>Def</code>后续会用来标识和计算<code>iota</code>的值，当前还为空，记住，<code>Defn</code>中的<code>sym字</code>段是个指针，后续还有个骚操作。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210522205345g9b5mE.png" alt="WX20210522-204743@2x" /></p><p>再继续看一下<code>func (p *noder) decls</code>函数的返回值</p><p>这里这有4个Node，是因为对于<code>syntax.ImportDecl</code>，没有append加入到返回值中。</p><p>第一个Node上方已观察过，这里只关注第二个和第三个Node的信息，最后一个Node为func main，暂时也不关注。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210522210140bcEkG5.png" alt="WX20210522-210025@2x" /></p><h4 id="iota值的读取">iota值的读取</h4><p>继续执行编译流程，关注一下<code>DeclareUniverse</code>函数，在这个函数中会将上方说到的<code>Defn-&gt;sym-&gt;Def</code>字段统一赋值。</p><pre><code class="language-go">func DeclareUniverse() {// Operationally, this is similar to a dot import of builtinpkg, except// that we silently skip symbols that are already declared in the// package block rather than emitting a redeclared symbol error.for _, s := range types.BuiltinPkg.Syms {if s.Def == nil {continue}s1 := Lookup(s.Name)if s1.Def != nil {continue}s1.Def = s.Defs1.Block = s.Block}}</code></pre><p>这里其实对于Go中的所有内建(builtin)类型进行了处理，看下Go的内建类型都有哪些，这里应该和<code>$GOPATH/src/builtin/builtin.go</code>是可以对应的。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210522211055hCYZsD.png" alt="WX20210522-210725@2x" /></p><p><code>Defn-&gt;sym-&gt;Def</code>赋值后的信息如下，名字仍为<code>iota</code>，类型为<code>OIOTA</code>，后续会根据该信息进行进一步处理。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210522211512YL5N1o.png" alt="image-20210522211512724" /></p><p>在<code>func typecheckdef(n *ir.Name)</code>中拿到<code>Defn</code>进行进一步操作，注意，这里取出后就设置为<code>nil</code>清空了。</p><pre><code class="language-go">e := n.Defnn.Defn = nilif e == nil {  ir.Dump(&quot;typecheckdef nil defn&quot;, n)  base.ErrorfAt(n.Pos(), &quot;xxx&quot;)}e = Expr(e)</code></pre><p>接着到达<code>func Resolve(n ir.Node) (res ir.Node)</code>函数</p><p>该函数会对<code>OIOTA</code>类型的节点，取出其对应的真实值，并返回一个Int节点，进行后续的操作。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210522212752nyYhpx.png" alt="WX20210522-212737@2x" /></p><p>将<code>iota</code>的值取出的正常流程也比较简单从<code>Offest</code>字段直接获取即可，另外一个if分支我当前还不清楚是什么场景，先留个坑。</p><pre><code class="language-go">func getIotaValue() int64 {if i := len(typecheckdefstack); i &gt; 0 {if x := typecheckdefstack[i-1]; x.Op() == ir.OLITERAL {// 当前场景走这个分支return x.Iota()}}if ir.CurFunc != nil &amp;&amp; ir.CurFunc.Iota &gt;= 0 {return ir.CurFunc.Iota}return -1}func (n *Name) Iota() int64            { return n.Offset_ }</code></pre><p>至此，<code>iota</code>类型的生成，变换，读取的逻辑已经都找到了，<code>iota</code>这个语法糖也已经变为正常的<code>int</code>值，可以进行后续的正常处理了。</p><p>再来回顾一下iota值计算前后的Decls的状态</p><p>处理前：</p><p>const two和three的val为空。</p><p><img src="https://imghost.leonard.wang//uPic/image-202105222139125sasnX.png" alt="image-20210522213911917" /></p><p>处理后：</p><p><img src="https://imghost.leonard.wang//uPic/image-20210522214416QYK3aX.png" alt="image-20210522214411041" /></p><h4 id="group的生成和划分">Group的生成和划分</h4><p>还有一个坑要填，上方说的Group是如何生成和划分的？</p><p>这个其实是在Parser阶段就已经划分好了</p><p>找到 <code>const</code> 关键字时的处理</p><p><img src="https://imghost.leonard.wang//uPic/image-20210522215129gIPEgj.png" alt="WX20210522-215112@2x" /></p><p>继续处理的逻辑，其实比较简单</p><ul><li>如果const后面有 <code>(</code>，那么创建一个新的<code>Group</code>，继续向后扫描，直到找到与其匹配的<code>)</code></li><li>如果const后面没有 <code>(</code>，那么传入的<code>Group</code>为空</li></ul><p><img src="https://imghost.leonard.wang//uPic/image-20210522215944FIoU5O.png" alt="WX20210522-215226@2x" /></p><pre><code class="language-go">// ConstSpec = IdentifierList [ [ Type ] &quot;=&quot; ExpressionList ] .func (p *parser) constDecl(group *Group) Decl {if trace {defer p.trace(&quot;constDecl&quot;)()}d := new(ConstDecl)d.pos = p.pos()d.Group = groupd.Pragma = p.takePragma()d.NameList = p.nameList(p.name())if p.tok != _EOF &amp;&amp; p.tok != _Semi &amp;&amp; p.tok != _Rparen {d.Type = p.typeOrNil()if p.gotAssign() {d.Values = p.exprList()}}return d}</code></pre><p>当前token位置在<code>one</code>的位置，是名字信息，调用<code>p.name()</code>后，token会向后走，在<code>gotAssign</code>中取到<code>=</code>，<code>p.exprList()</code>中取到<code>value</code></p><p><img src="https://imghost.leonard.wang//uPic/image-20210522220702uDZ5xP.png" alt="image-20210522220701827" /></p><p>解析完成后的结果为，根据地址可以看出，归属于同一个Group</p><p><img src="https://imghost.leonard.wang//uPic/image-20210522221728NQf9vt.jpg" alt="1621693040112" /></p><p>后续的内容就可以和上方的衔接上了。</p><h3 id="几个额外的场景">几个额外的场景</h3><h4 id="不显式写出iota">不显式写出<code>iota</code></h4><pre><code class="language-go">package mainimport &quot;fmt&quot;const (zero = iotaonetwo)func main() {fmt.Println(zero, one, two)}// Output:// 0 1 2</code></pre><p>观察一下<code>func (p *noder) decls</code>中<code>decls</code>中的情况（计算iota值之前）</p><p>只有zero的Value是iota，one、two的Value都为空，且这三个常量属于同一个Group</p><p><img src="https://imghost.leonard.wang//uPic/image-202105222322530CBzYt.png" alt="WX20210522-232226@2x" /></p><p>再次解析一下<code>constDecl</code>函数中相关的逻辑代码</p><p>Note：处理<code>zero</code>、<code>one</code>、<code>two</code>的过程中会进入该函数三次</p><pre><code class="language-go">func (p *noder) constDecl(decl *syntax.ConstDecl, cs *constState) []ir.Node {// 处理zero时，新建一个constStateif decl.Group == nil || decl.Group != cs.group {*cs = constState{group: decl.Group,}}names := p.declNames(ir.OLITERAL, decl.NameList)typ := p.typeExprOrNil(decl.Type)var values []ir.Nodeif decl.Values != nil {// 处理zero时，Values不为空，将zero的typ和values信息缓存到cs中values = p.exprList(decl.Values)cs.typ, cs.values = typ, values} else {// 处理two、three时.Values为空// 读取cs中缓存的typ和values信息（即与zero的typ和values信息相同）// 达到”继承“上一个显式所写的 iota 表达式的目的if typ != nil {base.Errorf(&quot;const declaration cannot have type without expression&quot;)}typ, values = cs.typ, cs.values}cs.iota++return nn}</code></pre><h4 id="const值是和iota相关的表达式">const值是和iota相关的表达式</h4><pre><code class="language-go">package mainimport &quot;fmt&quot;const (test1 = iota + 2test2)func main() {fmt.Println(test1, test2)}// Output:// 2 3</code></pre><p>在AST中存储的结构为：</p><p><img src="https://imghost.leonard.wang//uPic/image-20210522224358teAGEz.png" alt="WX20210522-223606@2x" /></p><p>与上方所述流程不同的是，在将iota替换为对应的值后，还有一个二元表达式加法的计算和替换过程</p><pre><code class="language-go">func EvalConst(n ir.Node) ir.Node {// Pick off just the opcodes that can be constant evaluated.switch n.Op() { //case ir.OADD, ir.OSUB, ir.OMUL, ir.ODIV, ir.OMOD, ir.OOR, ir.OXOR, ir.OAND, ir.OANDNOT:n := n.(*ir.BinaryExpr)nl, nr := n.X, n.Y // 获取二元表达式的两个操作数if nl.Op() == ir.OLITERAL &amp;&amp; nr.Op() == ir.OLITERAL {rval := nr.Val()// check for divisor underflow in complex division (see issue 20227)if n.Op() == ir.ODIV &amp;&amp; n.Type().IsComplex() &amp;&amp; constant.Sign(square(constant.Real(rval))) == 0 &amp;&amp; constant.Sign(square(constant.Imag(rval))) == 0 {base.Errorf(&quot;complex division by zero&quot;)n.SetType(nil)return n}if (n.Op() == ir.ODIV || n.Op() == ir.OMOD) &amp;&amp; constant.Sign(rval) == 0 {base.Errorf(&quot;division by zero&quot;)n.SetType(nil)return n}tok := tokenForOp[n.Op()]if n.Op() == ir.ODIV &amp;&amp; n.Type().IsInteger() {tok = token.QUO_ASSIGN // integer division}return OrigConst(n, constant.BinaryOp(nl.Val(), tok, rval))}return n}  // OrigConst returns an OLITERAL with orig n and value v.func OrigConst(n ir.Node, v constant.Value) ir.Node {lno := ir.SetPos(n)v = convertVal(v, n.Type(), false)base.Pos = lnoswitch v.Kind() {case constant.Int:if constant.BitLen(v) &lt;= ir.ConstPrec {break}fallthroughcase constant.Unknown:what := overflowNames[n.Op()]if what == &quot;&quot; {base.Fatalf(&quot;unexpected overflow: %v&quot;, n.Op())}base.ErrorfAt(n.Pos(), &quot;constant %v overflow&quot;, what)n.SetType(nil)return n}// 返回一个新的ConstExpr Nodereturn ir.NewConstExpr(v, n)}    func BinaryOp(x_ Value, op token.Token, y_ Value) Value {x, y := match(x_, y_)switch x := x.(type) {case unknownVal:return xcase int64Val:a := int64(x)b := int64(y.(int64Val))var c int64switch op {case token.ADD:c = a + b // 计算Add之后的值}return int64Val(c)}}</code></pre><h4 id="同一行中有多个常量">同一行中有多个常量</h4><pre><code class="language-go">package mainimport &quot;fmt&quot;const (test1, test2 = iota + 1, iota + 2test3, test4 = iota + 3, iota + 4)func main() {fmt.Println(test1, test2, test3, test4)}// Output:// 1 2 4 5</code></pre><p>实际上 test1、test2属于同一个Decl、test3、test4属于同一个Decl，当然它们都属于同一个Group。</p><p>上方的例子中，<code>NameList</code>和<code>Values</code>中都是只有一个值，实际上它是可以存储多个值的。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210522234303EmUtcM.png" alt="WX20210522-233851@2x" /></p><p>再次回到<code>constDecl</code>函数中相关的逻辑代码</p><p>这个例子中会进入该函数两次，第一次时<code>NameList</code>为<code>[test1,test2]</code> ，第二次时<code>NameList</code>为 <code>[test3,test4]</code></p><pre><code class="language-go">func (p *noder) constDecl(decl *syntax.ConstDecl, cs *constState) []ir.Node {if decl.Group == nil || decl.Group != cs.group {*cs = constState{// cs.iota 初始值为0group: decl.Group,}}names := p.declNames(ir.OLITERAL, decl.NameList)typ := p.typeExprOrNil(decl.Type)var values []ir.Nodenn := make([]ir.Node, 0, len(names))// 遍历NameList中的所有值for i, n := range names {if i &gt;= len(values) {base.Errorf(&quot;missing value in const declaration&quot;)break}v := values[i]if decl.Values == nil {v = ir.DeepCopy(n.Pos(), v)}typecheck.Declare(n, typecheck.DeclContext)n.Ntype = typn.Defn = vn.SetIota(cs.iota) // 将其都设置为cs.iota的当前值nn = append(nn, ir.NewDecl(p.pos(decl), ir.ODCLCONST, n))}// 更新cs.iotacs.iota++return nn}</code></pre><p>计算<code>iota</code>后，对应的值如下</p><p><img src="https://imghost.leonard.wang//uPic/image-20210522235219tspQry.png" alt="image-20210522235219155" /></p><p><img src="https://imghost.leonard.wang//uPic/image-20210522235329I2lBRo.png" alt="image-20210522235329702" /></p><h4 id="const组中存在-的情况">const组中存在<code>_</code>的情况</h4><pre><code class="language-go">package mainimport &quot;fmt&quot;const (zero = iotaonetwothree_five = iota)func main() {fmt.Println(zero, one, two, three, five)}// Output:// 0 1 2 3 5</code></pre><p>分析过程与上述相似，读者可自行分析。</p><h4 id="const组中第一行不是iota">const组中第一行不是iota</h4><pre><code class="language-go">package mainimport &quot;fmt&quot;const (five  = 5two   = iotathree = iota)func main() {fmt.Println(five, two, three)}// Output:// 5 1 2</code></pre><p>分析过程与上述相似，读者可自行分析。</p><h2 id="总结">总结</h2><p>分析过<code>iota</code>的源码之后，如何人工debug <code>iota</code>的值呢？</p><ul><li><p>先分组(Group)：每组的iota值是相互独立的，初始值为0</p></li><li><p>再分行(line)：每组中的iota按行从0开始自增</p></li><li><p>确定每一行的iota值：同一行中iota的是相同的</p></li><li><p>确定每一行和iota相关表达式的值：再计算诸如iota+1的具体值</p></li></ul>]]>
                    </description>
                    <pubDate>Sat, 22 May 2021 23:36:36 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Init函数执行流程]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/inittask</link>
                    <description>
                            <![CDATA[<p>在Go1.14之后，init函数进行了改动，每个package会<strong>按需</strong>生成一个<code>package..inittask</code>的符号</p><p>该符号有三个字段</p><ul><li>state  本package init的状态，0：未初始化，1：正在初始化中，2：初始化完成</li><li>ndeps 本package init所依赖的package个数</li><li>nfns 本package中需要动态初始化的变量（var s = make([]int,10)）和显式写的func init的个数</li><li>后续是按照内存偏移，将具体所依赖的package..inittask、和init函数指针，写入到结构体后面</li></ul><pre><code class="language-go">type initTask struct {// TODO: pack the first 3 fields more tightly?state uintptr // 0 = uninitialized, 1 = in progress, 2 = donendeps uintptrnfns  uintptr// followed by ndeps instances of an *initTask, one per package depended on// followed by nfns pcs, one per init function to run}</code></pre><h3 id="一个小例子">一个小例子</h3><p><img src="https://imghost.leonard.wang//uPic/image-20210516001426bqxTqr.png" alt="image-20210516001425942" /></p><h3 id="执行时机">执行时机</h3><p>在用户main函数执行之前，按照<strong>深度优先</strong>的方式进行初始化</p><pre><code class="language-go">// $GOROOT/src/runtime/proc.go// 这个是`runtime.main`，也是运行的第一个goroutine，// 会先进行GC初始化等操作，运行`runtime..inittask`，// 运行`main..inittask`，再调用用户的 `func main`// The main goroutine.func main() {g := getg()// Allow newproc to start new Ms.mainStarted = trueif GOARCH != &quot;wasm&quot; { // no threads on wasm yet, so no sysmonsystemstack(func() {newm(sysmon, nil, -1)})}// 先执行runtime的init函数doInit(&amp;runtime_inittask) // must be before defermain_init_done = make(chan bool)// Note: 执行用户main包的init函数doInit(&amp;main_inittask)close(main_init_done)needUnlock = falseunlockOSThread()fn := main_mainfn() // 调用 用户的 func main// ……// 设置程序退出码为0，go的 main 函数没有返回值。exit(0)  }func doInit(t *initTask) {switch t.state {case 2: // 该package已初始化完成，直接返回，避免重复初始化returncase 1: // 如果调用doInit时，状态为1，说明在初始话某个package的过程中，又调用到了自身（类似于循环引用），是一种不应该发生的情况，直接报错throw(&quot;recursive call during initialization - linker skew&quot;)default: // 默认为0t.state = 1 // 先将设置为1，正在初始化中for i := uintptr(0); i &lt; t.ndeps; i++ {// 3是 state+ndeps+nfns 这三个字段的长度// 按照偏移去找到具体的dep，强转为*initTask，并递归调用doInitp := add(unsafe.Pointer(t), (3+i)*sys.PtrSize)t2 := *(**initTask)(p)doInit(t2)}// 执行到这里时，本package所有依赖的package的init函数已执行完成，下面开始执行本package中需要动态初始化和用户显示写的func init函数for i := uintptr(0); i &lt; t.nfns; i++ {p := add(unsafe.Pointer(t), (3+t.ndeps+i)*sys.PtrSize)f := *(*func())(unsafe.Pointer(&amp;p))f()}t.state = 2 // 设置初始化状态为已完成}}</code></pre><p>从源码可以看出：</p><ul><li><p>通过<code>state</code>是可以保证每个<code>package</code>只会被执行一次的</p></li><li><p>执行顺序是按照<strong>深度优先</strong>的方式进行初始化，所以可以保证其所依赖的package的初始化一定在当package初始化之前</p></li><li><p>同一个package中的初始化函数实际上是没有层级关系的，所以不应该依赖同一个package中初始函数的执行顺序</p></li></ul><p>比如说如下的例子</p><pre><code class="language-go">package mainimport &quot;fmt&quot;var global map[int64]stringfunc init() {global = make(map[int64]string, 100)fmt.Println(&quot;init 1&quot;)}func init() {global[1] = &quot;1&quot;fmt.Println(&quot;init 2&quot;)}func main() {fmt.Println(&quot;Hello World&quot;)}// Output:// init 1// init 2// Hello World</code></pre><p>虽然程序可以正常运行，且输出是稳定的，但是这依赖于编译器的实现，先扫描到哪个<code>func init</code>，再极端一点，如果再同一个package中的两个文件中，那又如何保证顺序呢？</p><p>从源码来说，这两个init 是没有层级关系的，Go对于同一个<code>package</code>中的<code>init</code>函数的执行顺序也没有任何说明，不建议使用这种未明确的“特性”。</p><h3 id="inittask是如何生成的">initTask是如何生成的？</h3><p>实际上是在编译过程中写入到程序二进制中的，在程序启动时被加载到内存中，所以后续的处理都是使用unsafe.Pointer和uintptr进行内存偏移的方式</p><p><code>fninit</code>的粒度是<code>package</code>，如注释所述每个<code>package</code>的<code>init</code>函数需要做三件事</p><ul><li>初始化 当前 <code>package</code> 所依赖的 <code>package</code></li><li>初始化 当前 <code>package</code> 中所有需要初始化的变量</li><li>执行 用户所写的 <code>func init()</code></li></ul><pre><code class="language-go">func fninit(n []*Node) {nf := initOrder(n)var deps []*obj.LSym // initTask records for packages the current package depends onvar fns []*obj.LSym  // functions to call for package initialization// 将当前Package所import的package的信息加入到 deps 中for _, s := range types.InitSyms {deps = append(deps, s.Linksym())}// 对于所有需要初始化的变量，封装到package.init函数中，并加入到fns中if len(nf) &gt; 0 {lineno = nf[0].Pos // prolog/epilog gets line number of first init stmtinitializers := lookup(&quot;init&quot;)disableExport(initializers)fn := dclfunc(initializers, nod(OTFUNC, nil, nil))for _, dcl := range dummyInitFn.Func.Dcl {dcl.Name.Curfn = fn}fn.Func.Dcl = append(fn.Func.Dcl, dummyInitFn.Func.Dcl...)dummyInitFn.Func.Dcl = nil……fns = append(fns, initializers.Linksym())}// 对于用户所写的 func init 也加入到fns中// 用户所写的 func init 在之前的流程中已经将Name重置为 package.init.X 从0按顺序进行生成for i := 0; i &lt; renameinitgen; i++ {s := lookupN(&quot;init.&quot;, i)fn := asNode(s.Def).Name.Defnif fn.Nbody.Len() == 0 {continue}fns = append(fns, s.Linksym())}// 如果没有需要进行初始化的操作，则直接返回(按需生成packege..inittask)// 对于 main 和 runtime 包会继续向下走if len(deps) == 0 &amp;&amp; len(fns) == 0 &amp;&amp; localpkg.Name != &quot;main&quot; &amp;&amp; localpkg.Name != &quot;runtime&quot; {return // nothing to initialize}// 生成 packege..inittask 符号sym := lookup(&quot;.inittask&quot;)// 写入state字段，占用一个uintptr长度ot = duintptr(lsym, ot, 0) // state: not initialized yet// 写入deps 的长度字段，占用一个uintptr长度ot = duintptr(lsym, ot, uint64(len(deps)))// 写入fns 的长度字段，占用一个uintptr长度ot = duintptr(lsym, ot, uint64(len(fns)))// 写入deps字段详细信息，每条信息为一个 *inittaskfor _, d := range deps {ot = dsymptr(lsym, ot, d, 0)}// 写入fns字段详细信息，每条信息为一个 *func()for _, f := range fns {ot = dsymptr(lsym, ot, f, 0)}ggloblsym(lsym, int32(ot), obj.NOPTR)}</code></pre><h3 id="如何调试inittask">如何调试initTask</h3><p>Go 1.16中提供了<code>GODEBUG=inittrace</code>环境变量来进行initTask的调试</p><pre><code class="language-bash">&gt; GODEBUG=inittrace=1 ./gin-vue-admininit internal/bytealg @0.011 ms, 0 ms clock, 0 bytes, 0 allocsinit runtime @0.42 ms, 0.72 ms clock, 0 bytes, 0 allocsinit errors @1.7 ms, 0.18 ms clock, 0 bytes, 0 allocsinit math @1.9 ms, 0.13 ms clock, 0 bytes, 0 allocs……init os @4.5 ms, 0.38 ms clock, 4456 bytes, 20 allocsinit fmt @4.9 ms, 0.17 ms clock, 32 bytes, 2 allocsinit context @5.1 ms, 0.005 ms clock, 128 bytes, 4 allocsinit path/filepath @5.1 ms, 0.16 ms clock, 16 bytes, 1 allocs……init go.uber.org/zap @14 ms, 0.33 ms clock, 888 bytes, 15 allocsinit github.com/go-redis/redis/internal/pool @14 ms, 0.15 ms clock, 32 bytes, 2 allocsinit github.com/go-redis/redis @15 ms, 0.16 ms clock, 192 bytes, 6 allocsinit encoding/csv @15 ms, 0.15 ms clock, 80 bytes, 5 allocsinit github.com/spf13/pflag @15 ms, 0.014 ms clock, 272 bytes, 2 allocsinit github.com/spf13/afero/mem @16 ms, 0 ms clock, 48 bytes, 3 allocsinit regexp/syntax @16 ms, 0.19 ms clock, 7648 bytes, 9 allocs</code></pre><p>可以看到被初始化的<code>package</code>、初始化开始的时间、初始化过程的耗时、初始化过程中的内存分配大小、初始化过程中的内存分配次数的信息。</p><p>也可以看出初始化的顺序大致为 runtime -&gt; 标准库 -&gt; 第三方库</p><h3 id="end">End</h3><p>这篇文章没有魔改定制版，额外说个冷知识</p><p><code>initTask</code>在什么时候会被调用？</p><p>上方已经说了，会在<code>runtime.main</code> goroutine中，用户<code>main</code>函数执行之前，会手动调用执行runtime和main的初始化函数，并进行递归初始化。</p><p>后续还有没有其他地方会调用呢？</p><p>答案是 有，加载<code>plugin</code>的时候如果有需要执行的<code>inittask</code>，也会调用<code>doInit</code>来递归执行。</p><pre><code class="language-bash">$GOROOT/src/plugin/plugin_dlopen.gofunc open(name string) (*Plugin, error) {filepath := C.GoString((*C.char)(unsafe.Pointer(&amp;cPath[0])))var cErr *C.charh := C.pluginOpen((*C.char)(unsafe.Pointer(&amp;cPath[0])), &amp;cErr)if h == 0 {pluginsMu.Unlock()return nil, errors.New(`plugin.Open(&quot;` + name + `&quot;): ` + C.GoString(cErr))}initStr := make([]byte, len(pluginpath)+len(&quot;..inittask&quot;)+1) // +1 for terminating NULcopy(initStr, pluginpath)copy(initStr[len(pluginpath):], &quot;..inittask&quot;)initTask := C.pluginLookup(h, (*C.char)(unsafe.Pointer(&amp;initStr[0])), &amp;cErr)if initTask != nil {// NotedoInit(initTask)}……return p, nil}// doInit is defined in package runtime//go:linkname doInit runtime.doInitfunc doInit(t unsafe.Pointer) // t should be a *runtime.initTask</code></pre><p>这里先埋两个坑，<code>plugin</code>、<code>//go:linkname</code>，等后续再填吧。</p><p>简单来说</p><p><code>plugin</code>是一种可动态加载的插件机制，可以将Go的<code>main</code> <code>package</code>编译生成一个so动态库，提供了<code>plugin.Open</code>和<code>Lookup</code>方法，用来加载动态库和查找符号，对应的底层方法是<code>dlopen</code>和<code>dlsym</code>。</p><p><code>//go:linkname</code>可以用来引用Go Runtime的一些私有方法，或者其他package的私有方法，本质是会将对应的符号进行重定向操作。</p>]]>
                    </description>
                    <pubDate>Sun, 16 May 2021 00:52:29 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[go version 是如何实现的？]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/goversion</link>
                    <description>
                            <![CDATA[<h2 id="go-version是什么">go version是什么？</h2><ol><li><p>可以用来查看当前的<code>go</code>版本</p><pre><code class="language-bash">&gt; go versiongo version go1.16.2 darwin/amd64</code></pre></li><li><p>在<a href="https://golang.org/doc/go1.13">go1.13</a>之后，还支持对二进制和目录进行操作</p><blockquote><p>The <a href="https://golang.org/cmd/go/#hdr-Print_Go_version"><code>go</code> <code>version</code></a> command now accepts arguments naming executables and directories. When invoked on an executable, <code>go</code> <code>version</code> prints the version of Go used to build the executable. If the <code>-m</code> flag is used, <code>go</code> <code>version</code> prints the executable's embedded module version information, if available. When invoked on a directory, <code>go</code> <code>version</code> prints information about executables contained in the directory and its subdirectories.</p></blockquote><p>对于可执行二进制，会打印用于构建可执行文件的go版本。</p><p>如果使用了<code>-m</code>标志，则<code>go version</code>将打印可执行文件的嵌入式module版本信息（如果有）。</p><p>当在目录上调用go版本时，它会打印有关该目录及其子目录中包含的可执行文件的信息。</p></li></ol><h2 id="go-version是如何实现的">go version是如何实现的？</h2><p>go 二进制是通过 <code>$GOROOT/src/cmd/go/main.go</code>这个文件编译生成的。</p><p><code>go version</code>的主要实现逻辑在 <code>$GOROOT/src/cmd/go/internal/version/version.go</code></p><p>在Goland中可以这样设置进行调试该功能，这种方式同样适用于调试go的编译器等工具。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210514223438PFXh8T.png" alt="image-20210513235417160" /></p><p>除去注释和错误检查的后的代码如下</p><pre><code class="language-go">func runVersion(ctx context.Context, cmd *base.Command, args []string) {// go vervion后面无参数时的处理逻辑if len(args) == 0 {fmt.Printf(&quot;go version %s %s/%s\n&quot;, runtime.Version(), runtime.GOOS, runtime.GOARCH)return}// go version后面有参数时的处理逻辑for _, arg := range args {info, err := os.Stat(arg)if info.IsDir() {scanDir(arg)// 递归扫描文件夹} else {scanFile(arg, info, true) // 扫描二进制文件}}}</code></pre><h3 id="go-vervion后面无参数时的处理逻辑">go vervion后面无参数时的处理逻辑</h3><p>如上节所示，会打印三个信息：</p><ul><li>版本号 go1.16.2</li><li>操作系统 darwin</li><li>处理器架构 amd64</li></ul><p>后面两个是写在指定文件中的，跟随官方源码一起发布的，重点关注<code>runtime.Version()</code></p><pre><code class="language-go">// $GOROOT/src/runtime/extern.gofunc Version() string {return sys.TheVersion}// $GOROOT/src/runtime/internal/sys/zversion.go// Code generated by go tool dist; DO NOT EDIT.package sysconst TheVersion = `go1.16.2`const Goexperiment = ``const StackGuardMultiplierDefault = 1</code></pre><p>这样看来，这个也是固定写到文件中的？</p><p>别急，再观察一下，这个文件是不被git管理的，而且在<a href="https://github.com/golang/go/tree/master/src/runtime/internal/sys">官方代码库</a>中这个文件也是不存在的。</p><pre><code class="language-go">// $GOROOT/src/cmd/dist/build.go// gentab records how to generate some trivial files.var gentab = []struct {nameprefix stringgen        func(string, string)}{{&quot;zdefaultcc.go&quot;, mkzdefaultcc},{&quot;zosarch.go&quot;, mkzosarch},{&quot;zversion.go&quot;, mkzversion},{&quot;zcgo.go&quot;, mkzcgo},// not generated anymore, but delete the file if we see it{&quot;enam.c&quot;, nil},{&quot;anames5.c&quot;, nil},{&quot;anames6.c&quot;, nil},{&quot;anames8.c&quot;, nil},{&quot;anames9.c&quot;, nil},}// $GOROOT/src/cmd/dist/buildruntime.gofunc mkzversion(dir, file string) {var buf bytes.Bufferfmt.Fprintf(&amp;buf, &quot;// Code generated by go tool dist; DO NOT EDIT.\n&quot;)fmt.Fprintln(&amp;buf)fmt.Fprintf(&amp;buf, &quot;package sys\n&quot;)fmt.Fprintln(&amp;buf)fmt.Fprintf(&amp;buf, &quot;const TheVersion = `%s`\n&quot;, findgoversion())fmt.Fprintf(&amp;buf, &quot;const Goexperiment = `%s`\n&quot;, os.Getenv(&quot;GOEXPERIMENT&quot;))fmt.Fprintf(&amp;buf, &quot;const StackGuardMultiplierDefault = %d\n&quot;, stackGuardMultiplierDefault())writefile(buf.String(), file, writeSkipSame)}</code></pre><p>这个文件实际上是在编译go的过程中自动生成的。</p><p>和<code>TheVersion</code>相关的是<code>findgoversion</code>函数</p><pre><code class="language-go">// $GOROOT/src/cmd/dist/build.go// findgoversion determines the Go version to use in the version string.func findgoversion() string {  // 查找 $GOROOT/VERSION文件是否存在，如果存在直接返回该文件中的信息  // 该文件在正式有tag号的版本中都是存在的，但是对于master和一些实验性分支是没有的path := pathf(&quot;%s/VERSION&quot;, goroot)if isfile(path) {b := chomp(readfile(path))if b != &quot;&quot; {if strings.HasPrefix(b, &quot;devel&quot;) {// 看注释是交叉编译相关的处理，暂不关心if hostType := os.Getenv(&quot;META_BUILDLET_HOST_TYPE&quot;); strings.Contains(hostType, &quot;-cross&quot;) {fmt.Fprintf(os.Stderr, &quot;warning: changing VERSION from %q to %q\n&quot;, b, &quot;builder &quot;+hostType)b = &quot;builder &quot; + hostType}}return b}}// 这是一个缓存文件，生成该文件的逻辑在下方，避免每次都调用git去重新查找版本信息path = pathf(&quot;%s/VERSION.cache&quot;, goroot)if isfile(path) {return chomp(readfile(path))}// 预期$GOROOT应该是一个git仓库if !isGitRepo() {fatalf(&quot;FAILED: not a Git repo; must put a VERSION file in $GOROOT&quot;)}// 调用git获取当前分支branch := chomp(run(goroot, CheckExit, &quot;git&quot;, &quot;rev-parse&quot;, &quot;--abbrev-ref&quot;, &quot;HEAD&quot;))// What are the tags along the current branch?tag := &quot;devel&quot;precise := false// 如果当前是release-branch.XXX分支，寻找该分支上最近的一个tag号，但是当前仓库中release-branch分支也都是有VERSION这个文件的，理论上当前这是一个不会走到的if分支if strings.HasPrefix(branch, &quot;release-branch.&quot;) {tag, precise = branchtag(branch)}// 对于master等分支，使用git信息生成版本号if !precise {// Tag does not point at HEAD; add hash and date to version.tag += chomp(run(goroot, CheckExit, &quot;git&quot;, &quot;log&quot;, &quot;-n&quot;, &quot;1&quot;, &quot;--format=format: +%h %cd&quot;, &quot;HEAD&quot;))}// 写入版本信息到缓存文件，上方会优先读取缓存文件中的版本号writefile(tag, path, 0)return tag}</code></pre><p>对于master分支，<code>go version</code>的输出如下</p><pre><code class="language-bash">go version devel go1.17-4a7effa418 Wed Apr 28 14:01:59 2021 +0000 darwin/amd64</code></pre><h3 id="go-version后面有参数时的处理逻辑">go version后面有参数时的处理逻辑</h3><pre><code class="language-go">&gt; go version gin-vue-admingin-vue-admin: go1.16.2</code></pre><p><code>go version -m</code> 的输出，会将go mod的依赖也一些打印</p><pre><code class="language-bash">&gt; go version -m gin-vue-admingin-vue-admin: go1.16.2pathgin-vue-adminmodgin-vue-admin(devel)depgithub.com/360EntSecGroup-Skylar/excelize/v2v2.3.2h1:MHu5KWWt28FzRGQgc4Ryj/lZT/W/by4NvsnstbWwkkY=depgithub.com/Knetic/govaluatev3.0.1-0.20171022003610-9aa49832a739+incompatibleh1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=depgithub.com/KyleBanks/depthv1.2.1h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=depgithub.com/PuerkitoBio/purellv1.1.1……</code></pre><p>如何从文件中查找版本信息？</p><pre><code class="language-go">// $GOROOT/src/cmd/go/internal/version/version.gofunc scanFile(file string, info fs.FileInfo, mustPrint bool) {// 需要是一个可执行文件if !isExe(file, info) {if mustPrint {fmt.Fprintf(os.Stderr, &quot;%s: not executable file\n&quot;, file)}return}// 打开该文件x, err := openExe(file)if err != nil {if mustPrint {fmt.Fprintf(os.Stderr, &quot;%s: %v\n&quot;, file, err)}return}defer x.Close()// 查找version和module信息vers, mod := findVers(x)if vers == &quot;&quot; {if mustPrint {fmt.Fprintf(os.Stderr, &quot;%s: go version not found\n&quot;, file)}return}// 打印版本信息fmt.Printf(&quot;%s: %s\n&quot;, file, vers)// 如果开启了-m,打印module信息if *versionM &amp;&amp; mod != &quot;&quot; {fmt.Printf(&quot;\t%s\n&quot;, strings.ReplaceAll(mod[:len(mod)-1], &quot;\n&quot;, &quot;\n\t&quot;))}}// buildInfo section 的魔数信息var buildInfoMagic = []byte(&quot;\xff Go buildinf:&quot;)// findVers finds and returns the Go version and module version information// in the executable x.func findVers(x exe) (vers, mod string) {// Read the first 64kB of text to find the build info blob.text := x.DataStart() // 对于elf格式的文件，查找.go.buildinfo section的起始地址data, err := x.ReadData(text, 64*1024) // 从起始地址读取64KB信息if err != nil {return}// 找到 &quot;\xff Go buildinf:&quot; 所在位置for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[32:] {if len(data) &lt; 32 {return}}// Decode the blob.ptrSize := int(data[14]) // 读取指针大小bigEndian := data[15] != 0 // 大小端var bo binary.ByteOrderif bigEndian {bo = binary.BigEndian} else {bo = binary.LittleEndian}var readPtr func([]byte) uint64if ptrSize == 4 {readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) }} else {readPtr = bo.Uint64}// 从data[16:16+ptrSize] 读取记录version字符串metadata信息的地址 metadataAddr// 从metadataAddr中读取实际字符串的地址和长度信息// 根据上方读到的信息读取真实字符串vers = readString(x, ptrSize, readPtr, readPtr(data[16:]))if vers == &quot;&quot; {return}// 从data[16+ptrSize:16+2*ptrSize] 读取module字符串metadata信息的地址// 与上方读取version字符串的流程一致mod = readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:]))if len(mod) &gt;= 33 &amp;&amp; mod[len(mod)-17] == '\n' {// Strip module framing.mod = mod[16 : len(mod)-16]} else {mod = &quot;&quot;}return}// readString returns the string at address addr in the executable x.func readString(x exe, ptrSize int, readPtr func([]byte) uint64, addr uint64) string {// 从指定的Addr向后读取两个指针大小的数据hdr, err := x.ReadData(addr, uint64(2*ptrSize))if err != nil || len(hdr) &lt; 2*ptrSize {return &quot;&quot;}// 解析字符串真实地址dataAddr := readPtr(hdr)// 解析字符串的长度dataLen := readPtr(hdr[ptrSize:])// 读取字符串数据data, err := x.ReadData(dataAddr, dataLen)if err != nil || uint64(len(data)) &lt; dataLen {return &quot;&quot;}// 转换为string并返回return string(data)}// $GOROOT/src/encoding/binary/binary.go// 大端情况下，获取地址信息func (bigEndian) Uint64(b []byte) uint64 {_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808return uint64(b[7]) | uint64(b[6])&lt;&lt;8 | uint64(b[5])&lt;&lt;16 | uint64(b[4])&lt;&lt;24 |uint64(b[3])&lt;&lt;32 | uint64(b[2])&lt;&lt;40 | uint64(b[1])&lt;&lt;48 | uint64(b[0])&lt;&lt;56}// 小端情况下，获取地址信息func (littleEndian) Uint64(b []byte) uint64 {_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808return uint64(b[0]) | uint64(b[1])&lt;&lt;8 | uint64(b[2])&lt;&lt;16 | uint64(b[3])&lt;&lt;24 |uint64(b[4])&lt;&lt;32 | uint64(b[5])&lt;&lt;40 | uint64(b[6])&lt;&lt;48 | uint64(b[7])&lt;&lt;56}</code></pre><p>跟随上方流程，使用<code>readelf</code>和<code>objdump</code>来手动查找该信息</p><p>Note：只关注有用的信息，mac 的可执行文件为 Mach-O 而非 ELF，这里的内容是在Linux中操作。</p><ol><li><p>查看该二进制都有哪些Section</p><pre><code class="language-bash">&gt; readelf -S ./gin-vue-adminThere are 36 section headers, starting at offset 0x270:Section Headers:  [Nr] Name              Type             Address           Offset       Size              EntSize          Flags  Link  Info  Align  ……  [15] .gopclntab        PROGBITS         000000000135c160  00f5c160       000000000050f118  0000000000000000   A       0     0     32  [16] .go.buildinfo     PROGBITS         000000000186c000  0146c000       0000000000000020  0000000000000000  WA       0     0     16  [17] .dynamic          DYNAMIC          000000000186c020  0146c020       0000000000000130  0000000000000010  WA      10     0     8  ……</code></pre></li><li><p>Dump .go.buildinfo Section的信息</p><pre><code class="language-bash">&gt; objdump -s -j .go.buildinfo  ./gin-vue-admin./gin-vue-admin:     file format elf64-x86-64Contents of section .go.buildinfo: 186c000 ff20476f 20627569 6c64696e 663a0800  . Go buildinf:.. 186c010 f06fd002 00000000 3070d002 00000000  .o......0p......</code></pre><p>根据源码可知</p><ul><li>[0:14)区间内应为Magic信息 ：    <code>&quot;\xff Go buildinf:&quot;</code></li><li>第 15 个字符为指针大小：08，指针大小为8</li><li>第 16 个字符为大小端信息：00，不为0是大端，为0是小端，当前为小端</li><li>[16:16+ptrSize) 记录version字符串metadata信息的地址：ptrSize在上方已找出，为8，且为小端。原始信息为<code>f06fd002</code>，对应的地址信息应为 <code>0x02d06ff0</code></li><li>[16+ptrSize:16+2*ptrSize) 记录module字符串metadata信息的地址：ptrSize在上方已找出，为8，且为小端。原始信息为<code>3070d002</code>，对应的地址信息应为 <code>0x02d07030</code></li></ul></li><li><p>Dump 字符串metadata信息</p><p>从<code>0x02d06ff0</code>向后dump16个字节的信息，<code>0x02d06ff0 + 0x10 = 0x02d07000</code></p><pre><code class="language-bash">&gt; objdump -s --start-address 0x02d06ff0 --stop-address 0x02d07000 ./gin-vue-admin./gin-vue-admin:     file format elf64-x86-64Contents of section .data: 2d06ff0 95340b01 00000000 08000000 00000000  .4..............</code></pre><p>对应字符串的地址信息为<code>95340b01</code>，根据小端原则，字符串真实地址为<code>0x010b3495</code></p><p>对应字符串的长度信息为<code>08000000</code>，长度为8</p><pre><code class="language-bash">&gt; objdump -s --start-address  0x010b3495 --stop-address 0x010b349d ./gin-vue-admin./gin-vue-admin:     file format elf64-x86-64Contents of section .rodata: 10b3495 676f31 2e31362e 32                   go1.16.2</code></pre><p>获取到了版本信息为 <code>go1.16.2</code></p><p>对于module信息，方法一致</p><pre><code class="language-bash">&gt; objdump -s --start-address 0x02d07030 --stop-address 0x02d07040 ./gin-vue-admin./gin-vue-admin:     file format elf64-x86-64Contents of section .data: 2d07030 60e40f01 00000000 c0200000 00000000&gt; objdump -s --start-address 0x010fe460 --stop-address 0x1100520 ./gin-vue-adminontents of section .rodata: 10fe460 3077af0c 92740802 41e1c107 e6d618e6  0w...t..A....... 10fe470 70617468 0967696e 2d767565 2d61646d  path.gin-vue-adm 10fe480 696e0a6d 6f640967 696e2d76 75652d61  in.mod.gin-vue-a 10fe490 646d696e 09286465 76656c29 090a6465  dmin.(devel)..de 10fe4a0 70096769 74687562 2e636f6d 2f333630  p.github.com/360 10fe4b0 456e7453 65634772 6f75702d 536b796c  EntSecGroup-Skyl 10fe4c0 61722f65 7863656c 697a652f 76320976  ar/excelize/v2.v 10fe4d0 322e332e 32096831 3a4d4875 354b5757  2.3.2.h1:MHu5KWW 10fe4e0 74323846 7a524751 67633452 796a2f6c  t28FzRGQgc4Ryj/l 10fe4f0 5a542f57 2f627934 4e76736e 73746257  ZT/W/by4NvsnstbW 10fe500 776b6b59 3d0a6465 70096769 74687562  wkkY=.dep.github ……</code></pre></li></ol><p>该信息实际上是从二进制中读取的，那么还有最后一个问题，是何时写入的？</p><p>这里实际上是go的链接器做的事情了</p><pre><code class="language-go">// $GOROOT/src/cmd/link/internal/ld/main.go// Main is the main entry point for the linker code.func Main(arch *sys.Arch, theArch Arch) {// 开始写入buildinfo Sectionbench.Start(&quot;buildinfo&quot;)ctxt.buildinfo()}// $GOROOT/src/cmd/link/internal/ld/data.gofunc (ctxt *Link) buildinfo() {if ctxt.linkShared || ctxt.BuildMode == BuildModePlugin {// -linkshared and -buildmode=plugin get confused// about the relocations in go.buildinfo// pointing at the other data sections.// The version information is only available in executables.return}ldr := ctxt.loaders := ldr.CreateSymForUpdate(&quot;.go.buildinfo&quot;, 0)// On AIX, .go.buildinfo must be in the symbol table as// it has relocations.s.SetNotInSymbolTable(!ctxt.IsAIX())s.SetType(sym.SBUILDINFO)s.SetAlign(16)// The \xff is invalid UTF-8, meant to make it less likely// to find one of these accidentally.const prefix = &quot;\xff Go buildinf:&quot; // 14 bytes, plus 2 data bytes filled in belowdata := make([]byte, 32)// 写入Magiccopy(data, prefix)// 写入指针大小data[len(prefix)] = byte(ctxt.Arch.PtrSize)data[len(prefix)+1] = 0// 写入大小端信息if ctxt.Arch.ByteOrder == binary.BigEndian {data[len(prefix)+1] = 1}s.SetData(data)s.SetSize(int64(len(data)))r, _ := s.AddRel(objabi.R_ADDR)r.SetOff(16)r.SetSiz(uint8(ctxt.Arch.PtrSize))// 写入 runtime.buildVersion 符号的地址r.SetSym(ldr.LookupOrCreateSym(&quot;runtime.buildVersion&quot;, 0))r, _ = s.AddRel(objabi.R_ADDR)r.SetOff(16 + int32(ctxt.Arch.PtrSize))r.SetSiz(uint8(ctxt.Arch.PtrSize))// 写入 runtime.modinfo 符号的地址r.SetSym(ldr.LookupOrCreateSym(&quot;runtime.modinfo&quot;, 0))}// $GOROOT/src/runtime/proc.govar buildVersion = sys.TheVersion</code></pre><p>知道了原理能做什么？</p><pre><code>&gt; strip gin-vue-admin&gt; readelf -S ./gin-vue-adminThere are 27 section headers, starting at offset 0x2942388:Section Headers:  [Nr] Name              Type             Address           Offset       Size              EntSize          Flags  Link  Info  Align  [15] .go.buildinfo     PROGBITS         000000000186c000  0146c000       0000000000000020  0000000000000000  WA       0     0     16</code></pre><p>可以看到，在使用<code>strip</code>去除二进制的调试信息后，<code>.go.buildinfo Section</code>仍然是存在的。</p><p>那么如果你拿到了一个没有符号表的Go二进制，就可以通过上方的流程，来进行”逆向“，不过<code>go version</code>也可以直接操作去除符号信息的二进制，哈哈哈。</p><p>那么，就这？</p><h2 id="定制-go-版本信息">定制 Go 版本信息</h2><p>那么理解了原理，就可以开始魔改了，如果定制Go版本信息，可以在那里修改呢？</p><p>在 <code>$GOROOT/src/runtime/internal/sys/zversion.go</code>中修改？</p><p>这个文件每次重新编译都会重新生成的，所以是无法生效的。</p><p>在 <code>runVersion</code>中修改？</p><p>这个是依赖于运行 <code>go verison</code>时的go二进制的，可以改变<code>go version</code>无参数时的输出信息。</p><p>该信息不会被写入到二进制中，所以编译出的二进制，使用正常go发行版去检查版本信息时，无法达到预期的效果。</p><p>我选择在 <code>$GOROOT/src/cmd/dist/buildruntime.go</code> 的<code>mkzversion</code>中进行修改</p><pre><code class="language-go">func mkzversion(dir, file string) {var buf bytes.Bufferfmt.Fprintf(&amp;buf, &quot;// Code generated by go tool dist; DO NOT EDIT.\n&quot;)fmt.Fprintln(&amp;buf)fmt.Fprintf(&amp;buf, &quot;package sys\n&quot;)fmt.Fprintln(&amp;buf)// 修改这一行fmt.Fprintf(&amp;buf, &quot;const TheVersion = `%s Powered by LeonardWang`\n&quot;, findgoversion())fmt.Fprintf(&amp;buf, &quot;const Goexperiment = `%s`\n&quot;, os.Getenv(&quot;GOEXPERIMENT&quot;))fmt.Fprintf(&amp;buf, &quot;const StackGuardMultiplierDefault = %d\n&quot;, stackGuardMultiplierDefault())writefile(buf.String(), file, writeSkipSame)}</code></pre><pre><code class="language-bash">&gt; go versiongo version go1.16.2 Powered by LeonardWang darwin/amd64&gt; go build -o main ./main.go&gt; go version main main: go1.16.2 Powered by LeonardWang</code></pre><h2 id="参考资料">参考资料</h2><p><a href="https://segmentfault.com/a/1190000039227408">从 Go 的二进制文件中获取其依赖的模块信息</a></p>]]>
                    </description>
                    <pubDate>Fri, 14 May 2021 22:33:53 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[GODEBUG参数是如何实现的？]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/godebug</link>
                    <description>
                            <![CDATA[<p><code>GODEBUG</code> 支持如下的选项，还有一个<code>memprofilerate</code></p><pre><code class="language-go">var dbgvars = []dbgVar{{&quot;allocfreetrace&quot;, &amp;debug.allocfreetrace},{&quot;clobberfree&quot;, &amp;debug.clobberfree},{&quot;cgocheck&quot;, &amp;debug.cgocheck},{&quot;efence&quot;, &amp;debug.efence},{&quot;gccheckmark&quot;, &amp;debug.gccheckmark},{&quot;gcpacertrace&quot;, &amp;debug.gcpacertrace},{&quot;gcshrinkstackoff&quot;, &amp;debug.gcshrinkstackoff},{&quot;gcstoptheworld&quot;, &amp;debug.gcstoptheworld},{&quot;gctrace&quot;, &amp;debug.gctrace},{&quot;invalidptr&quot;, &amp;debug.invalidptr},{&quot;madvdontneed&quot;, &amp;debug.madvdontneed},{&quot;sbrk&quot;, &amp;debug.sbrk},{&quot;scavtrace&quot;, &amp;debug.scavtrace},{&quot;scheddetail&quot;, &amp;debug.scheddetail},{&quot;schedtrace&quot;, &amp;debug.schedtrace},{&quot;tracebackancestors&quot;, &amp;debug.tracebackancestors},{&quot;asyncpreemptoff&quot;, &amp;debug.asyncpreemptoff},{&quot;inittrace&quot;, &amp;debug.inittrace},}</code></pre><p>较常用的是<code>gctrace</code> 用来打印GC信息</p><p>可以通过 <code>export GODEBUG=gctrace=1 &amp;&amp; ./demo</code></p><p>或者 <code>GODEBUG=gctrace=1 ./demo</code></p><p>来开启<code>GODEBUG</code> 选项来运行程序</p><p><code>GODEBUG</code>还支持两个选项组合使用，比如说可以使用如下的方式开启gc打印和关闭异步抢占</p><p><code>GODEBUG=gctrace=1,asyncpreemptoff=1</code></p><h3 id="何时解析">何时解析</h3><p>在程序启动时，调用到<code>schedinit</code>函数时</p><pre><code class="language-go">// The bootstrap sequence is:////call osinit//call schedinit//make &amp; queue new G//call runtime·mstart//// The new G calls runtime·main.func schedinit() {    ……goargs()goenvs()parsedebugvars() // 在这个函数中进行解析……}</code></pre><h3 id="如何解析">如何解析</h3><p>其实就是在对字符串的基本操作</p><pre><code class="language-go">func parsedebugvars() {// 有一些默认开启的参数debug.cgocheck = 1debug.invalidptr = 1// 获取GODEBUG环境变量的内容，如果不为空时 进行解析for p := gogetenv(&quot;GODEBUG&quot;); p != &quot;&quot;; {field := &quot;&quot;// 先按 , 进行拆分i := index(p, &quot;,&quot;)if i &lt; 0 {field, p = p, &quot;&quot;} else {// 如果存在 “,”  获取第一个 “,” 前的内容，放置到 field 中field, p = p[:i], p[i+1:]}// 在field 中 按“=” 继续拆分i = index(field, &quot;=&quot;)if i &lt; 0 {continue}// 获取 “=” 前后的内容，存储到key和value中key, value := field[:i], field[i+1:]// Update MemProfileRate directly here since it// is int, not int32, and should only be updated// if specified in GODEBUG.// 对于 memprofilerate 特殊处理，设置memprofile的采样频率if key == &quot;memprofilerate&quot; {if n, ok := atoi(value); ok {MemProfileRate = n}} else {// 查找预定义的dbgvars中的内容，找到name与key值相同时，设置其对应的valuefor _, v := range dbgvars {if v.name == key {if n, ok := atoi32(value); ok {*v.value = n}}}}}    setTraceback(gogetenv(&quot;GOTRACEBACK&quot;))traceback_env = traceback_cache}</code></pre><p>所以只有在<code>dbgvars</code> 中预定义的参数才可以正确解析</p><p>解析<code>GODEBUG=gctrace=1</code>后，在runtime代码中，对于<code>if debug.gctrace &gt; 0</code>的代码，即可满足这个检查条件，进而进行打印gc信息</p><h3 id="如何添加自定义配置选项">如何添加自定义配置选项</h3><p>当然需要重新编译Go</p><p>如何从源码编译安装Go，<a href="https://golang.org/doc/install/source">官方教程</a></p><ul><li>Go从1.5开始已经实现自举，先从官方下载一份已经编译好的二进制发行版</li><li>解压，并将该路径设置为<code>GOROOT_BOOTSTRAP</code></li><li>从<a href="https://github.com/golang/go">git仓库</a>克隆go源码，并将该路径设置为<code>GOROOT</code></li><li>git checkout 到想要的branch或者tag</li><li><code>cd $GOROOT/src &amp;&amp; ./make.bash</code></li><li><code>go version</code> 进行验证版本号</li></ul><pre><code class="language-go">// $GOROOT\src\runtime\runtime1.govar debug struct {asyncpreemptoff    int32gocustom      int32}var dbgvars = []dbgVar{{&quot;asyncpreemptoff&quot;, &amp;debug.asyncpreemptoff},{&quot;gocustom&quot;, &amp;debug.gocustom},}// $GOROOT\src\runtime\proc.go// The main goroutine.func main() {fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtimeif debug.gocustom &gt;0 {println(&quot;Hello LeonardWang&quot;)}// 执行用户main函数fn()}</code></pre><p>效果如下，当设置了<code>GODEBUG=gocustom=1</code> 时，会先打印 <code>Hello LeonardWang</code></p><pre><code class="language-bash">&gt; cat main.gopackage mainimport &quot;fmt&quot;func main() {        fmt.Println(&quot;Hello World&quot;)}&gt; go build -o main ./main.go&gt; ./mainHello World&gt; GODEBUG=gocustom=1 ./mainHello LeonardWangHello World</code></pre><p>添加自定义的<code>GODEBUG</code>选项可以在runtime中记录关心的统计信息，在适当的时机进行输出分析。</p>]]>
                    </description>
                    <pubDate>Wed, 12 May 2021 22:53:28 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[一个print 0 的问题定位过程]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/print0</link>
                    <description>
                            <![CDATA[<p>注：本文章仅用来记录问题分析的过程，从实际使用上来说，该例子存在data  race，不应该依赖其执行结果。</p><p>该例子是在饶大的一次夜读分享中所展示的。</p><pre><code class="language-go">package mainimport (&quot;fmt&quot;&quot;runtime&quot;&quot;time&quot;)func main() {runtime.GOMAXPROCS(8)var x int64 = 0for i := 0; i &lt; 8; i++ {go func() { // 后续中没有特殊说明的goroutine都是该goroutinefor {x++}}()}time.Sleep(time.Second)fmt.Println(x)}</code></pre><p>该例子的本意是想验证Go的异步抢占机制。</p><p>在Go1.13版本中，由于设置了<code>runtime.GOMAXPROCS(8)</code>，随后又启动了8个死循环的Goroutine，所以main goroutine在调用<code>time.Sleep</code>“协作式”的让出执行权后，再无机会得到执行，会导致程序一直无法执行打印和退出。</p><p>在Go1.14版本中，由于加入了异步抢占，使得可以“暂停”死循环的Goroutine，使得main goroutine可以继续执行打印。</p><p>现象到这里都还符合预期，但是打印的值却稳定为0。</p><p>在main goroutine sleep的过程中，其他goroutine是会执行的，那为什么x的值没有变呢？</p><p>开始时，有以下几个怀疑点：</p><ul><li>goroutine 中的x是一个副本，与外层的x不是同一个变量</li><li>goroutine 没有得到执行</li><li>硬件层面，Cpu cache导致，x的值还没有被刷新到内存中</li></ul><p>那么如何确认呢？</p><p>goroutine 中的x是一个副本，与外层的x不是同一个变量？</p><p><code>go build -gcflags=&quot;-m=2&quot; ./main.go</code></p><p>关注如下几点：</p><p><code>main.func1 capturing by ref: x</code> main.func1是main中的匿名函数，即例子中 main函数所启动的goroutine，x被捕获，且是被引用捕获的</p><p><code>x escapes to heap</code> x 逃逸到了堆上</p><pre><code class="language-bash">./main.go:15:5: main.func1 capturing by ref: x (addr=true assign=true width=8)./main.go:11:6: x escapes to heap:./main.go:11:6: moved to heap: x./main.go:20:13: x escapes to heap</code></pre><p>x在goroutine中是被引用捕获的而非值捕获，所以并不是发生了拷贝导致的。</p><p>goroutine 没有得到执行？</p><p>这个其实确认的方式有很多，可以使用 go trace 进行确认，下图可以看出main.func1 是在多个P中并行执行的。</p><p><img src="https://imghost.leonard.wang//uPic/image-20210502102223AUkiMy.png" alt="image-20210502102222836" /></p><p>硬件层面，Cpu cache导致，x的值还没有被刷新到内存中？</p><p>这个其实细想下来原理上说不通，假如机器只有8个核心，那么8个goroutine占据了8个核心，main goroutine结束时总会在这8个核心中的一个，会公用同一个cache，也不会为0。</p><p>而且通过加大goroutine数量和加大睡眠时间，输出结果仍稳定为0。</p><p>看起来上述的怀疑点都不能说通，接下来搬出大杀器：汇编。</p><p><code>go build -gcflags=&quot;-m=2&quot; ./main.go</code>或者先编译为二进制，再反汇编都可以。</p><p>仅关注main.func1 的汇编代码</p><pre><code class="language-bash">&quot;&quot;.main.func1 STEXT nosplit size=8 args=0x8 locals=0x0 funcid=0x00x0000 00000 (main.go:19)     TEXT    &quot;&quot;.main.func1(SB), NOSPLIT|ABIInternal, $0-80x0000 00000 (main.go:19)     FUNCDATA        $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)0x0000 00000 (main.go:19)     FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0000 00000 (main.go:20)     JMP     20x0002 00002 (main.go:21)     JMP     40x0004 00004 (main.go:21)     JMP     60x0006 00006 (main.go:21)     JMP     20x0000 eb 00 eb 00 eb 00 eb fa</code></pre><p>FUNCDATA是编译器插入的GC相关的伪指令，不会被编译到二进制中，在反汇编代码中是不存在的，先忽略。</p><p>可以看到汇编代码中仅有4条跳转(JMP)指令，而没有任何执行加法(ADD)的汇编执行</p><p>大概的执行流程如下，第一行跳转到第二行，第二行跳转到第三行，第三行又跳转到第一行，如此往复。</p><pre><code>0x0000 00000 (main.go:20)   |---&gt; JMP     2 ----                            |                  |0x0002 00002 (main.go:21)   |     JMP     4 &lt;---                            |             | ----                            |                  |0x0004 00004 (main.go:21)   |     JMP     6 &lt;---                            |             | ----                            |                  |0x0006 00006 (main.go:21)   ----  JMP     2 &lt;---</code></pre><p>那么可以确定了，goroutine虽然在运行，但是却没有做”正经事“加法。</p><p>编译过程中将该加法操作优化掉了，所以和前面的现象也就对得上了。</p><p>至此，该问题的根因已经查清楚了，编译器做了优化，<strong>没有实际执行++操作</strong>，所以无论main goroutine等待多久，输出仍稳定为0。</p><p>问题后续：</p><p>作者认为这是一个“错误的”优化，<a href="https://github.com/golang/go/issues/45826">官方的答复</a>如下</p><blockquote><p>A program with a data race in it is not a valid Go program and it could do anything. In this case, it look like the compiler decided that it is allowed to remove the writes to <code>x</code>, so the program will print 0. We usually don't consider this kind of issues (of a racy program) as bugs.</p></blockquote><p>该例子存在数据竞争，即多个线程同时对x进行读写操作，但是没有做任何同步保证，Go项目中对于这种data rece不做任何保证，看起来编译器决定允许删除对x的写入，因此程序将输出0，这并不被认为是一个bug。</p><p>如何进行检测rece呢？Go中提供了 data race的检测机制，说明详见 <a href="https://blog.golang.org/race-detector">https://blog.golang.org/race-detector</a><br />使用的方式有如下几种：</p><blockquote><pre><code>$ go test -race mypkg    // test the package$ go run -race mysrc.go  // compile and run the program$ go build -race mycmd   // build the command$ go install -race mypkg // install the package</code></pre></blockquote><p>经检测，该例子确实存在 data race，但是由于插入了race检测的桩代码，所以输出结果也并不为0了。</p><pre><code class="language-bash">go run -race ./main.go                                                                                                                        [49df10c] ==================WARNING: DATA RACERead at 0x00c00013a058 by goroutine 9:  main.main.func1()      /Users/leonardwang//main.go:21 +0x38Previous write at 0x00c00013a058 by goroutine 8:  main.main.func1()      /Users/leonardwang/main.go:21 +0x4eGoroutine 9 (running) created at:  main.main()      /Users/leonardwang/main.go:19 +0x124Goroutine 8 (running) created at:  main.main()      /Users/leonardwang/main.go:19 +0x124==================16328120Found 1 data race(s)exit status 66</code></pre>]]>
                    </description>
                    <pubDate>Sun, 02 May 2021 10:55:54 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[如何完整复制一个Git项目]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/duplicating-git-repository</link>
                    <description>
                            <![CDATA[<h2 id="克隆项目">克隆项目</h2><pre><code class="language-bash">git clone --bare https://github.com/exampleuser/old-repository.git</code></pre><h2 id="进入文件夹-push到新仓库">进入文件夹 push到新仓库</h2><pre><code class="language-bash">$ cd old-repository.git$ git push --mirror https://github.com/exampleuser/new-repository.git</code></pre><h2 id="参考链接">参考链接</h2><p><a href="https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/duplicating-a-repository">https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/duplicating-a-repository</a></p>]]>
                    </description>
                    <pubDate>Wed, 10 Mar 2021 01:04:26 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[LockOSThread实现原理]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/lockosthread</link>
                    <description>
                            <![CDATA[<p><code>$GOROOT\src\runtime\proc.go</code></p><pre><code class="language-go">runtime.LockOSThread()runtime.UnlockOSThread()</code></pre><p>其实注释中比较详细了，这里简化说下</p><ul><li>将当前的G 和当前的 操作系统线程 M绑定</li><li>绑定后，G只能在这个M上运行，这个M也只能运行这个G</li><li>解除绑定的方法是，调用 与LockOSThread次数相同的 UnlockOSThread</li><li>如果处于LockOSThread的G退出了，那么与其绑定的M会被退出</li><li>如果在任何init函数中调用LockOSThread，会导致main函数和M绑定（因为runtime.main也是一个Goroutine，这个G会先执行init函数，并调用用户的main函数）</li><li>主要用途是在依赖 每线程独立状态（per-thread state）时，进行LockOSThread</li><li>ps：仍然是需要P去执行的，可以重复LockOSThread</li></ul><pre><code class="language-go">func LockOSThread() {if atomic.Load(&amp;newmHandoff.haveTemplateThread) == 0 &amp;&amp; GOOS != &quot;plan9&quot; {startTemplateThread()}_g_ := getg()    // 将M的lockedExt字段计数加一_g_.m.lockedExt++if _g_.m.lockedExt == 0 {_g_.m.lockedExt--panic(&quot;LockOSThread nesting overflow&quot;)}dolockOSThread()}func dolockOSThread() {if GOARCH == &quot;wasm&quot; {return // no threads on wasm yet}_g_ := getg()    // 设置M的lockedg字段为G_g_.m.lockedg.set(_g_)    // 设置G的lockedm字段为M_g_.lockedm.set(_g_.m)}func UnlockOSThread() {_g_ := getg()    // 如果当前没有被锁定，直接返回if _g_.m.lockedExt == 0 {return}    // 将M的lockedExt字段计数减一_g_.m.lockedExt--dounlockOSThread()}func dounlockOSThread() {if GOARCH == &quot;wasm&quot; {return // no threads on wasm yet}_g_ := getg()    // 如果lockedExt仍有绑定计数时，也直接返回（多次LockOSThread，没有完全解锁）if _g_.m.lockedInt != 0 || _g_.m.lockedExt != 0 {return}_g_.m.lockedg = 0// 清空M中的lockedg标志_g_.lockedm = 0// 清空G中的lockedm标志}</code></pre><p>继续分析，如果做到“G只能在这个M上运行，这个M也只能运行这个G”</p><p>LockOSThread时设置了两个状态 m.lockedg 、 g.lockedm，在schedule中会使用这两个字段进行判断</p><pre><code class="language-go">func schedule() {if _g_.m.lockedg != 0 {// 如果一个 LockOSThread 的M进入调度stoplockedm()// 停止当前M，“死等”与其绑定的Gexecute(_g_.m.lockedg.ptr(), false) // 执行与其绑定的G.}    ……top:    ……if gp.lockedm != 0 { // 如果一个 LockOSThread的G进入调度// 将当前M的P交出给 与G绑定的M，去运行这个G，唤醒与G绑定的M，然后停止当前Mstartlockedm(gp)goto top}execute(gp, inheritTime)}func stoplockedm() {_g_ := getg()if _g_.m.p != 0 {// Schedule another M to run this p._p_ := releasep()handoffp(_p_)}incidlelocked(1)// Wait until another thread schedules lockedg again.notesleep(&amp;_g_.m.park)// 等待被唤醒    ------------------------------------noteclear(&amp;_g_.m.park)// 被唤醒status := readgstatus(_g_.m.lockedg.ptr())acquirep(_g_.m.nextp.ptr())// 获取P_g_.m.nextp = 0    // 回到调度，执行与其绑定的G}func startlockedm(gp *g) {_g_ := getg()mp := gp.lockedm.ptr()// directly handoff current P to the locked mincidlelocked(-1)_p_ := releasep()mp.nextp.set(_p_)// 将当前P直接传递给 与G绑定的M，让其去运行notewakeup(&amp;mp.park) // 唤醒与G绑定的M（在上方 notesleep(&amp;_g_.m.park) 中等待）stopm()}</code></pre><p>如何做到 “如果处于LockOSThread的G退出了，那么与其绑定的M会被退出”<br />这里参考 qcrao的一篇文章<a href="https://mp.weixin.qq.com/s/kwKqrT4BoeheM9MvSh-rLw">千难万险 —— goroutine 从生到死（六）</a></p><blockquote><p>在创建G的时候，会在栈底埋入<code>goexit</code>，G退出时，会自动跳到<code>goexit</code>中，并进一步跳转到<code>goexit0</code>，正常情况下会再次进入调度循环。</p></blockquote><pre><code class="language-go">func goexit0(gp *g) {_g_ := getg()locked := gp.lockedm != 0 // 先缓存G是否被 LockOSThreadgp.lockedm = 0// 清空LockOSThread标志_g_.m.lockedg = 0 // 清空LockOSThread标志    ……dropg()gfput(_g_.m.p.ptr(), gp)if locked {        // 如果是G退出时仍处于LockOSThread状态        // 直接让M退出，而不是进入runtime调度线程池（这里应该只是个策略选择问题，这个M中可能仍缓存了一些 “每线程独立状态”，所以让其退出避免复用时出现冲突）        // 调用gogo(&amp;_g_.m.g0.sched) 后，会回到 启动M时的mstart函数，进而触发结束当前M的流程if GOOS != &quot;plan9&quot; { // See golang.org/issue/22227.gogo(&amp;_g_.m.g0.sched)} else {// Clear lockedExt on plan9 since we may end up re-using// this thread._g_.m.lockedExt = 0}}schedule()}</code></pre>]]>
                    </description>
                    <pubDate>Sun, 03 Jan 2021 23:30:24 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Goland配置]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/goland</link>
                    <description>
                            <![CDATA[<h1 id="显示local-changes">显示Local Changes</h1><p>View-&gt;Tool Windows-&gt;Commit<br /><img src="http://imghost.leonard.wang/halo/image_1608477318401.png" alt="image.png" /><br />如果想在git页中显示Local Changes<br />File-&gt;Settings-&gt;Version Control-&gt;Commit<br /><strong>取消勾选</strong>Use non-modal commit interface<br /><img src="http://imghost.leonard.wang/halo/image_1608477487204.png" alt="image.png" /></p>]]>
                    </description>
                    <pubDate>Sun, 20 Dec 2020 23:13:44 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Golang信号处理]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/golang-signal</link>
                    <description>
                            <![CDATA[<h2 id="linux信号处理">Linux信号处理</h2><p>Linux 上也存在不可靠信号和可靠信号的 概念。其中不可靠信号可能丢失，多次发送相同的信号只能收到一次，取值从 1 至 31； 可靠信号则可以进行排队，取值从 32 至 64。</p><p><a href="https://www.cnblogs.com/coding-my-life/p/4782529.html">https://www.cnblogs.com/coding-my-life/p/4782529.html</a></p><p><a href="https://www.cnblogs.com/cobbliu/p/5592659.html">https://www.cnblogs.com/cobbliu/p/5592659.html</a></p><h2 id="go中的信号处理">Go中的信号处理</h2><p>参考：<a href="https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/signal/">https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/signal/</a></p><p>遗留问题：</p><ul><li>如果保证连续发送两个相同信号，信号不丢失的？</li><li>对于可靠信号和不可靠信号是否有不同处理？</li><li>不同<code>M</code>所监听的信号<code>mask</code>需要保持完全一致吗？</li></ul><p>由于对于Go程序员来说，并不感知线程（M）的存在，所以Go中的程序接管了操作系统的信号，并进行转发处理。</p><p>信号处理主要函数</p><pre><code class="language-go">//go:nowritebarrierrecfunc sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {_g_ := getg()c := &amp;sigctxt{info, ctxt}if sig == _SIGPROF {sigprof(c.sigpc(), c.sigsp(), c.siglr(), gp, _g_.m)return}if sig == _SIGTRAP &amp;&amp; testSigtrap != nil &amp;&amp; testSigtrap(info, (*sigctxt)(noescape(unsafe.Pointer(c))), gp) {return}if sig == _SIGUSR1 &amp;&amp; testSigusr1 != nil &amp;&amp; testSigusr1(gp) {return}// 抢占调度if sig == sigPreempt {// Might be a preemption signal.doSigPreempt(gp, c)// Even if this was definitely a preemption signal, it// may have been coalesced with another signal, so we// still let it through to the application.}flags := int32(_SigThrow)if sig &lt; uint32(len(sigtable)) {flags = sigtable[sig].flags}if flags&amp;_SigPanic != 0 &amp;&amp; gp.throwsplit {// We can't safely sigpanic because it may grow the// stack. Abort in the signal handler instead.flags = (flags &amp;^ _SigPanic) | _SigThrow}if isAbortPC(c.sigpc()) {// On many architectures, the abort function just// causes a memory fault. Don't turn that into a panic.flags = _SigThrow}if c.sigcode() != _SI_USER &amp;&amp; flags&amp;_SigPanic != 0 {// The signal is going to cause a panic.// Arrange the stack so that it looks like the point// where the signal occurred made a call to the// function sigpanic. Then set the PC to sigpanic.// Have to pass arguments out of band since// augmenting the stack frame would break// the unwinding code.gp.sig = siggp.sigcode0 = uintptr(c.sigcode())gp.sigcode1 = uintptr(c.fault())gp.sigpc = c.sigpc()c.preparePanic(sig, gp)return}    // 对用户注册的信号进行转发if c.sigcode() == _SI_USER || flags&amp;_SigNotify != 0 {if sigsend(sig) {return}}// 设置为可忽略的用户信号if c.sigcode() == _SI_USER &amp;&amp; signal_ignored(sig) {return}    // 正常情况在这里已返回，下方是kill、throw、crash等if flags&amp;_SigKill != 0 {dieFromSignal(sig)}if flags&amp;_SigThrow == 0 {return}_g_.m.throwing = 1_g_.m.caughtsig.set(gp)if crashing == 0 {startpanic_m()}if sig &lt; uint32(len(sigtable)) {print(sigtable[sig].name, &quot;\n&quot;)} else {print(&quot;Signal &quot;, sig, &quot;\n&quot;)}print(&quot;PC=&quot;, hex(c.sigpc()), &quot; m=&quot;, _g_.m.id, &quot; sigcode=&quot;, c.sigcode(), &quot;\n&quot;)if _g_.m.lockedg != 0 &amp;&amp; _g_.m.ncgo &gt; 0 &amp;&amp; gp == _g_.m.g0 {print(&quot;signal arrived during cgo execution\n&quot;)gp = _g_.m.lockedg.ptr()}print(&quot;\n&quot;)level, _, docrash := gotraceback()if level &gt; 0 {goroutineheader(gp)tracebacktrap(c.sigpc(), c.sigsp(), c.siglr(), gp)if crashing &gt; 0 &amp;&amp; gp != _g_.m.curg &amp;&amp; _g_.m.curg != nil &amp;&amp; readgstatus(_g_.m.curg)&amp;^_Gscan == _Grunning {// tracebackothers on original m skipped this one; trace it now.goroutineheader(_g_.m.curg)traceback(^uintptr(0), ^uintptr(0), 0, _g_.m.curg)} else if crashing == 0 {tracebackothers(gp)print(&quot;\n&quot;)}dumpregs(c)}if docrash {crashing++if crashing &lt; mcount()-int32(extraMCount) {// There are other m's that need to dump their stacks.// Relay SIGQUIT to the next m by sending it to the current process.// All m's that have already received SIGQUIT have signal masks blocking// receipt of any signals, so the SIGQUIT will go to an m that hasn't seen it yet.// When the last m receives the SIGQUIT, it will fall through to the call to// crash below. Just in case the relaying gets botched, each m involved in// the relay sleeps for 5 seconds and then does the crash/exit itself.// In expected operation, the last m has received the SIGQUIT and run// crash/exit and the process is gone, all long before any of the// 5-second sleeps have finished.print(&quot;\n-----\n\n&quot;)raiseproc(_SIGQUIT)usleep(5 * 1000 * 1000)}crash()}printDebugLog()exit(2)}</code></pre><p>函数 <code>sigsend</code> 会将用户信号发送到信号队列 <code>sig</code> 中，将<code>sig.mask</code>对应槽位设置标志位。</p><p><code>$GOROOT\src\runtime\sigqueue.go</code></p><pre><code class="language-go">var sig struct {note       notemask       [(_NSIG + 31) / 32]uint32wanted     [(_NSIG + 31) / 32]uint32ignored    [(_NSIG + 31) / 32]uint32recv       [(_NSIG + 31) / 32]uint32state      uint32delivering uint32inuse      bool}</code></pre><pre><code class="language-go">// sigsend delivers a signal from sighandler to the internal signal delivery queue.// It reports whether the signal was sent. If not, the caller typically crashes the program.// It runs from the signal handler, so it's limited in what it can do.func sigsend(s uint32) bool {bit := uint32(1) &lt;&lt; uint(s&amp;31)if !sig.inuse || s &gt;= uint32(32*len(sig.wanted)) {return false}atomic.Xadd(&amp;sig.delivering, 1)// We are running in the signal handler; defer is not available.if w := atomic.Load(&amp;sig.wanted[s/32]); w&amp;bit == 0 {atomic.Xadd(&amp;sig.delivering, -1)return false}// Add signal to outgoing queue.for {mask := sig.mask[s/32]if mask&amp;bit != 0 {atomic.Xadd(&amp;sig.delivering, -1)return true // signal already in queue}if atomic.Cas(&amp;sig.mask[s/32], mask, mask|bit) {break}}// Notify receiver that queue has new bit.Send:for {switch atomic.Load(&amp;sig.state) {default:throw(&quot;sigsend: inconsistent state&quot;)case sigIdle:if atomic.Cas(&amp;sig.state, sigIdle, sigSending) {break Send}case sigSending:// notification already pendingbreak Sendcase sigReceiving:if atomic.Cas(&amp;sig.state, sigReceiving, sigIdle) {if GOOS == &quot;darwin&quot; {sigNoteWakeup(&amp;sig.note)break Send}notewakeup(&amp;sig.note)break Send}}}atomic.Xadd(&amp;sig.delivering, -1)return true}</code></pre><p>信号接收</p><pre><code class="language-go">// Called to receive the next queued signal.// Must only be called from a single goroutine at a time.//go:linkname signal_recv os/signal.signal_recvfunc signal_recv() uint32 {for {// Serve any signals from local copy.for i := uint32(0); i &lt; _NSIG; i++ {if sig.recv[i/32]&amp;(1&lt;&lt;(i&amp;31)) != 0 {sig.recv[i/32] &amp;^= 1 &lt;&lt; (i &amp; 31)return i}}// Wait for updates to be available from signal sender.Receive:for {switch atomic.Load(&amp;sig.state) {default:throw(&quot;signal_recv: inconsistent state&quot;)case sigIdle:if atomic.Cas(&amp;sig.state, sigIdle, sigReceiving) {if GOOS == &quot;darwin&quot; {sigNoteSleep(&amp;sig.note)break Receive}notetsleepg(&amp;sig.note, -1)noteclear(&amp;sig.note)break Receive}case sigSending:if atomic.Cas(&amp;sig.state, sigSending, sigIdle) {break Receive}}}// Incorporate updates from sender into local copy.        // 从sig.mask中转移到sig.recv中，并清零for i := range sig.mask {sig.recv[i] = atomic.Xchg(&amp;sig.mask[i], 0)}}}</code></pre><p><code>signal_recv</code>会在<code>os/signal</code>中被调用</p><p><code>os/signal</code></p><p>初始化函数</p><pre><code class="language-go">func init() {signal_enable(0) // first call - initializewatchSignalLoop = loop}</code></pre><p>循环处理接收到的信号</p><p><code>signal_recv</code>对应上方信号接收函数，通过<code>go:linkname</code> 进行链接</p><pre><code class="language-go">func loop() {for {process(syscall.Note(signal_recv()))}}</code></pre><p>对于收到的信号，转到至对应的<code>channel</code>（<code>Notify</code>时传入的参数）</p><pre><code class="language-go">func process(sig os.Signal) {n := signum(sig)if n &lt; 0 {return}handlers.Lock()defer handlers.Unlock()for c, h := range handlers.m {if h.want(n) {// send but do not block for itselect {case c &lt;- sig:default:}}}// Avoid the race mentioned in Stop.for _, d := range handlers.stopping {if d.h.want(n) {select {case d.c &lt;- sig:default:}}}}</code></pre><pre><code class="language-go">func Notify(c chan&lt;- os.Signal, sig ...os.Signal) {if c == nil {panic(&quot;os/signal: Notify using nil channel&quot;)}    // 启动后台转发goroutinewatchSignalLoopOnce.Do(func() {if watchSignalLoop != nil {go watchSignalLoop()}})handlers.Lock()defer handlers.Unlock()// 将channel和对应要处理的的信号，存储到map中h := handlers.m[c]if h == nil {if handlers.m == nil {handlers.m = make(map[chan&lt;- os.Signal]*handler)}h = new(handler) // handler是一个存储mask的结构体handlers.m[c] = h}add := func(n int) {if n &lt; 0 {return}if !h.want(n) {h.set(n)            // 如果该信号是第一次监听，会调用enableSignalif handlers.ref[n] == 0 {enableSignal(n)}handlers.ref[n]++}}// 如果为空，监听所有信号？if len(sig) == 0 {for n := 0; n &lt; numSig; n++ {add(n)}} else {        // 设置handler对应的maskfor _, s := range sig {add(signum(s))}}}</code></pre><pre><code>func enableSignal(sig int) {signal_enable(uint32(sig))}</code></pre><pre><code class="language-go">// Must only be called from a single goroutine at a time.//go:linkname signal_enable os/signal.signal_enablefunc signal_enable(s uint32) {if !sig.inuse {// The first call to signal_enable is for us// to use for initialization. It does not pass// signal information in m.// 设置信号监听标识sig.inuse = true // enable reception of signals; cannot disableif GOOS == &quot;darwin&quot; {sigNoteSetup(&amp;sig.note)return}noteclear(&amp;sig.note)return}if s &gt;= uint32(len(sig.wanted)*32) {return}w := sig.wanted[s/32]w |= 1 &lt;&lt; (s &amp; 31)atomic.Store(&amp;sig.wanted[s/32], w)i := sig.ignored[s/32]i &amp;^= 1 &lt;&lt; (s &amp; 31)atomic.Store(&amp;sig.ignored[s/32], i)sigenable(s)}</code></pre><pre><code class="language-go">// sigenable enables the Go signal handler to catch the signal sig.// It is only called while holding the os/signal.handlers lock,// via os/signal.enableSignal and signal_enable.func sigenable(sig uint32) {if sig &gt;= uint32(len(sigtable)) {return}// SIGPROF is handled specially for profiling.if sig == _SIGPROF {return}t := &amp;sigtable[sig]if t.flags&amp;_SigNotify != 0 {ensureSigM()enableSigChan &lt;- sig&lt;-maskUpdatedChanif atomic.Cas(&amp;handlingSig[sig], 0, 1) {atomic.Storeuintptr(&amp;fwdSig[sig], getsig(sig))            // 重新设置要监听的信号？setsig(sig, funcPC(sighandler))}}}</code></pre><p>这里会确保，后台一定有一个<code>M</code>（虽然启动的是一个<code>goroutine</code>，但是调用了<code>LockOSThread</code>，所以这个<code>G</code>是独占一个<code>M</code>的）会监听所有需要监听的信号。</p><pre><code class="language-go">// ensureSigM starts one global, sleeping thread to make sure at least one thread// is available to catch signals enabled for os/signal.func ensureSigM() {if maskUpdatedChan != nil {return}maskUpdatedChan = make(chan struct{})disableSigChan = make(chan uint32)enableSigChan = make(chan uint32)go func() {// Signal masks are per-thread, so make sure this goroutine stays on one// thread.LockOSThread()defer UnlockOSThread()// The sigBlocked mask contains the signals not active for os/signal,// initially all signals except the essential. When signal.Notify()/Stop is called,// sigenable/sigdisable in turn notify this thread to update its signal// mask accordingly.sigBlocked := sigset_allfor i := range sigtable {if !blockableSig(uint32(i)) {sigdelset(&amp;sigBlocked, i)}}sigprocmask(_SIG_SETMASK, &amp;sigBlocked, nil)for {select {case sig := &lt;-enableSigChan:if sig &gt; 0 {sigdelset(&amp;sigBlocked, int(sig))}case sig := &lt;-disableSigChan:if sig &gt; 0 &amp;&amp; blockableSig(sig) {sigaddset(&amp;sigBlocked, int(sig))}}sigprocmask(_SIG_SETMASK, &amp;sigBlocked, nil)maskUpdatedChan &lt;- struct{}{}}}()}</code></pre><pre><code class="language-go">//go:nosplit//go:nowritebarrierrecfunc sigprocmask(how int32, new, old *sigset) {rtsigprocmask(how, new, old, int32(unsafe.Sizeof(*new)))}</code></pre><pre><code>TEXT runtime·rtsigprocmask(SB),NOSPLIT,$0-28MOVLhow+0(FP), DIMOVQnew+8(FP), SIMOVQold+16(FP), DXMOVLsize+24(FP), R10MOVL$SYS_rt_sigprocmask, AXSYSCALLCMPQAX, $0xfffffffffffff001JLS2(PC)MOVL$0xf1, 0xf1  // crashRET</code></pre>]]>
                    </description>
                    <pubDate>Fri, 11 Dec 2020 23:53:34 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[[译] 添加一个新语句到Golang编译器内部-第二部分]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/go-compiler-part2</link>
                    <description>
                            <![CDATA[<p><a href="https://eli.thegreenplace.net/2019/go-compiler-internals-adding-a-new-statement-to-go-part-2/">原文链接</a></p><p>这是探讨Go编译器的两部分系列文章中的第二篇。 在<a href="https://www.jianshu.com/p/e020603c5327">第一部分</a>中，我们通过构建定制版本的编译器向Go语言添加了一条新语句。 为此，我们按照此图介绍了编译器的前五个阶段：</p><p><img src="http://imghost.leonard.wang/uPic/image-20201114172723JSluLd.png" alt="Go gc compiler flow" /></p><p>在&quot;rewrite AST&quot;阶段，最终将<code>until</code>  降级(lower)到 <code>for</code>；具体来说，在编译器进行<code>SSA</code>转换和代码生成之前，在<code>gc/walk.go</code>中，   已经完成了对<code>until</code>的转换。</p><p>在本部分中，我们将尝试另外一种方式--在编译器的<code>SSA</code>和代码生成阶段中处理新增的<code>until</code>语句。</p><h2 id="ssa"><code>SSA</code></h2><p>在<code>gc</code>编译器运行 <code>walk</code>转换后，会调用<code>gc/ssa.go</code> 文件中的<code>buildssa</code> 将AST转换为<a href="https://zh.wikipedia.org/wiki/%E9%9D%99%E6%80%81%E5%8D%95%E8%B5%8B%E5%80%BC%E5%BD%A2%E5%BC%8F">静态单分配（SSA）形式</a>的新中间表示（IR）。</p><p><code>SSA</code>(static single assignment form)是什么意思，为什么编译器要这样做?让我们从第一个问题开始;我推荐阅读上面链接的SSA维基百科页面和其他资源，但这里是一个快速解释。</p><p>静态单一分配意味着IR中分配的每个变量仅分配一次。 考虑以下伪IR：</p><pre><code>x = 1y = 7// do stuff with x and yx = yy = func()// do more stuff with x and y</code></pre><p>这不是SSA，因为名称<code>x</code>和<code>y</code>被分配了多次。 如果将此代码段转换为SSA，我们可能会得到类似以下内容的信息：</p><pre><code>x = 1y = 7// do stuff with x and yx_1 = yy_1 = func()// do more stuff with x_1 and y_1</code></pre><p>注意每个分配如何获得唯一的变量名。 当<code>x</code>重新分配了另一个值时，将创建一个新名称<code>x_1</code>。 您可能想知道这在一般情况下是如何工作的……这样的代码会发生什么呢</p><pre><code>x = 1if condition: x = 2use(x)</code></pre><p>如果我们简单地将第二个赋值重命名为<code>x_1 = 2</code>，那么<code>use</code>将使用什么值？  <code>x</code>或<code>x_1</code>或...？</p><p>为了处理这种重要情况，<code>SSA</code>形式<code>的IR</code>具有特殊的<code>phi</code>（originally <em>phony</em>）功能，可根据其来自的代码路径来选择一个值。 它看起来像这样：</p><p><img src="http://imghost.leonard.wang/uPic/image-20201114185134GANAev.png" alt="CFG diagram for previous example showing phi node" /></p><p>该<code>phi</code>节点由编译器用来在分析和优化此类<code>IR</code>时维护<code>SSA</code>，并在稍后阶段由实际的机器代码代替。</p><p><code>SSA</code>名称的静态部分起着类似于静态类型的作用。 这意味着在查看源代码时（在编译时或静态地）每个名称的分配都是唯一的，而在运行时可能会多次发生。 如果上面显示的代码示例处于循环中，则实际的<code>x_1 = 2</code>分配可能发生多次。</p><p>现在我们对什么是SSA有了基本的了解，接下来的问题是为什么。</p><p>化是编译器后端的重要组成部分[1]，后端通常是专门为促进有效和高效的优化而构造的。 再次查看此代码片段：</p><pre><code>x = 1if condition: x = 2use(x)</code></pre><p>并假设编译器要运行一个非常常见的优化-常量传播(<em>constant propagation</em>)； 也就是说，它希望在<code>x = 1</code>分配后用<code>1</code>替换x的所有用法。 怎么会这样呢？ 它不能只找到赋值后对<code>x</code>的所有引用，因为<code>x</code>可以重写为其他内容（如我们的示例）。</p><p>考虑这个代码片段</p><pre><code>z = x + y</code></pre><p>通常，编译器必须执行数据流分析才能找到：</p><ol><li><p><code>x</code>和<code>y</code>指的是哪个定义。 在存在控制流的情况下，这并不容易，需要进行优势分析(<em>dominance</em> analysis)。</p></li><li><p>在此定义之后使用z时，同样具有挑战性。</p></li></ol><p>就时间和空间而言，这种分析的创建和维护成本很高。 而且，必须在每次优化后（至少部分）重新运行它。</p><p><code>SSA</code>提供了一个很好的选择。 如果<code>z = x + y</code>在<code>SSA</code>中，则我们立即知道<code>x</code>和<code>y</code>所引用的定义（只能有一个！），并且我们立即知道在哪里使用<code>z</code>（此语句之后对<code>z</code>的所有引用）。 在<code>SSA</code>中，用法和定义在<code>IR</code>中进行了编码，并且优化不会违反不变性。</p><h2 id="go编译器中的ssa">Go编译器中的SSA</h2><p>我们继续描述Go编译器中如何构造和使用<code>SSA</code>。 <code>SSA</code>是Go中一个<a href="https://blog.golang.org/go1.7">相对较新的功能</a>。除了将<code>AST</code>转换为<code>SSA</code>的大量代码（位于<code>gc/ssa.go</code>中）外，其大部分代码都位于<code>ssa</code>中。</p><p><code>ssa</code>目录中的<code>README</code>文件是<code>Go SSA</code>的非常有用的说明-请阅读一下！</p><p><code>Go SSA</code>实现也有一些“我”见过的最好的编译器工具。通过设置<code>GOSSAFUNC</code>环境变量，我们将获得一个HTML页面，其中包含所有编译阶段以及每个编译阶段之后的<code>IR</code>，因此我们可以轻松地检测出需要进行哪些优化。 附加设置可以在每次通过时将控制流图绘制为SVG。</p><p>让我们研究一下从<code>AST</code>为该代码段创建的初始<code>SSA</code>：</p><pre><code class="language-go">func usefor() {  i := 4  for !(i == 0) {    i--    sayhi()  }}func sayhi() {  fmt.Println(&quot;Hello, for!&quot;)}</code></pre><p>我将打印输出移出<code>usefor</code>的原因是为了使<code>SSA</code>的结果更干净。使用<code>-l</code>进行编译以禁用内联，使得保留<code>sayhi()</code>的函数调用（由于常量字符串，内联对<code>fmt.Println</code>的调用会生成更多代码）。</p><p>产生的<code>SSA</code>为：</p><pre><code class="language-bash">b1:        v1 (?) = InitMem &lt;mem&gt;        v2 (?) = SP &lt;uintptr&gt;        v3 (?) = SB &lt;uintptr&gt;        v4 (?) = Const64 &lt;int&gt; [4] (i[int])        v6 (?) = Const64 &lt;int&gt; [0]        v9 (?) = Const64 &lt;int&gt; [1]    Plain → b2 (10)    b2: ← b1 b4        v5 (10) = Phi &lt;int&gt; v4 v10 (i[int])        v14 (14) = Phi &lt;mem&gt; v1 v12        v7 (10) = Eq64 &lt;bool&gt; v5 v6    If v7 → b5 b3 (unlikely) (10)    b3: ← b2        v8 (11) = Copy &lt;int&gt; v5 (i[int])        v10 (11) = Sub64 &lt;int&gt; v8 v9 (i[int])        v11 (12) = Copy &lt;mem&gt; v14        v12 (12) = StaticCall &lt;mem&gt; {&quot;&quot;.sayhi} v11    Plain → b4 (12)    b4: ← b3    Plain → b2 (10)    b5: ← b2        v13 (14) = Copy &lt;mem&gt; v14    Ret v13</code></pre><p>这里要注意的有趣部分是：</p><ul><li><code>bN</code>是控制流程图的基本块。</li><li><code>phi</code>节点是明确的。 最有趣的是分配给<code>v5</code>。 这正是选择器分配给<code>i</code>的情况； 一条路径来自<code>v4</code>（初始化程序），另一个路径来自体循环内部的<code>v10 (i--)</code>。</li><li>出于本练习的目的，请忽略带有<code>&lt;mem&gt;</code>的节点。Go有一种有趣的方式可以在其<code>IR</code>中显式传播内存状态，在本文中我们将不涉及。 如果有兴趣，请参见上述自述文件以了解更多详细信息。</li></ul><p>顺便说一句，这里的<code>for</code>循环正是我们想要将<code>until</code>语句转换成的形式。</p><h2 id="将until-ast节点转换为ssa">将<code>until</code> AST节点转换为SSA</h2><p>像往常一样，我们的代码将基于<code>for</code>语句的处理进行建模。首先，让我们粗略地描述一下控制流图应该如何处理<code>until</code>语句</p><p><img src="http://imghost.leonard.wang//uPic/image-20201114191519XfT4Gx.png" alt="CFG for until nodes" /></p><p>现在我们只需要在代码中构建这个CFG。提醒:我们在第1部分中添加的新<code>AST</code>节点类型是<code>OUNTIL</code>。</p><p>我们将在<code>gc/ssa.go</code>中的<code>state.stmt</code>方法中添加一个新的<code>switch</code>子句，以将具有<code>OUNTIL</code>的<code>AST</code>节点转换为<code>SSA</code>。块和注释的命名应使代码易于阅读，并与上面显示的CFG相关。</p><pre><code class="language-go">case OUNTIL:  // OUNTIL: until Ninit; Left { Nbody }  // cond (Left); body (Nbody)  bCond := s.f.NewBlock(ssa.BlockPlain)  bBody := s.f.NewBlock(ssa.BlockPlain)  bEnd := s.f.NewBlock(ssa.BlockPlain)  bBody.Pos = n.Pos  // first, entry jump to the condition  b := s.endBlock()  b.AddEdgeTo(bCond)  // generate code to test condition  s.startBlock(bCond)  if n.Left != nil {    s.condBranch(n.Left, bEnd, bBody, 1)  } else {    b := s.endBlock()    b.Kind = ssa.BlockPlain    b.AddEdgeTo(bBody)  }  // set up for continue/break in body  prevContinue := s.continueTo  prevBreak := s.breakTo  s.continueTo = bCond  s.breakTo = bEnd  lab := s.labeledNodes[n]  if lab != nil {    // labeled until loop    lab.continueTarget = bCond    lab.breakTarget = bEnd  }  // generate body  s.startBlock(bBody)  s.stmtList(n.Nbody)  // tear down continue/break  s.continueTo = prevContinue  s.breakTo = prevBreak  if lab != nil {    lab.continueTarget = nil    lab.breakTarget = nil  }  // done with body, goto cond  if b := s.endBlock(); b != nil {    b.AddEdgeTo(bCond)  }  s.startBlock(bEnd)</code></pre><p>如果您想知道<code>n.Ninit</code>的处理位置-它在<code>switch</code>之前完成，对于所有节点类型都是统一的。<br />事实上，这就是我们在编译器的最后阶段对于<code>until</code>语句所要做的一切。如果我们对于如下代码，运行编译器获取<code>SSA</code>：</p><pre><code>func useuntil() {  i := 4  until i == 0 {    i--    sayhi()  }}func sayhi() {  fmt.Println(&quot;Hello, for!&quot;)}</code></pre><p>我们将得到SSA，它在结构上与<code>for</code>循环相同，条件为负数，正如预期的那样。</p><h2 id="ssa变换">SSA变换</h2><p>构造初始<code>SSA</code>之后，编译器会在<code>SSA IR</code>上执行以下较长的遍历过程：</p><ol><li>执行优化</li><li>将其<code>lower</code>为更接近机器代码的形式<br />可以在<code>ssa/compile.go</code>的<code>passs</code>slice 中找到所有<code>pass</code>，并且在同一文件的<code>passOrder</code> slice 中可以限制它们的运行顺序。 对于现代编译器而言，优化是相当标准的。<code>lower</code>还包括针对我们正在编译的特定体系结构的指令选择以及寄存器分配。<br />有关这些<code>pass</code>的其他详细信息，请参见<a href="https://github.com/golang/go/blob/master/src/cmd/compile/internal/ssa/README.md">SSA自述文件</a>以及<a href="https://quasilyte.dev/blog/post/go_ssa_rules/">这篇博客</a>，其中详细介绍了如何指定<code>SSA</code>优化规则。</li></ol><h2 id="生成机器码">生成机器码</h2><p>最后，编译器调用<code>gc/ssa.go</code>文件中的<code>genssa</code>，从<code>SSA IR</code>发出机器代码。<br />我们不需要修改任何代码，因为<code>until</code>语句生成的<code>ssa</code> 由在编译器中其他位置使用的构建块组成，我们也不需要添加新的指令类型。<br />然而，这对于我们研究<code>useuntil</code>函数的代码生成具有指导意义。Go有其历史根源的<a href="https://golang.org/doc/asm">Plan9汇编语法</a>。我不会在这里进行所有详细介绍，但是以下是带注释的（带有＃注释）反汇编文件，应该比较容易理解。我删除了一些垃圾回收器的指令（<code>PCDATA</code>和<code>FUNCDATA</code>）以使输出变小。</p><pre><code>&quot;&quot;.useuntil STEXT size=76 args=0x0 locals=0x10  0x0000 00000 (useuntil.go:5)  TEXT  &quot;&quot;.useuntil(SB), ABIInternal, $16-0  # Function prologue  0x0000 00000 (useuntil.go:5)  MOVQ  (TLS), CX  0x0009 00009 (useuntil.go:5)  CMPQ  SP, 16(CX)  0x000d 00013 (useuntil.go:5)  JLS  69  0x000f 00015 (useuntil.go:5)  SUBQ  $16, SP  0x0013 00019 (useuntil.go:5)  MOVQ  BP, 8(SP)  0x0018 00024 (useuntil.go:5)  LEAQ  8(SP), BP  # AX will be used to hold 'i', the loop counter; it's initialized  # with the constant 4. Then, unconditional jump to the 'cond' block.  0x001d 00029 (useuntil.go:5)  MOVL  $4, AX  0x0022 00034 (useuntil.go:7)  JMP  62  # The end block is here, it executes the function epilogue and returns.  0x0024 00036 (&lt;unknown line number&gt;)  MOVQ  8(SP), BP  0x0029 00041 (&lt;unknown line number&gt;)  ADDQ  $16, SP  0x002d 00045 (&lt;unknown line number&gt;)  RET  # This is the loop body. AX is saved on the stack, so as to  # avoid being clobbered by &quot;sayhi&quot; (this is the caller-saved  # calling convention). Then &quot;sayhi&quot; is called.  0x002e 00046 (useuntil.go:7)  MOVQ  AX, &quot;&quot;.i(SP)  0x0032 00050 (useuntil.go:9)  CALL  &quot;&quot;.sayhi(SB)  # Restore AX (i) from the stack and decrement it.  0x0037 00055 (useuntil.go:8)  MOVQ  &quot;&quot;.i(SP), AX  0x003b 00059 (useuntil.go:8)  DECQ  AX  # The cond block is here. AX == 0 is tested, and if it's true, jump to  # the end block. Otherwise, it jumps to the loop body.  0x003e 00062 (useuntil.go:7)  TESTQ  AX, AX  0x0041 00065 (useuntil.go:7)  JEQ  36  0x0043 00067 (useuntil.go:7)  JMP  46  0x0045 00069 (useuntil.go:7)  NOP  0x0045 00069 (useuntil.go:5)  CALL  runtime.morestack_noctxt(SB)  0x004a 00074 (useuntil.go:5)  JMP  0</code></pre><p>如果您仔细观察的话，您可能已经注意到<code>cond</code>块移到了函数的末尾，而不是它最初在<code>SSA</code>表示中的位置。为什么会这样？<br />答案是在最后阶段在<code>SSA</code>上运行“loop rotate” <code>pass</code>。 此<code>pass</code>对块进行重新排序，使主体直接流入<code>cond</code>，避免每次迭代产生额外的跳跃。 如果您有兴趣，请参阅<code>ssa/looprotate.go</code>了解更多详细信息。</p><h2 id="结论">结论</h2><p>就是这样!在这两篇文章中，我们通过两种不同的方式实现一个新的语句，学习了Go编译器的一些内部原理。当然，这只是冰山一角，但我希望它为您开始自己的探索提供了一个良好的起点。<br />最后一点：我们在这里构建了一个可运行的编译器，但是Go工具都无法识别新的<code>until</code>关键字。不幸的是，此时Go工具使用了完全不同的路径来解析Go代码，并且没有与Go编译器本身共享此代码。在以后的文章中，我将详细介绍如何使用工具处理Go代码。</p><h2 id="附录-重现这些结果">附录-重现这些结果</h2><p>为了重现我们在这里结束的Go工具链的版本，您可以从第1部分开始，<strong>还原<code>walk.go</code>中的AST转换代码</strong>，然后添加上述的<code>AST-&gt; SSA</code>转换。<br />或者，您也可以从我的项目中获取<a href="https://github.com/eliben/go/tree/adduntil2">adduntil2分支</a>。<br />构建工具链后运行以下命令，可以在一个HTML文件中获取所有<code>SSA</code>和代码生成<code>pass</code>的<code>SSA</code><br /><code>GOSSAFUNC=useuntil &lt;src checkout&gt;/bin/go tool compile -l useuntil.go</code><br />然后在浏览器中打开ssa.html。 如果您还想查看CFG的某些<code>pass</code>，请在函数名后添加<code>pass</code>名，以<code>:</code>分隔。 例如</p><p><code>GOSSAFUNC = useuntil：number_lines</code>。<br />要获取反汇编代码，请运行<br /><code>&lt;src checkout&gt;/bin/go tool compile -l -S useuntil.go</code></p>]]>
                    </description>
                    <pubDate>Sat, 14 Nov 2020 19:23:20 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Go P.runnext 是什么？]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/golangprunnext是什么</link>
                    <description>
                            <![CDATA[<p>下面示例的输出是什么？</p><pre><code class="language-go">// go 1.14package mainimport (&quot;fmt&quot;&quot;os&quot;&quot;runtime&quot;&quot;runtime/trace&quot;&quot;sync&quot;)func main() {runtime.GOMAXPROCS(1)wg := &amp;sync.WaitGroup{}wg.Add(3)go func() {fmt.Println(&quot;A&quot;)wg.Done()}()go func() {fmt.Println(&quot;B&quot;)wg.Done()}()go func() {fmt.Println(&quot;C&quot;)wg.Done()}()wg.Wait()}</code></pre><p>先说结论，如下，且是稳定的</p><pre><code class="language-bash">// OUTPUT:// C// A// B</code></pre><p>涉及如下知识点：</p><ol><li><p><code>runtime.GOMAXPROCS()</code></p><blockquote><p>GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously and returns the previous setting.</p></blockquote><p>设置同时可执行的<code>cpu</code>数量，当设置为<code>1</code>时，可以认为是单线程</p></li><li><p><code>sync.WaitGroup{}</code></p><p>一种等待同步机制，<code>wg.Wait()</code>会一直等待<code>wg</code>的值为<code>0</code>才会继续执行。<code>wg.Add()</code>增加，<code>wg.Done()</code>减一。</p></li><li><p><code>go</code>关键字都做了什么？</p><p>这里需要对Go的GMP调度模型有一定了解，<a href="https://learnku.com/articles/41728">推荐 [典藏版] Golang 调度器 GMP 原理与调度全分析</a></p><p>go关键字在代码生成过程中会被转换为<code>runtime.newproc</code></p><pre><code class="language-go">//`$GOROOT/src/cmd/compile/internal/gc/ssa.go`case k == callGo:call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, newproc, s.mem())</code></pre><pre><code class="language-go">//`$GOROOT/src/runtime/proc.go`func newproc(siz int32, fn *funcval) {argp := add(unsafe.Pointer(&amp;fn), sys.PtrSize)gp := getg()pc := getcallerpc()systemstack(func() {newproc1(fn, argp, siz, gp, pc)})}// Create a new g running fn with narg bytes of arguments starting// at argp. callerpc is the address of the go statement that created// this. The new g is put on the queue of g's waiting to run.func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) {// 省略部分代码// 获取当前 goroutine_g_ := getg()// 获取当前 goroutine 所在的 P  _p_ := _g_.m.p.ptr()// 生成新创建的 goroutine 数据结构newg := gfget(_p_)// 将新创建的 goroutine 状态设置为`_Grunnable`casgstatus(newg, _Gdead, _Grunnable)// 生成新创建的 goroutine idnewg.goid = int64(_p_.goidcache)// 将新创建的  goroutine 放入 “当前 goroutine 所在的 P” 的本地队列中，注意第三个参数为`true` runqput(_p_, newg, true)if atomic.Load(&amp;sched.npidle) != 0 &amp;&amp; atomic.Load(&amp;sched.nmspinning) == 0 &amp;&amp; mainStarted {wakep()}releasem(_g_.m)}</code></pre></li><li><p><code>P.runnext</code>是什么？</p><p>每个P上都有一个<code>runnext</code>字段，类型是<code>guintptr</code>，语义为下一个优先执行的<code>goroutine</code></p><p>但是只能存储一个，如果有多个，被抢占掉的<code>goroutine</code>会被放到可执行队列的队尾，等到正常调度</p><pre><code class="language-go">//`$GOROOT/src/runtime/proc.go`// runqput tries to put g on the local runnable queue.// If next is false, runqput adds g to the tail of the runnable queue.// If next is true, runqput puts g in the _p_.runnext slot.// If the run queue is full, runnext puts g on the global queue.// Executed only by the owner P.func runqput(_p_ *p, gp *g, next bool) {if next {retryNext:oldnext := _p_.runnext// 将 _p_.runnext 的旧值和当前 goroutine 进行交换if !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {goto retryNext}// 如果 _p_.runnext 的旧值为空，则直接返回if oldnext == 0 {return}// Kick the old runnext out to the regular run queue.    // 如果 _p_.runnext 的旧值不为空，获取其对应的 goroutine 指针，gp 为_p_.runnext 的旧值gp = oldnext.ptr()}// 如果 next 为 false ，则 gp 仍为新创建的 goroutine// 如果 next 为 true ，则 gp 为_p_.runnext 的旧值// 将 gp 放置到 P 本地队列的队尾，如果 P 的本地队列已满，则放置到全局队列上，等待调度器进行调度retry:h := atomic.LoadAcq(&amp;_p_.runqhead) // load-acquire, synchronize with consumerst := _p_.runqtailif t-h &lt; uint32(len(_p_.runq)) {_p_.runq[t%uint32(len(_p_.runq))].set(gp)atomic.StoreRel(&amp;_p_.runqtail, t+1) // store-release, makes the item available for consumptionreturn}if runqputslow(_p_, gp, h, t) {return}// the queue is not full, now the put above must succeedgoto retry}</code></pre></li></ol><p>调度相关的任务，分为 每个 P 上的本地队列（减少锁冲突的一种优化）、全局队列（本地队列已满或者P需要被释放时，会将任务放置到全局队列中）、还有本文所涉及的 <code>P.runnext</code></p><ol start="5"><li><p>调度器视角下的任务优先级，调度器的函数为<code>runtime.schedule()</code>，该函数的目的是找到一个可执行的<code>goroutine</code>去运行，在该函数中，获取<code>goroutine</code>的位置越靠前，则可以认为其优先级越高。</p><pre><code class="language-go">//`$GOROOT/src/runtime/proc.go`// One round of scheduler: find a runnable goroutine and execute it.// Never returns.func schedule() {_g_ := getg()  // runtime.LockOSThread()相关，将G和M进行双向绑定if _g_.m.lockedg != 0 {stoplockedm()execute(_g_.m.lockedg.ptr(), false) // Never returns.}top:pp := _g_.m.p.ptr()pp.preempt = false// 如果处于 Stop The World 等待过程中，则优先进行 STW，等待Start The World 后再进行调度if sched.gcwaiting != 0 {gcstopm()goto top}// 在go1.14之后，timer的运行检查放置到 schedule 中checkTimers(pp, 0)var gp *gvar inheritTime bool// Normal goroutines will check for need to wakeP in ready,// but GCworkers and tracereaders will not, so the check must// be done here instead.tryWakeP := false// trace相关goroutineif trace.enabled || trace.shutdown {gp = traceReader()if gp != nil {casgstatus(gp, _Gwaiting, _Grunnable)traceGoUnpark(gp, 0)tryWakeP = true}}// gc相关goroutineif gp == nil &amp;&amp; gcBlackenEnabled != 0 {gp = gcController.findRunnableGCWorker(_g_.m.p.ptr())tryWakeP = tryWakeP || gp != nil}// 下面开始正常 goroutine 的查找（越靠前的优先级越高）if gp == nil {// Check the global runnable queue once in a while to ensure fairness.// Otherwise two goroutines can completely occupy the local runqueue// by constantly respawning each other.// 为了防止全局队列“饿死”，每个P 每处理 61 个goroutine，会从全局获取一个goroutine进行处理if _g_.m.p.ptr().schedtick%61 == 0 &amp;&amp; sched.runqsize &gt; 0 {lock(&amp;sched.lock)gp = globrunqget(_g_.m.p.ptr(), 1)unlock(&amp;sched.lock)}}// 从 P.runnext 和 P的本地队列中进行寻找if gp == nil {gp, inheritTime = runqget(_g_.m.p.ptr())// We can see gp != nil here even if the M is spinning,// if checkTimers added a local goroutine via goready.}// 这个函数比较复杂，再继续检查timer、从P的本地队列、全局队列、netpoll、偷取其他P的本地队列、等进行获取if gp == nil {gp, inheritTime = findrunnable() // blocks until work is available}// This thread is going to run a goroutine and is not spinning anymore,// so if it was marked as spinning we need to reset it now and potentially// start a new spinning M.if _g_.m.spinning {resetspinning()}// If about to schedule a not-normal goroutine (a GCworker or tracereader),// wake a P if there is one.if tryWakeP {if atomic.Load(&amp;sched.npidle) != 0 &amp;&amp; atomic.Load(&amp;sched.nmspinning) == 0 {wakep()}}// runtime.LockOSThread()相关if gp.lockedm != 0 {// Hands off own p to the locked m,// then blocks waiting for a new p.startlockedm(gp)goto top}// 运行所找到的 goroutineexecute(gp, inheritTime)}</code></pre><pre><code class="language-go">// Get g from local runnable queue.// If inheritTime is true, gp should inherit the remaining time in the// current time slice. Otherwise, it should start a new time slice.// Executed only by the owner P.func runqget(_p_ *p) (gp *g, inheritTime bool) {// If there's a runnext, it's the next G to run.for {// 优先从_p_.runnext进行查找，这也就是为什么 _p_.runnext 比其他 goroutine 优先级高的原因next := _p_.runnextif next == 0 {break}if _p_.runnext.cas(next, 0) {return next.ptr(), true}}// 从本地队列进行查找for {h := atomic.LoadAcq(&amp;_p_.runqhead) // load-acquire, synchronize with other consumerst := _p_.runqtailif t == h {return nil, false}gp := _p_.runq[h%uint32(len(_p_.runq))].ptr()if atomic.CasRel(&amp;_p_.runqhead, h, h+1) { // cas-release, commits consumereturn gp, false}}}</code></pre><p>综上所述，<code>Goroutine</code> 所涉及的优先级为：</p><p><code>P.runnext</code> &gt; <code>P.localrunq</code> &gt; <code>globalrunq</code></p><p><img src="https://imghost.leonard.wang/uPic/image-2020082300354361k3kM.png" alt="image-20200823003542926" /></p><p>所以上述例子中，goroutine的提交顺序为：</p><pre><code class="language-go">// go 1.14package mainimport (&quot;fmt&quot;&quot;os&quot;&quot;runtime&quot;&quot;runtime/trace&quot;&quot;sync&quot;)func main() {runtime.GOMAXPROCS(1)wg := &amp;sync.WaitGroup{}wg.Add(3)// 提交G1go func() {fmt.Println(&quot;A&quot;)wg.Done()}()// 提交G2go func() {fmt.Println(&quot;B&quot;)wg.Done()}()// 提交G3go func() {fmt.Println(&quot;C&quot;)wg.Done()}()wg.Wait()}</code></pre><p><img src="https://imghost.leonard.wang//uPic/image-20200823004345BeKWFB.png" alt="image-20200823004345342" /></p><p>所以<code>goroutine</code>最终的执行顺序为<code>G3 -&gt; G1 -&gt;G2</code></p><p>在<code>Go1.13</code>中，可能如下的<code>demo</code>会推翻我们上面的结论</p><pre><code class="language-go">// go1.13package mainimport (&quot;fmt&quot;&quot;runtime&quot;&quot;time&quot;)func main() {runtime.GOMAXPROCS(1)go func() {fmt.Println(&quot;A&quot;)}()go func() {fmt.Println(&quot;B&quot;)}()go func() {fmt.Println(&quot;C&quot;)}()time.Sleep(time.Second)}</code></pre><p>输出如下，且是稳定的</p><pre><code class="language-bash">// OUTPUT:// A// B// C</code></pre><p>别急，事出反常必有妖~</p><p>我们来通过<code>trace</code>看一下发生了什么！</p><p><img src="https://imghost.leonard.wang//uPic/image-20200823005126M0PVYB.png" alt="image-20200823005126156" /></p><p>由<code>trace</code>可以看出第一个运行的<code>goroutine</code>为<code>runtime.timerproc()</code></p><pre><code class="language-go">//`$GOROOT/src/runtime/time.go`// timeSleep puts the current goroutine to sleep for at least ns nanoseconds.//go:linkname timeSleep time.Sleepfunc timeSleep(ns int64) {if ns &lt;= 0 {return}gp := getg()t := gp.timerif t == nil {t = new(timer)gp.timer = t}*t = timer{}t.when = nanotime() + nst.f = goroutineReadyt.arg = gptb := t.assignBucket()lock(&amp;tb.lock)if !tb.addtimerLocked(t) {unlock(&amp;tb.lock)badTimer()}goparkunlock(&amp;tb.lock, waitReasonSleep, traceEvGoSleep, 2)}// Add a timer to the heap and start or kick timerproc if the new timer is// earlier than any of the others.// Timers are locked.// Returns whether all is well: false if the data structure is corrupt// due to user-level races.func (tb *timersBucket) addtimerLocked(t *timer) bool {// when must never be negative; otherwise timerproc will overflow// during its delta calculation and never expire other runtime timers.if t.when &lt; 0 {t.when = 1&lt;&lt;63 - 1}t.i = len(tb.t)tb.t = append(tb.t, t)if !siftupTimer(tb.t, t.i) {return false}if t.i == 0 {// siftup moved to top: new earliest deadline.if tb.sleeping &amp;&amp; tb.sleepUntil &gt; t.when {tb.sleeping = falsenotewakeup(&amp;tb.waitnote)}if tb.rescheduling {tb.rescheduling = falsegoready(tb.gp, 0)}if !tb.created {tb.created = truego timerproc(tb)// 注意，这里隐含启动了一个goroutine}}return true}</code></pre><p>见上方注释，在<code>go1.13</code>中，对于每个<code>timer bucket</code> 都会启动一个后台<code>goroutine</code>，来监测自己<code>bucket</code>上的<code>timer</code>是否需要执行。</p><p>所以对于上图，在<code>go1.13</code>中会新增一个<code>goroutine</code>，变为</p><p><img src="https://imghost.leonard.wang//uPic/image-2020082301014227dVoM.png" alt="image-20200823010142185" /></p><p>最后一个问题，都有那些情况下，goroutine由这个“插队”（提交到<code>P.runnext</code>）的机会</p><p><img src="https://imghost.leonard.wang/uPic/image-20200823011749iiL6oF.png" alt="image-20200823011748686" /></p></li></ol><p>注：本文所涉及例子仅用来表述这个问题，望各位面试官高抬贵手不要收录</p>]]>
                    </description>
                    <pubDate>Sun, 23 Aug 2020 01:19:49 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Defer Panic and Recover]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/deferpanicandrecover</link>
                    <description>
                            <![CDATA[<h1 id="defer">Defer</h1><p>延时操作，在函数<code>return</code>时执行的操作</p><p>一般用来释放锁资源，关闭文件等清理操作。</p><p>在多处返回时，可以简化清理流程。</p><p>捕获异常</p><h2 id="使用场景">使用场景</h2><pre><code class="language-go">mu.Lock()defer mu.Unlock()</code></pre><pre><code class="language-go">f,err := os.Open(filename)if err != nil {    panic(err)}if f != nil {    defer f.Close()}</code></pre><h2 id="defer触发时机">defer触发时机</h2><blockquote><p>A &quot;defer&quot; statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.</p></blockquote><p><code>Go</code>官方文档中对defer的执行时机做了阐述，分别是。</p><ul><li>包裹<code>defer</code>的函数返回时</li><li>包裹<code>defer</code>的函数执行到末尾时</li><li>所在的<code>goroutine</code>发生<code>panic</code>时</li></ul><h2 id="注意事项">注意事项</h2><h3 id="后进先出">后进先出</h3><p>defer 的执行顺序是后进先出</p><pre><code class="language-go">func main() {defer fmt.Println(1)defer fmt.Println(2)defer fmt.Println(3)}// Output// 3// 2// 1</code></pre><p>出现 panic 语句的时候，会先按照 defer 的后进先出的顺序执 行，最后才会执行panic</p><pre><code class="language-go">func main() {defer fmt.Println(1)defer fmt.Println(2)defer fmt.Println(3)panic(&quot;my panic&quot;)}// Output// 3// 2// 1// panic: my panic</code></pre><pre><code class="language-go">func defer_call() {   defer func() { fmt.Println(&quot;打印前&quot;) }()   defer func() { fmt.Println(&quot;打印中&quot;) }()   defer func() { fmt.Println(&quot;打印后&quot;) }()   panic(&quot;触发异常&quot;)}func main() {   fmt.Println(&quot;Start&quot;)   defer fmt.Println(1)   defer fmt.Println(2)   defer fmt.Println(3)   defer_call()   panic(&quot;my panic&quot;)   fmt.Println(&quot;Done&quot;)}// Output// Start// 打印后// 打印中// 打印前// 3// 2// 1// panic: 触发异常</code></pre><p>defer的嵌套</p><p>defer在当前函数return时执行</p><h3 id="踩坑点">踩坑点</h3><p>使用 defer 最容易采坑的地方是和<strong>带命名返回参数的函数</strong>一起使用时。</p><p>defer 语句定义时，对外部变量的引用是有两种方式的，分别是作为<strong>函数参数</strong>和作为<strong>闭包引用</strong>。作为函数参数，则在 defer 定义时就把值传递给 defer，并被缓存起来；作为闭包引用的话，则会在 defer 函数真正调用时根据整个上下文确定当前的值。</p><p>return xxx</p><p>等价于</p><pre><code>返回值 = xxx调用 defer 函数空的 return</code></pre><pre><code class="language-go">func increaseA() int {    var i int    defer func() {        i++    }()    return i}// 0// annoy = i// i++// return</code></pre><pre><code class="language-go">func increaseB() (r int) {    defer func() {        r++    }()    return r}// 1</code></pre><pre><code class="language-go">func f1() (r int) {defer func() {r++}()return 0}// 1</code></pre><pre><code class="language-go">func f2() (r int) {   t := 5   defer func() {      t = t + 5   }()   return t}// 5</code></pre><pre><code class="language-go">func f3() (r int) {   defer func() {      r = r + 5   }()   return 1}// 6</code></pre><pre><code class="language-go">func f4() (r int) {   defer func(r int) {      r = r + 5   }(r)   return 1}// 1</code></pre><pre><code class="language-go">func main() {    var whatever [3]struct{}        for i := range whatever {        defer func() {             fmt.Println(i)         }()    }}// 2// 2// 2</code></pre><pre><code class="language-go">type number intfunc (n number) print()   { fmt.Println(n) }func (n *number) pprint() { fmt.Println(*n) }func main() {    var n number    defer n.print()    defer n.pprint()    defer func() { n.print() }()    defer func() { n.pprint() }()    n = 3}// 3 闭包，引用外部函数的n, 最终结果是3// 3 同第四个// 3 n是引用，最终求值是3// 0 对n直接求值，开始的时候n=0, 所以最后是0</code></pre><pre><code class="language-go">func main() {   defer fmt.Println(&quot;defer main&quot;)   var user = os.Getenv(&quot;USER_&quot;)   go func() {      defer func() {         fmt.Println(&quot;defer caller&quot;)         if err := recover(); err != nil {            fmt.Println(&quot;recover success. err: &quot;, err)         }      }()      func() {         defer func() {            fmt.Println(&quot;defer here&quot;)         }()         if user == &quot;&quot; {            panic(&quot;should set user env.&quot;)         }         // 此处不会执行         fmt.Println(&quot;after panic&quot;)      }()      fmt.Println(&quot;Goroutine Done&quot;)   }()   time.Sleep(time.Second)   fmt.Println(&quot;end of main function&quot;)}// Output// defer here// defer caller// recover success. err:  should set user env.// -------// end of main function// defer main</code></pre><h1 id="panic">Panic</h1><p>嵌套<code>panic</code></p><pre><code class="language-go">func main() {defer fmt.Println(&quot;in main&quot;)defer func() {defer func() {panic(&quot;panic again and again&quot;)}()panic(&quot;panic again&quot;)}()panic(&quot;panic once&quot;)}$ go run main.goin mainpanic: panic oncepanic: panic againpanic: panic again and againgoroutine 1 [running]:...exit status 2</code></pre><p>编译器会将关键字 <code>panic</code> 转换成 <a href="https://github.com/golang/go/blob/22d28a24c8b0d99f2ad6da5fe680fa3cfa216651/src/runtime/panic.go#L887-L1062"><code>runtime.gopanic</code></a></p><p>编译器会将关键字 <code>recover</code> 转换成 <a href="https://github.com/golang/go/blob/22d28a24c8b0d99f2ad6da5fe680fa3cfa216651/src/runtime/panic.go#L1080-L1094"><code>runtime.gorecover</code></a></p><pre><code class="language-go">package mainimport &quot;fmt&quot;func test2() {defer func() {fmt.Println(&quot;defer Test02&quot;)if r := recover(); r != nil {fmt.Println(&quot;recover&quot;, r)panic(&quot;test2 defer panic&quot;)}}()panic(&quot;test2 panic&quot;)fmt.Println(&quot;test2 Done&quot;)}func test1() {defer func() {fmt.Println(&quot;defer Test01&quot;)if r := recover(); r != nil {fmt.Println(&quot;recover&quot;, r)}}()test2()fmt.Println(&quot;test1 Done&quot;)}func main() {defer func() { fmt.Println(&quot;defer main&quot;) }()test1()fmt.Println(&quot;main Done&quot;)}// OutPut:// defer Test02// recover test2 panic// defer Test01// recover test2 defer panic// main Done// defer main</code></pre><h1 id="实现详解">实现详解</h1><p>编译器会将 <code>defer</code> 关键字都转换成 <a href="https://github.com/golang/go/blob/22d28a24c8b0d99f2ad6da5fe680fa3cfa216651/src/runtime/panic.go#L218-L258"><code>runtime.deferproc</code></a> 函数，函数负责创建新的延迟调用</p><p>它还会为所有调用 <code>defer</code> 的函数末尾插入 <a href="https://github.com/golang/go/blob/22d28a24c8b0d99f2ad6da5fe680fa3cfa216651/src/runtime/panic.go#L526-L571"><code>runtime.deferreturn</code></a> 的函数调用，函数负责在函数调用结束时执行所有需要的的延迟调用</p><p>runtime的几种  panic</p><p>优化点</p><p>栈上分配</p><p>开放编码</p><p>defer</p><p>deferreturn</p><p><a href="https://studygolang.com/articles/5932">https://studygolang.com/articles/5932</a></p><p><a href="https://juejin.im/post/5b9b4acde51d450e5071d51f">https://juejin.im/post/5b9b4acde51d450e5071d51f</a></p><p><a href="https://mp.weixin.qq.com/s?__biz=MzI2MDA1MTcxMg==&amp;mid=2648466918&amp;idx=2&amp;sn=151a8135f22563b7b97bf01ff480497b&amp;chksm=f2474389c530ca9f3dc2ae1124e4e5ed3db4c45096924265bccfcb8908a829b9207b0dd26047&amp;scene=21#wechat_redirect">5 年 Gopher 都不知道的 defer 细节，你别再掉进坑里！</a></p><p><a href="https://segmentfault.com/a/1190000018169295#articleHeader4">https://segmentfault.com/a/1190000018169295#articleHeader4</a></p><p><a href="https://qcrao.com/2020/03/23/how-to-traverse-defer-links/">https://qcrao.com/2020/03/23/how-to-traverse-defer-links/</a></p>]]>
                    </description>
                    <pubDate>Wed, 22 Jul 2020 00:48:28 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Golang Timer用法]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/golangtimer用法</link>
                    <description>
                            <![CDATA[<p>基于Go1.13</p><h1 id="创建停止重置">创建、停止、重置</h1><p>一次性Timer</p><pre><code class="language-go">func NewTimer(d Duration) *Timer// 支持Stop、Reset</code></pre><p>周期性Timer</p><pre><code class="language-go">func NewTicker(d Duration) *Ticker// 支持Stop</code></pre><p>NewTicker的包装</p><pre><code class="language-go">func Tick(d Duration) &lt;-chan Time</code></pre><p>当达到指定时间时，<code>chan</code>的返回值为<code>time.Now()</code></p><h1 id="用法">用法</h1><pre><code class="language-go">func main() {d := time.Secondt := time.NewTicker(d)defer t.Stop()for {&lt;-t.Clog.Println(&quot;timeout...&quot;)}}// 2020/07/20 00:06:10 timeout...// 2020/07/20 00:06:11 timeout...// 2020/07/20 00:06:12 timeout...// 2020/07/20 00:06:13 timeout...</code></pre><pre><code class="language-go">func main() {d := time.Secondt := time.NewTimer(d)defer t.Stop()&lt;-t.Clog.Println(&quot;timeout...&quot;)}// 2020/07/20 00:07:06 timeout...</code></pre><pre><code class="language-go">func main() {d := time.Secondt := time.NewTimer(d)defer t.Stop()for {&lt;-t.Clog.Println(&quot;timeout...&quot;)t.Reset(time.Second)}}// 2020/07/20 00:07:50 timeout...// 2020/07/20 00:07:51 timeout...// 2020/07/20 00:07:52 timeout...// 2020/07/20 00:07:53 timeout...</code></pre><pre><code class="language-go">func main() {d := time.Second// t := time.NewTicker(d)t := time.Tick(d)for range t {log.Println(&quot;timeout...&quot;)}}// 2020/07/20 00:10:37 timeout...// 2020/07/20 00:10:38 timeout...// 2020/07/20 00:10:39 timeout...// 2020/07/20 00:10:40 timeout...</code></pre><pre><code class="language-go">func main() {   log.Println(time.Now())   &lt;- time.After(1 * time.Second)   log.Println(time.Now())}// 2020/07/20 23:50:44 2020-07-20 23:50:44.924367 +0800 CST m=+0.000094541// 2020/07/20 23:50:45 2020-07-20 23:50:45.925691 +0800 CST m=+1.001411484</code></pre><pre><code class="language-go">func main() {   log.Println(&quot;AfterFuncDemo start: &quot;, time.Now())   // 会启动一个新的goroutine运行该func   time.AfterFunc(1 * time.Second, func() {      log.Println(&quot;AfterFuncDemo end: &quot;, time.Now())   })   time.Sleep(2 * time.Second) // 等待协程退出}// 2020/07/20 23:51:17 AfterFuncDemo start:  2020-07-20 23:51:17.201096 +0800 CST m=+0.000109548// 2020/07/20 23:51:18 AfterFuncDemo end:  2020-07-20 23:51:18.204792 +0800 CST m=+1.003800076</code></pre><h1 id="参考资料">参考资料</h1><p><a href="https://my.oschina.net/renhc/blog/3026957">https://my.oschina.net/renhc/blog/3026957</a></p>]]>
                    </description>
                    <pubDate>Mon, 20 Jul 2020 23:57:12 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[一文带你读懂Golang实验性功能SetMaxHeap 固定值GC]]>
                    </title>
                    <link>https://blog.leonard.wang/archives/golang-setmaxheap</link>
                    <description>
                            <![CDATA[<p>简单来说，<code>SetMaxHeap</code>提供了一种可以设置固定触发阈值的 <code>GC</code>（Garbage Collection垃圾回收）方式</p><p>官方源码链接 <a href="https://go-review.googlesource.com/c/go/+/227767/3">https://go-review.googlesource.com/c/go/+/227767/3</a></p><h1 id="可以解决什么问题">可以解决什么问题？</h1><p>大量临时对象分配导致的<code>GC</code>触发频率过高，<code>GC</code>后实际存活的对象较少，</p><p>或者机器内存较充足，希望使用剩余内存，降低<code>GC</code>频率的场景</p><p><img src="http://imghost.leonard.wang/halo/setmaxheap-1_1595089737176.png" alt="setmaxheap1.png" /></p><h1 id="为什么要降低gc频率">为什么要降低GC频率？</h1><p><code>GC</code>会<code>STW</code>（<code>Stop The World</code>），对于时延敏感场景，在一个周期内连续触发两轮<code>GC</code>，那么<code>STW</code>和<code>GC</code>占用的<code>CPU</code>资源都会造成很大的影响，<code>SetMaxHeap</code>并不一定是完美的，在某些场景下做了些权衡，官方也在进行相关的实验，当前方案仍没有合入主版本。</p><p>先看下如果没有<code>SetMaxHeap</code>，对于如上所述的场景的解决方案</p><p>这里简单说下<code>GC</code>的几个值的含义，可通过<code>GODEBUG=gctrace=1</code>获得如下数据</p><pre><code class="language-bash">gc 16 @1.106s 3%: 0.010+19+0.038 ms clock, 0.21+0.29/95/266+0.76 ms cpu, 128-&gt;132-&gt;67 MB, 135 MB goal, 20 Pgc 17 @1.236s 3%: 0.010+20+0.040 ms clock, 0.21+0.37/100/267+0.81 ms cpu, 129-&gt;132-&gt;67 MB, 135 MB goal, 20 P</code></pre><p>这里只关注<code>128-&gt;132-&gt;67 MB 135 MB goal</code> ，</p><p>分别为 GC开始时内存使用量 -&gt; GC标记完成时内存使用量 -&gt; GC标记完成时的存活内存量  本轮GC标记完成时的<strong>预期</strong>内存使用量（上一轮<code>GC</code>完成时确定）</p><p>引用<a href="https://docs.google.com/document/d/1wmjrocXIWTr1JxU-3EQBI6BK6KgtiFArkG47XK73xIQ/edit#">GC peace设计文档</a>中的一张图来说明</p><p><img src="http://imghost.leonard.wang/halo/setmaxheap-2_1595089761931.png" alt="setmaxheap2.png" /></p><p>对应关系如下：</p><ul><li><p>GC开始时内存使用量：<code>GC trigger</code></p></li><li><p>GC标记完成时内存使用量：<code>Heap size at GC completion</code></p></li><li><p>GC标记完成时的存活内存量：图中标记的<code>Previous marked heap size</code>为上一轮的GC标记完成时的存活内存量</p></li><li><p>本轮GC标记完成时的<strong>预期</strong>内存使用量：<code>Goal heap size</code></p></li></ul><p>简单说下<code>GC pacing</code>（信用机制）</p><blockquote><p>注：基于自己的理解做了些简化，如有错误还请指出</p></blockquote><p><code>GC pacing</code>有两个目标，</p><ul><li><p><code>|Ha-Hg|</code>最小</p></li><li><p><code>GC</code>使用的<code>cpu</code>资源约为总数的<code>25%</code></p></li></ul><p>那么当一轮<code>GC</code>完成时，如何只根据本轮<code>GC</code>存活量去实现这两个小目标呢？</p><p>这里实际是根据当前的一些数据或状态去<strong>预估</strong>“未来”，所有会存在些误差</p><p>首先确定<code>gc Goal</code>  <code>goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100</code></p><p><code>heap_marked</code>为本轮<code>GC</code>存活量，<code>gcpercent</code>默认为<code>100</code>，可以通过环境变量<code>GOGC=100</code>或者<code>debug.SetGCPercent(100)</code> 来设置</p><p>那么默认情况下 <code>goal = 2 * heap_marked</code></p><p><code>gc_trigger</code>是与<code>goal</code>相关的一个值（<code>gc_trigger</code>大约为<code>goal</code>的<code>90%</code>左右），每轮<code>GC</code>标记完成时，会根据<code>|Ha-Hg|</code>和实际使用的<code>cpu</code>资源 动态调整<code>gc_trigger</code>与<code>goal</code>的差值</p><p><code>goal</code>与<code>gc_trigger</code>的差值即为，为<code>GC</code>期间分配的对象所预留的空间</p><p><code>GC pacing</code>还会预估下一轮<code>GC</code>发生时，需要扫描对象对象的总量，进而换算为下一轮<code>GC</code>所需的工作量，进而计算出<code>mark assist</code>的值</p><p>本轮<code>GC</code>触发（<code>gc_trigger</code>），到本轮的<code>goal</code>期间，需要尽力完成<code>GC mark</code> 标记操作，所以当<code>GC</code>期间，某个<code>goroutine</code>分配大量内存时，就会被拉去做<code>mark assist</code>工作，先进行<code>GC mark</code>标记赚取足够的信用值后，才能分配对应大小的对象</p><p>根据本轮<code>GC</code>存活的内存量（<code>heap_marked</code>）和下一轮<code>GC</code>触发的阈值（<code>gc_trigger</code>）计算<code>sweep assist</code>的值，本轮<code>GC</code>完成，到下一轮<code>GC</code>触发（<code>gc_trigger</code>）时，需要尽力刚好完成<code>sweep</code>清扫操作</p><p>预估下一轮<code>GC</code>所需的工作量的方式如下：</p><pre><code class="language-go">// heap_scan is the number of bytes of &quot;scannable&quot; heap. This// is the live heap (as counted by heap_live), but omitting// no-scan objects and no-scan tails of objects.//// Whenever this is updated, call gcController.revise().heap_scan uint64// Compute the expected scan work remaining.//// This is estimated based on the expected// steady-state scannable heap. For example, with// GOGC=100, only half of the scannable heap is// expected to be live, so that's what we target.//// (This is a float calculation to avoid overflowing on// 100*heap_scan.)scanWorkExpected := int64(float64(memstats.heap_scan) * 100 / float64(100+gcpercent))</code></pre><h1 id="如何解决问题">如何解决问题</h1><p>继续分析文章开头的问题，如何充分利用剩余内存，降低<code>GC</code>频率和<code>GC</code>对<code>CPU</code>的资源消耗</p><h2 id="增大-gcpercent">增大 gcpercent？</h2><p>如上图可以看出，<code>GC</code>后，存活的对象为<code>2GB</code>左右，如果将<code>gcpercent</code>设置为<code>400</code>，那么就可以将下一轮<code>GC</code>触发阈值提升到<code>10GB</code>左右<br /><img src="http://imghost.leonard.wang/halo/setmaxheap-3_1595089792841.png" alt="setmaxheap-3" /></p><p>前面一轮看起来很好，提升了<code>GC</code>触发的阈值到<code>10GB</code>，但是如果某一轮<code>GC</code>后的存活对象到达<code>2.5GB</code>的时候，那么下一轮<code>GC</code>触发的阈值，将会超过内存阈值，造成<code>OOM</code>（<code>Out of Memory</code>），进而导致程序崩溃。</p><h2 id="关闭gc监控堆内存使用状态手动进行gc">关闭GC，监控堆内存使用状态，手动进行GC？</h2><p>可以通过<code>GOGC=off</code>或者<code>debug.SetGCPercent(-1)</code>来关闭<code>GC</code></p><p>可以通过进程外监控内存使用状态，使用信号触发的方式通知程序，或<code>ReadMemStats</code>、或<code>linkname</code> <code>runtime.heapRetained</code>等方式进行堆内存使用的监测</p><p>可以通过调用<code>runtime.GC()</code>或者<code>debug.FreeOSMemory()</code>来手动进行<code>GC</code>。</p><p>这里还需要说几个事情来解释这个方案所存在的问题</p><p>通过<code>GOGC=off</code>或者<code>debug.SetGCPercent(-1)</code>是如何关闭<code>GC</code>的？</p><p><code>gc 4 @1.006s 0%: 0.033+5.6+0.024 ms clock, 0.27+4.4/11/25+0.19 ms cpu, 428-&gt;428-&gt;16 MB, 17592186044415 MB goal, 8 P (forced)</code></p><p>通过<code>GC trace</code>可以看出，上面所说的<code>goal</code>变成了一个很诡异的值<code>17592186044415</code></p><p>实际上关闭<code>GC</code>后，<code>Go</code>会将<code>goal</code>设置为一个极大值<code>^uint64(0)</code>，那么对应的<code>GC</code>触发阈值也被调成了一个极大值，这种处理方式看起来也没什么问题，将阈值调大，预期永远不会再触发<code>GC</code></p><pre><code>^uint64(0)&gt;&gt;20 == 17592186044415 MB</code></pre><p>那么如果在关闭<code>GC</code>的情况下，手动调用<code>runtime.GC()</code>会导致什么呢？</p><p>由于<code>goal</code>和<code>gc_trigger</code>被设置成了极大值，而<code>mark assist</code>和<code>sweep assist</code>仍会按照这个错误的值去计算，导致<code>assist</code>的工作量变大，这一点可以从<code>trace</code>中进行证明</p><p><img src="http://imghost.leonard.wang/halo/setmaxheap-4_1595089832638.png" alt="setmaxheap4.png" /><br /><img src="http://imghost.leonard.wang/halo/setmaxheap-5_1595089839944.png" alt="setmaxheap5.png" /></p><p>可以看到很诡异的<code>trace</code>图，这里不做深究，该方案与<code>GC pacing</code>信用机制不兼容</p><p>记住，不要在关闭<code>GC</code>的情况下手动触发<code>GC</code>，至少在当前<code>Go1.14</code>版本中仍存在这个问题</p><h2 id="本文的主角---setmaxheap">本文的主角-- <code>SetMaxHeap</code></h2><p><code>SetMaxHeap</code>的实现原理，简单来说是强行控制了<code>goal</code>的值</p><p>注：<code>SetMaxHeap</code>，本质上是一个软限制，并不能解决<strong>极端场景</strong>下的<code>OOM</code>，可以配合内存监控和<code>debug.FreeOSMemory()</code>使用</p><p><code>SetMaxHeap</code>控制的是堆内存大小，<code>Go</code>中除了堆内存还分配了如下内存，所以实际使用过程中，与实际硬件内存阈值之间需要留有一部分余量。</p><pre><code class="language-go">stacks_sys   uint64 // only counts newosproc0 stack in mstats; differs from MemStats.StackSysmspan_sys    uint64mcache_sys   uint64buckhash_sys uint64 // profiling bucket hash tablegc_sys       uint64 // updated atomically or during STWother_sys    uint64 // updated atomically or during STW</code></pre><p>对于文章开始所述问题，使用<code>SetMaxHeap</code>后，预期的<code>GC</code>过程大概是这个样子</p><p>简单用法1</p><pre><code class="language-go">notify := make(chan struct{}, 1)  // 先不管这个notifydebug.SetMaxHeap(12&lt;&lt;30, notify)  // 设置阈值为12GBdebug.SetGCPercent(-1)            // 该方案实现过程中对关闭GC的情况做了兼容，可以这样使用</code></pre><p>该方法简单粗暴，直接将<code>goal</code>设置为了固定值<br /><img src="http://imghost.leonard.wang/halo/setmaxheap-6_1595089884049.png" alt="setmaxheap6.png" /></p><p>注：通过上文所讲，触发<code>GC</code>实际上是<code>gc_trigger</code>，所以当阈值设置为<code>12GB</code>时，会提前一点触发<code>GC</code>，这里为了描述方便，近似认为<code>gc_trigger=goal</code></p><p>简单用法2</p><pre><code class="language-go">notify := make(chan struct{}, 1)  // 先不管这个notifydebug.SetMaxHeap(12&lt;&lt;30, notify)  // 设置阈值为12GB</code></pre><p>当不关闭<code>GC</code>时，<code>SetMaxHeap</code>的逻辑是，<code>goal</code>仍按照<code>gcpercent</code>进行计算，当<code>goal</code>小于<code>SetMaxHeap</code>阈值时不进行处理；当<code>goal</code>大于<code>SetMaxHeap</code>阈值时，将<code>goal</code>限制为<code>SetMaxHeap</code>阈值<br /><img src="http://imghost.leonard.wang/halo/setmaxheap-7_1595089928296.png" alt="setmaxheap7.png" /></p><p>注：通过上文所讲，触发<code>GC</code>实际上是<code>gc_trigger</code>，所以当阈值设置为<code>12GB</code>时，会提前一点触发<code>GC</code>，这里为了描述方便，近似认为<code>gc_trigger=goal</code></p><h1 id="获取代码">获取代码</h1><p>切换到<code>go1.14</code>分支，作者选择了<code>git checkout go1.14.5</code></p><p>选择官方提供的<code>cherry-pick</code>方式(可能需要梯子，文件改动不多，我后面会列出具体改动)</p><p><code>git fetch &quot;https://go.googlesource.com/go&quot; refs/changes/67/227767/3 &amp;&amp; git cherry-pick FETCH_HEAD</code></p><p>需要重新编译Go源码</p><h1 id="实现原理">实现原理</h1><h3 id="函数原型">函数原型</h3><pre><code class="language-go">func SetMaxHeap(bytes uintptr, notify chan&lt;- struct{}) uintptr </code></pre><p>入参bytes为要设置的阈值</p><p><code>notify</code>  简单理解为 <code>GC</code>的策略 发生变化时会向<code>channel</code>发送通知，后续源码可以看出“策略”具体指哪些内容</p><blockquote><p>Whenever the garbage collector's scheduling policy changes as a result of this heap limit (that is, the result that would be returned by ReadGCPolicy changes), the garbage collector will send to the notify channel.</p></blockquote><p>返回值为本次设置之前的<code>MaxHeap</code>值</p><h3 id="官方推荐用法">官方推荐用法</h3><pre><code class="language-go">notify := make(chan struct{}, 1)var gcp GCPolicyprev := SetMaxHeap(limit, notify)// Check that that notified us of heap pressure.select {case &lt;-notify:  ReadGCPolicy(&amp;gcp)// 获取变化后的GC策略信息default:  t.Errorf(&quot;missing GC pressure notification&quot;)}</code></pre><p><code>$GOROOT/src/runtime/debug/garbage.go</code></p><pre><code class="language-go">// SetGCPercent sets the garbage collection target percentage:// a collection is triggered when the ratio of freshly allocated data// to live data remaining after the previous collection reaches this percentage.// SetGCPercent returns the previous setting.// The initial setting is the value of the GOGC environment variable// at startup, or 100 if the variable is not set.// A negative percentage disables triggering garbage collection// based on the ratio of fresh allocation to previously live heap.// However, GC can still be explicitly triggered by runtime.GC and// similar functions, or by the maximum heap size set by SetMaxHeap.func SetGCPercent(percent int) int {return int(setGCPercent(int32(percent)))}// GCPolicy reports the garbage collector's policy for controlling the// heap size and scheduling garbage collection work.type GCPolicy struct {// GCPercent is the current value of GOGC, as set by the GOGC// environment variable or SetGCPercent.//// If triggering GC by relative heap growth is disabled, this// will be -1.GCPercent int// MaxHeapBytes is the current soft heap limit set by// SetMaxHeap, in bytes.//// If there is no heap limit set, this will be ^uintptr(0).MaxHeapBytes uintptr// AvailGCPercent is the heap space available for allocation// before the next GC, as a percent of the heap used at the// end of the previous garbage collection. It measures memory// pressure and how hard the garbage collector must work to// achieve the heap size goals set by GCPercent and// MaxHeapBytes.//// For example, if AvailGCPercent is 100, then at the end of// the previous garbage collection, the space available for// allocation before the next GC was the same as the space// used. If AvailGCPercent is 20, then the space available is// only a 20% of the space used.//// AvailGCPercent is directly comparable with GCPercent.//// If AvailGCPercent &gt;= GCPercent, the garbage collector is// not under pressure and can amortize the cost of garbage// collection by allowing the heap to grow in proportion to// how much is used.//// If AvailGCPercent &lt; GCPercent, the garbage collector is// under pressure and must run more frequently to keep the// heap size under MaxHeapBytes. Smaller values of// AvailGCPercent indicate greater pressure. In this case, the// application should shed load and reduce its live heap size// to relieve memory pressure.//// AvailGCPercent is always &gt;= 0.AvailGCPercent int}// 只是针对Go堆内存的软限制，默认不开启// SetMaxHeap sets a soft limit on the size of the Go heap and returns// the previous setting. By default, there is no limit.//// If a max heap is set, the garbage collector will endeavor to keep// the heap size under the specified size, even if this is lower than// would normally be determined by GOGC (see SetGCPercent).//// 当由于SetMaxHeap导致垃圾收集器的策略发生变化（ReadGCPolicy返回的结果发生更改）时，// 会向notify发生通知// Whenever the garbage collector's scheduling policy changes as a// result of this heap limit (that is, the result that would be// returned by ReadGCPolicy changes), the garbage collector will send// to the notify channel. This is a non-blocking send, so this should// be a single-element buffered channel, though this is not required.// Only a single channel may be registered for notifications at a// time; SetMaxHeap replaces any previously registered channel.//// The application is strongly encouraged to respond to this// notification by calling ReadGCPolicy and, if AvailGCPercent is less// than GCPercent, shedding load to reduce its live heap size. Setting// a maximum heap size limits the garbage collector's ability to// amortize the cost of garbage collection when the heap reaches the// heap size limit. This is particularly important in// request-processing systems, where increasing pressure on the// garbage collector reduces CPU time available to the application,// making it less able to complete work, leading to even more pressure// on the garbage collector. The application must shed load to avoid// this &quot;GC death spiral&quot;.//// The limit set by SetMaxHeap is soft. If the garbage collector would// consume too much CPU to keep the heap under this limit (leading to// &quot;thrashing&quot;), it will allow the heap to grow larger than the// specified max heap.//// 堆内存大小不是包括进程内存占用的所有内容，它不包括stacks，C分配的内存和许多runtime内部使用的结构。// The heap size does not include everything in the process's memory// footprint. Notably, it does not include stacks, C-allocated memory,// or many runtime-internal structures.//// 当bytes== ^uintptr(0)时，关闭SetMaxHeap，仅当这种情况下notify可以为nil// To disable the heap limit, pass ^uintptr(0) for the bytes argument.// In this case, notify can be nil.//// 如果只依赖SetMaxHeap去触发过GC，在设置阈值后调用SetGCPercent(-1)来关闭原生GC// To depend only on the heap limit to trigger garbage collection,// call SetGCPercent(-1) after setting a heap limit.func SetMaxHeap(bytes uintptr, notify chan&lt;- struct{}) uintptr {// 关闭该功能if bytes == ^uintptr(0) {return gcSetMaxHeap(bytes, nil)}// 开启该功能时，应传入一个notifyif notify == nil {panic(&quot;SetMaxHeap requires a non-nil notify channel&quot;)}return gcSetMaxHeap(bytes, notify)}// gcSetMaxHeap is provided by package runtime.// 在这里只进行函数定义，在runtime中实现具体功能func gcSetMaxHeap(bytes uintptr, notify chan&lt;- struct{}) uintptr// ReadGCPolicy reads the garbage collector's current policy for// managing the heap size. This includes static settings controlled by// the application and dynamic policy determined by heap usage.// ReadGCPolicy读取垃圾收集器当前的用于管理堆大小的策略,// 这包括由应用程序控制的静态设置和由堆使用情况确定的动态策略,后面可以看下具体返回的是什么func ReadGCPolicy(gcp *GCPolicy) {gcp.GCPercent, gcp.MaxHeapBytes, gcp.AvailGCPercent = gcReadPolicy()}// gcReadPolicy is provided by package runtime.// 在这里只进行函数定义，在runtime中实现具体功能func gcReadPolicy() (gogc int, maxHeap uintptr, egogc int)</code></pre><p><code>$GOROOT/src/runtime/mgc.go</code></p><pre><code class="language-go">// To begin with, maxHeap is infinity.// 默认关闭，为极大值var maxHeap uintptr = ^uintptr(0)func gcinit() {if unsafe.Sizeof(workbuf{}) != _WorkbufSize {throw(&quot;size of Workbuf is suboptimal&quot;)}// No sweep on the first cycle.mheap_.sweepdone = 1// Set a reasonable initial GC trigger.memstats.triggerRatio = 7 / 8.0// Fake a heap_marked value so it looks like a trigger at// heapminimum is the appropriate growth from heap_marked.// This will go into computing the initial GC goal.memstats.heap_marked = uint64(float64(heapminimum) / (1 + memstats.triggerRatio))// Disable heap limit initially.// 默认关闭gcPressure.maxHeap = ^uintptr(0)// Set gcpercent from the environment. This will also compute// and set the GC trigger and goal._ = setGCPercent(readgogc())work.startSema = 1work.markDoneSema = 1}//go:linkname setGCPercent runtime/debug.setGCPercentfunc setGCPercent(in int32) (out int32) {// Run on the system stack since we grab the heap lock.var updated boolsystemstack(func() {lock(&amp;mheap_.lock)out = gcpercentif in &lt; 0 {in = -1}gcpercent = in// 为了做兼容，gcpercent&lt;0时，不再将goal设置为极大值if gcpercent &gt;= 0 {heapminimum = defaultHeapMinimum * uint64(gcpercent) / 100} else {heapminimum = 0}// Update pacing in response to gcpercent change.updated = gcSetTriggerRatio(memstats.triggerRatio)unlock(&amp;mheap_.lock)})// Pacing changed, so the scavenger should be awoken.wakeScavenger()// 如果开启了SetMaxHeap，并且发生变化时，则向notify发送通知if updated {gcPolicyNotify()}// If we just disabled GC, wait for any concurrent GC mark to// finish so we always return with no GC running.if in &lt; 0 {gcWaitOnMark(atomic.Load(&amp;work.cycles))}return out}// 新增SetMaxHeap相关内部变量var gcPressure struct {// lock may be acquired while mheap_.lock is held. Hence, it// must only be acquired from the system stack.lock mutex// notify is a notification channel for GC pressure changes// with a notification sent after every gcSetTriggerRatio.// It is provided by package debug. It may be nil.notify chan&lt;- struct{}// Together gogc, maxHeap, and egogc represent the GC policy.//// gogc is GOGC, maxHeap is the GC heap limit, and egogc is the effective GOGC.// gogc，maxHeap和egogc一起代表了GC策略// gogc是GOGC，maxHeap是GC堆限制，egogc是有效的GOGC//// These are set by the user with debug.SetMaxHeap. GC will// attempt to keep heap_live under maxHeap, even if it has to// violate GOGC (up to a point).gogc    intmaxHeap uintptregogc   int}//go:linkname gcSetMaxHeap runtime/debug.gcSetMaxHeap// 通过linkname的方式，在这里实现debug包中的gcSetMaxHeap函数内容func gcSetMaxHeap(bytes uintptr, notify chan&lt;- struct{}) uintptr {var (prev    uintptrupdated bool)systemstack(func() {// gcPressure.notify has a write barrier on it so it must be protected// by gcPressure's lock instead of mheap's, otherwise we could deadlock.// 加锁，获取设置之前的阈值，将bytes设置到maxHeap中lock(&amp;gcPressure.lock)gcPressure.notify = notifyunlock(&amp;gcPressure.lock)lock(&amp;mheap_.lock)// Update max heap.prev = maxHeapmaxHeap = bytes// Update pacing. This will update gcPressure from the// globals gcpercent and maxHeap.updated = gcSetTriggerRatio(memstats.triggerRatio)unlock(&amp;mheap_.lock)})if updated {gcPolicyNotify()}// 返回设置之前的阈值return prev}// gcSetTriggerRatio sets the trigger ratio and updates everything// derived from it: the absolute trigger, the heap goal, mark pacing,// and sweep pacing.//// This can be called any time. If GC is the in the middle of a// concurrent phase, it will adjust the pacing of that phase.//// This depends on gcpercent, mheap_.maxHeap, memstats.heap_marked,// and memstats.heap_live. These must be up to date.//// Returns whether or not there was a change in the GC policy.// If it returns true, the caller must call gcPolicyNotify() after// releasing the heap lock.//// mheap_.lock must be held or the world must be stopped.//// This must be called on the system stack because it acquires// gcPressure.lock.////go:systemstack// 新增返回值changed，当返回true时会向notify发送通知// 该函数会在gcSetMaxHeap、setGCPercent、gcMarkTermination中调用// gcSetMaxHeap、setGCPercent只会被主动调用;gcMarkTermination在每轮GC的STW2期间被调用func gcSetTriggerRatio(triggerRatio float64) (changed bool) {// Since GOGC ratios are in terms of heap_marked, make sure it// isn't 0. This shouldn't happen, but if it does we want to// avoid infinities and divide-by-zeroes.if memstats.heap_marked == 0 {memstats.heap_marked = 1}// Compute the next GC goal, which is when the allocated heap// has grown by GOGC/100 over the heap marked by the last// cycle, or maxHeap, whichever is lower.// 先根据gcpercent计算goal值goal := ^uint64(0)if gcpercent &gt;= 0 {goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100}lock(&amp;gcPressure.lock)// 如果开启了SetMaxHeap功能，并且goal大于设置的阈值，强行修改goalif gcPressure.maxHeap != ^uintptr(0) &amp;&amp; goal &gt; uint64(gcPressure.maxHeap) { // Careful of 32-bit uintptr!// Use maxHeap-based goal.goal = uint64(gcPressure.maxHeap)unlock(&amp;gcPressure.lock)// Avoid thrashing by not letting the// effective GOGC drop below 10.//// TODO(austin): This heuristic is pulled from// thin air. It might be better to do// something to more directly force// amortization of GC costs, e.g., by limiting// what fraction of the time GC can be active.var minGOGC uint64 = 10if gcpercent &gt;= 0 &amp;&amp; uint64(gcpercent) &lt; minGOGC {// The user explicitly requested// GOGC &lt; minGOGC. Use that.minGOGC = uint64(gcpercent)}lowerBound := memstats.heap_marked + memstats.heap_marked*minGOGC/100if goal &lt; lowerBound {goal = lowerBound}} else {unlock(&amp;gcPressure.lock)}// Set the trigger ratio, capped to reasonable bounds.if triggerRatio &lt; 0 {// This can happen if the mutator is allocating very// quickly or the GC is scanning very slowly.triggerRatio = 0} else if gcpercent &gt;= 0 &amp;&amp; triggerRatio &gt; float64(gcpercent)/100 {// Cap trigger ratio at GOGC/100.triggerRatio = float64(gcpercent) / 100}memstats.triggerRatio = triggerRatio// Compute the absolute GC trigger from the trigger ratio.//// We trigger the next GC cycle when the allocated heap has// grown by the trigger ratio over the marked heap size.trigger := ^uint64(0)// 计算gc_triggerif goal != ^uint64(0) {trigger = uint64(float64(memstats.heap_marked) * (1 + triggerRatio))// Ensure there's always a little margin so that the// mutator assist ratio isn't infinity.if trigger &gt; goal*95/100 {trigger = goal * 95 / 100}// If we let triggerRatio go too low, then if the application// is allocating very rapidly we might end up in a situation// where we're allocating black during a nearly always-on GC.// The result of this is a growing heap and ultimately an// increase in RSS. By capping us at a point &gt;0, we're essentially// saying that we're OK using more CPU during the GC to prevent// this growth in RSS.//// The current constant was chosen empirically: given a sufficiently// fast/scalable allocator with 48 Ps that could drive the trigger ratio// to &lt;0.05, this constant causes applications to retain the same peak// RSS compared to not having this allocator.const minTriggerRatio = 0.6minTrigger := memstats.heap_marked + uint64(minTriggerRatio*float64(goal-memstats.heap_marked))if trigger &lt; minTrigger {trigger = minTrigger}// Don't trigger below the minimum heap size.minTrigger = heapminimumif !isSweepDone() {// Concurrent sweep happens in the heap growth// from heap_live to gc_trigger, so ensure// that concurrent sweep has some heap growth// in which to perform sweeping before we// start the next GC cycle.sweepMin := atomic.Load64(&amp;memstats.heap_live) + sweepMinHeapDistanceif sweepMin &gt; minTrigger {minTrigger = sweepMin}}if trigger &lt; minTrigger {trigger = minTrigger}if int64(trigger) &lt; 0 {print(&quot;runtime: next_gc=&quot;, memstats.next_gc, &quot; heap_marked=&quot;, memstats.heap_marked, &quot; heap_live=&quot;, memstats.heap_live, &quot; initialHeapLive=&quot;, work.initialHeapLive, &quot;triggerRatio=&quot;, triggerRatio, &quot; minTrigger=&quot;, minTrigger, &quot;\n&quot;)throw(&quot;gc_trigger underflow&quot;)}if trigger &gt; goal {// The trigger ratio is always less than GOGC/100, but// other bounds on the trigger may have raised it.// Push up the goal, too.goal = trigger}}// Commit to the trigger and goal.memstats.gc_trigger = triggermemstats.next_gc = goalif trace.enabled {traceNextGC()}// Update mark pacing.if gcphase != _GCoff {gcController.revise()}// Update sweep pacing.if isSweepDone() {mheap_.sweepPagesPerByte = 0} else {// Concurrent sweep needs to sweep all of the in-use// pages by the time the allocated heap reaches the GC// trigger. Compute the ratio of in-use pages to sweep// per byte allocated, accounting for the fact that// some might already be swept.heapLiveBasis := atomic.Load64(&amp;memstats.heap_live)heapDistance := int64(trigger) - int64(heapLiveBasis)// Add a little margin so rounding errors and// concurrent sweep are less likely to leave pages// unswept when GC starts.heapDistance -= 1024 * 1024if heapDistance &lt; _PageSize {// Avoid setting the sweep ratio extremely highheapDistance = _PageSize}pagesSwept := atomic.Load64(&amp;mheap_.pagesSwept)pagesInUse := atomic.Load64(&amp;mheap_.pagesInUse)sweepDistancePages := int64(pagesInUse) - int64(pagesSwept)if sweepDistancePages &lt;= 0 {mheap_.sweepPagesPerByte = 0} else {mheap_.sweepPagesPerByte = float64(sweepDistancePages) / float64(heapDistance)mheap_.sweepHeapLiveBasis = heapLiveBasis// Write pagesSweptBasis last, since this// signals concurrent sweeps to recompute// their debt.atomic.Store64(&amp;mheap_.pagesSweptBasis, pagesSwept)}}gcPaceScavenger()// Update the GC policy due to a GC pressure change.lock(&amp;gcPressure.lock)gogc, maxHeap, egogc := gcReadPolicyLocked()// 如果gogc、maxHeap、egogc中的任何一个值与上次调用gcSetTriggerRatio时的值不一样，则返回trueif gogc != gcPressure.gogc || maxHeap != gcPressure.maxHeap || egogc != gcPressure.egogc {gcPressure.gogc, gcPressure.maxHeap, gcPressure.egogc = gogc, maxHeap, egogcchanged = true}unlock(&amp;gcPressure.lock)return}// Sends a non-blocking notification on gcPressure.notify.//// mheap_.lock and gcPressure.lock must not be held.// 简单做些判断后，向notify以非阻塞的方式发送通知func gcPolicyNotify() {// Switch to the system stack to acquire gcPressure.lock.var n chan&lt;- struct{}gp := getg()systemstack(func() {lock(&amp;gcPressure.lock)if gcPressure.notify == nil {unlock(&amp;gcPressure.lock)return}if raceenabled {// notify is protected by gcPressure.lock, but// the race detector can't see that.raceacquireg(gp, unsafe.Pointer(&amp;gcPressure.notify))}// Just grab the channel first so that we're holding as// few locks as possible when we actually make the channel send.n = gcPressure.notifyif raceenabled {racereleaseg(gp, unsafe.Pointer(&amp;gcPressure.notify))}unlock(&amp;gcPressure.lock)})if n == nil {return}// Perform a non-blocking send on the channel.select {case n &lt;- struct{}{}:default:}}//go:linkname gcReadPolicy runtime/debug.gcReadPolicyfunc gcReadPolicy() (gogc int, maxHeap uintptr, egogc int) {systemstack(func() {lock(&amp;mheap_.lock)gogc, maxHeap, egogc = gcReadPolicyLocked()unlock(&amp;mheap_.lock)})return}// mheap_.lock must be locked, therefore this must be called on the// systemstack.//go:systemstack// 按照前面的注释所述，gogc是GOGC，maxHeap是GC堆限制，egogc是有效的GOGCfunc gcReadPolicyLocked() (gogc int, maxHeapOut uintptr, egogc int) {// 获取计算出的实际goal值  goal := memstats.next_gc  // 如果goal小于阈值，并且没有关闭GC，那么是按照正常的gcpercent计算的，没有进行干预  // 参考简单用法2，时间点5时的GC触发值，返回gcpercentif goal &lt; uint64(maxHeap) &amp;&amp; gcpercent &gt;= 0 {// We're not up against the max heap size, so just// return GOGC.egogc = int(gcpercent)} else {// Back out the effective GOGC from the goal.// 获取实际情况的gcpercentegogc = int(gcEffectiveGrowthRatio() * 100)// The effective GOGC may actually be higher than// gcpercent if the heap is tiny. Avoid that confusion// and just return the user-set GOGC.// 当开启GC，并且实际GOGC大于设置的gcpercent时，仍然返回设置的gcpercentif gcpercent &gt;= 0 &amp;&amp; egogc &gt; int(gcpercent) {egogc = int(gcpercent)}}  // 返回的三个值分别为 设置的gcpercent、设置的maxHeap、当前实际的gcpercentreturn int(gcpercent), maxHeap, egogc}func gcEffectiveGrowthRatio() float64 {  // 计算实际的 gcpercent，根据本轮GC存活对象和下一轮GC的goal  // `goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100` 这个公式的逆运算egogc := float64(memstats.next_gc-memstats.heap_marked) / float64(memstats.heap_marked)if egogc &lt; 0 {// Shouldn't happen, but just in case.egogc = 0}return egogc}// test reports whether the trigger condition is satisfied, meaning// that the exit condition for the _GCoff phase has been met. The exit// condition should be tested when allocating.// 这个函数是计算是否需要触发GC的，返回true时会调用gcStart 进行GCfunc (t gcTrigger) test() bool {if !memstats.enablegc || panicking != 0 || gcphase != _GCoff {return false}switch t.kind {case gcTriggerHeap:// Non-atomic access to heap_live for performance. If// we are going to trigger on this, this thread just// atomically wrote heap_live anyway and we'll see our// own write.return memstats.heap_live &gt;= memstats.gc_triggercase gcTriggerTime:if gcpercent &lt; 0 &amp;&amp; gcPressure.maxHeap == ^uintptr(0) {return false}// 如果设置了debug.SetMaxHeap，即使配置debug.SetGCPercent(-1)时，超过2分钟没有触发GC时，仍然触发GClastgc := int64(atomic.Load64(&amp;memstats.last_gc_nanotime))return lastgc != 0 &amp;&amp; t.now-lastgc &gt; forcegcperiodcase gcTriggerCycle:// t.n &gt; work.cycles, but accounting for wraparound.return int32(t.n-work.cycles) &gt; 0}return true}func gcMarkTermination(nextTriggerRatio float64) {…………// Update GC trigger and pacing for the next cycle.var notify boolsystemstack(func() {notify = gcSetTriggerRatio(nextTriggerRatio)})if notify {gcPolicyNotify()} …… ……}</code></pre><p>几个注意点：</p><p>是对于<code>Go</code><strong>堆内存</strong>的<strong>软限制</strong></p><p>设置后<code>debug.SetMaxHeap</code>，即使<strong>关闭GC</strong>，<code>sysmon</code>监控到超过2分钟没有触发<code>GC</code>时，<strong>仍会触发</strong><code>GC</code></p><p>注：作者尽量用通俗易懂的语言去解释<code>Go</code>的一些机制和<code>SetMaxHeap</code>功能，可能有些描述与实现细节不完全一致<br />转载请注明出处，谢谢</p>]]>
                    </description>
                    <pubDate>Sat, 18 Jul 2020 22:17:25 CST</pubDate>
                </item>
    </channel>
</rss>