<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>第 14 章 执行栈管理 on Go 语言原本</title>
    <link>/zh-cn/part4memory/ch14stack/</link>
    <description>Recent content in 第 14 章 执行栈管理 on Go 语言原本</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <atom:link href="/zh-cn/part4memory/ch14stack/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>14.1 连续栈的设计</title>
      <link>/zh-cn/part4memory/ch14stack/design/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>/zh-cn/part4memory/ch14stack/design/</guid>
      <description>&lt;h1 id=&#34;141-连续栈的设计&#34;&gt;14.1 连续栈的设计&lt;/h1&gt;&#xA;&lt;p&gt;每个 goroutine 都带着一个执行栈。它存放着函数调用的局部变量、参数、返回地址，是程序&#xA;得以「正在执行」的物理载体。读者熟悉的 C 程序里，这个栈由操作系统线程提供，大小在线程&#xA;创建时一次确定（&lt;code&gt;ulimit -s&lt;/code&gt; 默认常是 8MB），此后不再变化。Go 走了另一条路：goroutine 的栈&#xA;不是线程栈，而是运行时在堆上分配、自己管理、可以伸缩的一段内存，初始仅 2KB。&lt;/p&gt;&#xA;&lt;p&gt;这条设计选择不是细节，它是 goroutine 能够「便宜到可以开成千上万个」的前提&#xA;（&lt;a href=&#34;../../../part3concurrency/ch09sched/mpg&#34;&gt;9.3&lt;/a&gt;）。本节先把栈在运行时里的形态讲清楚：它由哪几个&#xA;字段描述、&lt;code&gt;stackguard0&lt;/code&gt; 如何在每个函数序言里守住栈底，再回到历史，看 Go 为什么从早年的&#xA;分段栈（segmented stack）切换到今天的连续栈（contiguous stack），以及这次切换换来了什么、&#xA;付出了什么。栈的分配、拷贝与收缩等机制留给本章后续各节，这里只谈设计。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1411-栈在运行时里的形态&#34;&gt;14.1.1 栈在运行时里的形态&lt;/h2&gt;&#xA;&lt;p&gt;一个 goroutine 由一个 &lt;code&gt;g&lt;/code&gt; 对象描述，它的头几个字段就刻画了执行栈。栈的边界用一对地址&#xA;表示，区间是半开的 &lt;code&gt;[lo, hi)&lt;/code&gt;，两端没有任何隐式的元数据：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// stack 描述一段栈内存，边界恰为 [lo, hi)，两侧无隐式结构（速写）&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;stack&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;lo&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;uintptr&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 栈的低地址端（栈向低地址生长，lo 是「栈底极限」）&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;hi&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;uintptr&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 栈的高地址端（最初的栈顶）&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;栈向低地址方向生长：函数调用压栈时，栈指针 SP 从 &lt;code&gt;hi&lt;/code&gt; 一侧朝 &lt;code&gt;lo&lt;/code&gt; 移动。&lt;code&gt;lo&lt;/code&gt; 因此是这段&#xA;栈所能用到的最低地址，越过它就是栈溢出。&lt;/p&gt;</description>
    </item>
    <item>
      <title>14.2 栈的分配与缓存</title>
      <link>/zh-cn/part4memory/ch14stack/alloc/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>/zh-cn/part4memory/ch14stack/alloc/</guid>
      <description>&lt;h1 id=&#34;142-栈的分配与缓存&#34;&gt;14.2 栈的分配与缓存&lt;/h1&gt;&#xA;&lt;p&gt;&lt;a href=&#34;.././design&#34;&gt;14.1&lt;/a&gt; 把执行栈定位为一段 &lt;code&gt;[lo, hi)&lt;/code&gt; 的连续内存：它由运行时管理，本质上落在堆里，&#xA;所以每个 Goroutine 一启动就要先有人给它划出这段地址。问题随之而来：谁来划？划多大？划完之后&#xA;反复创建、销毁 Goroutine，这些两三 KB 的小块内存如果每次都直接找操作系统或全局堆去要，&#xA;锁与系统调用的代价会立刻压垮调度。&lt;/p&gt;&#xA;&lt;p&gt;答案是栈有它自己的一套分配器。它与 &lt;a href=&#34;/zh-cn/part4memory/ch12alloc/&#34;&gt;12 内存分配器&lt;/a&gt; 不是同一套代码，却几乎是&#xA;同一张设计图：&lt;strong&gt;每 P 无锁缓存在前，全局加锁仓库在后，堆与操作系统垫底&lt;/strong&gt;。读过 &lt;a href=&#34;../../ch12alloc/component&#34;&gt;12.2&lt;/a&gt;&#xA;的读者会发现，这一层不过是把 mcache / mcentral / mheap 的招式照搬到了栈上。本节先讲清这套&#xA;并行结构长什么样，再说清它为何要与对象分配分开、又如何与垃圾回收衔接。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1421-为什么栈要有自己的分配器&#34;&gt;14.2.1 为什么栈要有自己的分配器&lt;/h2&gt;&#xA;&lt;p&gt;把栈交给通用对象分配器，初看是省事的，可栈有三条与普通对象不同的性质，逼着运行时另起一套。&lt;/p&gt;&#xA;&lt;p&gt;其一，&lt;strong&gt;尺寸高度规整&lt;/strong&gt;。Go 的栈最小 2KB（&lt;code&gt;stackMin = 2048&lt;/code&gt;），每次伸缩都按 2 的幂翻倍&#xA;（&lt;a href=&#34;.././grow&#34;&gt;14.3&lt;/a&gt;），于是绝大多数栈只会是 2K / 4K / 8K / 16K 这几种大小。规整的尺寸天然适合&#xA;用自由表（free list，&lt;a href=&#34;../../ch12alloc/component&#34;&gt;12.2.1&lt;/a&gt;）按尺寸分桶管理，连尺寸类的查表都&#xA;省了，一次移位就能算出该去哪个桶。&lt;/p&gt;&#xA;&lt;p&gt;其二，&lt;strong&gt;生命周期与回收方式特殊&lt;/strong&gt;。栈不像普通对象那样被指针图引用、靠标记存活，它的存活与&#xA;对应的 Goroutine 绑定。栈内存被标成 &lt;code&gt;mSpanManual&lt;/code&gt; 状态（手动管理），不参与 GC 的标记清扫，&#xA;而是在栈收缩或 Goroutine 死亡时由运行时显式归还（&lt;a href=&#34;.././shrink&#34;&gt;14.5&lt;/a&gt;）。把这类「手动管理」的&#xA;内存与「自动回收」的对象内存分开放，回收逻辑才干净。&lt;/p&gt;&#xA;&lt;p&gt;其三，&lt;strong&gt;分配频率极高且在系统栈上&lt;/strong&gt;。&lt;code&gt;stackalloc&lt;/code&gt; 必须跑在调度栈（g0）上，因为它自己绝不能再&#xA;触发栈增长，否则会死锁（runtime issue 1547）。这条约束要求分配路径短、不可重入，更不能在&#xA;热路径上动辄加锁。&lt;/p&gt;&#xA;&lt;p&gt;这三条加起来，结论就是：给栈配一套尺寸固定、不走 GC、热路径无锁的专用分配器。它的形状，&#xA;和对象分配器是同构的。&lt;/p&gt;</description>
    </item>
    <item>
      <title>14.3 栈的增长</title>
      <link>/zh-cn/part4memory/ch14stack/grow/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>/zh-cn/part4memory/ch14stack/grow/</guid>
      <description>&lt;h1 id=&#34;143-栈的增长&#34;&gt;14.3 栈的增长&lt;/h1&gt;&#xA;&lt;p&gt;&lt;a href=&#34;.././readme&#34;&gt;14.1&lt;/a&gt; 交代了 Go 选择连续栈的理由：栈起步只有 2KB，溢出时换一块两倍大的新栈，&#xA;把旧栈整个拷过去。这一节只回答其中一个问题：运行时是怎么在「正确的时刻」知道栈快满了，又是&#xA;怎么把控制权一路交到那段负责扩容的代码手里的。栈拷贝本身的细节留到 &lt;a href=&#34;.././copy&#34;&gt;14.4&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;p&gt;困难在于，栈增长这件事必须在栈快要溢出、却还没真正溢出的瞬间发生，而触发它的又恰恰是普通的&#xA;函数调用。换言之，每一次进入函数都要先问一句「我这一帧放得下吗」，放不下才去扩容。这道检查不能&#xA;靠程序员手写，它由编译器在每个函数的开头自动埋下，这就是&lt;strong&gt;函数序言&lt;/strong&gt;（prologue）里的分段检查。&#xA;整条链路是：序言检查 → &lt;code&gt;morestack&lt;/code&gt;（汇编）→ &lt;code&gt;newstack&lt;/code&gt;（Go）→ &lt;code&gt;copystack&lt;/code&gt; → 返回原处继续执行。&#xA;我们顺着这条链走一遍。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1431-函数序言每次调用都先问一句&#34;&gt;14.3.1 函数序言：每次调用都先问一句&lt;/h2&gt;&#xA;&lt;p&gt;每个 Goroutine 的 &lt;code&gt;g&lt;/code&gt; 结构里有一个字段 &lt;code&gt;stackguard0&lt;/code&gt;，它是「栈还能用到哪里」的警戒线&#xA;（&lt;a href=&#34;../../../part1overview/ch02asm/callconv&#34;&gt;2.2&lt;/a&gt;）。正常情况下它等于 &lt;code&gt;stack.lo + stackGuard&lt;/code&gt;，&#xA;即栈底再抬高一个保护区的位置。函数序言要做的，就是拿当前的栈指针 SP 和这条警戒线比较：栈向&#xA;低地址生长，一旦 SP 跌到 &lt;code&gt;stackguard0&lt;/code&gt; 之下，就说明剩余空间不足以容纳即将压入的这一帧，必须&#xA;先扩容。&lt;/p&gt;&#xA;&lt;p&gt;编译器并不会对所有函数都生成同样的检查。帧越大，越要把帧本身的大小算进去，于是检查分三档&#xA;（常量 &lt;code&gt;StackSmall = 128&lt;/code&gt;、&lt;code&gt;StackBig = 4096&lt;/code&gt; 见 &lt;code&gt;internal/abi/stack.go&lt;/code&gt;）。下面是 amd64 上&#xA;序言的伪汇编，对应 &lt;code&gt;cmd/internal/obj/x86/obj6.go&lt;/code&gt; 里的 &lt;code&gt;stacksplit&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;14&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-asm&#34; data-lang=&#34;asm&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 小帧 framesize &amp;lt;= StackSmall：直接比较 SP 与警戒线&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;nf&#34;&gt;CMPQ&lt;/span&gt;    &lt;span class=&#34;no&#34;&gt;SP&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;stackguard0&lt;/span&gt;     &lt;span class=&#34;c1&#34;&gt;// SP 是否已跌破警戒线？&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;nf&#34;&gt;JHI&lt;/span&gt;     &lt;span class=&#34;no&#34;&gt;ok&lt;/span&gt;                  &lt;span class=&#34;c1&#34;&gt;// SP 仍在警戒线之上，放得下，跳过&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;nf&#34;&gt;CALL&lt;/span&gt;    &lt;span class=&#34;no&#34;&gt;runtime&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;·&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;morestack&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;SB&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nl&#34;&gt;ok:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 中帧 StackSmall &amp;lt; framesize &amp;lt;= StackBig：把帧大小一并算进去&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;nf&#34;&gt;LEAQ&lt;/span&gt;    &lt;span class=&#34;p&#34;&gt;-(&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;framesize-StackSmall&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)(&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;SP&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;AX&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nf&#34;&gt;CMPQ&lt;/span&gt;    &lt;span class=&#34;no&#34;&gt;AX&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;stackguard0&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nf&#34;&gt;JHI&lt;/span&gt;     &lt;span class=&#34;no&#34;&gt;ok&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nf&#34;&gt;CALL&lt;/span&gt;    &lt;span class=&#34;no&#34;&gt;runtime&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;·&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;morestack&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;SB&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 大帧 framesize &amp;gt; StackBig：还要防 SP 接近 0 时的回绕（wraparound），&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 并显式检查警戒线是否被改成了 stackPreempt 这种「故意大于任何真实 SP」的值&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;警戒线被读成内存操作数 &lt;code&gt;stackguard0&lt;/code&gt;，它在 &lt;code&gt;g&lt;/code&gt; 中的偏移恰是第 3 个字（前两个字是&#xA;&lt;code&gt;stack.lo&lt;/code&gt;、&lt;code&gt;stack.hi&lt;/code&gt;），所以汇编里写作 &lt;code&gt;2*PtrSize&lt;/code&gt; 处；C 函数则比对 &lt;code&gt;stackguard1&lt;/code&gt;&#xA;（第 4 个字）。被 &lt;code&gt;//go:nosplit&lt;/code&gt; 标记的函数不插入这段检查，代价是它们的帧必须挤进栈底那块&#xA;&lt;code&gt;stackGuard - StackSmall&lt;/code&gt; 的保留区里，链接器会遍历所有不分段函数的调用链，确保这块预留够用。&lt;/p&gt;</description>
    </item>
    <item>
      <title>14.4 栈的拷贝与指针调整</title>
      <link>/zh-cn/part4memory/ch14stack/copy/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>/zh-cn/part4memory/ch14stack/copy/</guid>
      <description>&lt;h1 id=&#34;144-栈的拷贝与指针调整&#34;&gt;14.4 栈的拷贝与指针调整&lt;/h1&gt;&#xA;&lt;p&gt;连续栈的代价集中在一处：当一个 goroutine 的栈溢出，运行时分配一段两倍大的新栈，把旧栈整个&#xA;搬过去，再释放旧栈。搬家这一步看似就是一次 &lt;code&gt;memmove&lt;/code&gt;，把 &lt;code&gt;[old.lo, old.hi)&lt;/code&gt; 的字节复制到&#xA;&lt;code&gt;[new.lo, new.hi)&lt;/code&gt;。倘若真的只是复制字节，问题就来了：栈上的变量里可能存着指向这个栈自身的&#xA;指针，比如某个局部变量取了另一个局部变量的地址。字节被原样搬到新地址后，这些指针的值没有变，&#xA;仍然指着旧栈的位置，而旧栈马上就要被回收。复制完成的那一刻，它们就成了悬垂指针。&lt;/p&gt;&#xA;&lt;p&gt;所以栈的拷贝不是 &lt;code&gt;memmove&lt;/code&gt;，而是 &lt;code&gt;memmove&lt;/code&gt; 加上一遍&lt;strong&gt;指针调整&lt;/strong&gt;：搬完字节之后，运行时必须&#xA;遍历新栈，把每一个原本指向旧栈区间的指针，统统加上一个固定的位移 &lt;span class=&#34;katex&#34;&gt;&lt;span class=&#34;katex-mathml&#34;&gt;&lt;math xmlns=&#34;http://www.w3.org/1998/Math/MathML&#34;&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;δ&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mrow&gt;&lt;mi mathvariant=&#34;normal&#34;&gt;n&lt;/mi&gt;&lt;mi mathvariant=&#34;normal&#34;&gt;e&lt;/mi&gt;&lt;mi mathvariant=&#34;normal&#34;&gt;w&lt;/mi&gt;&lt;mi mathvariant=&#34;normal&#34;&gt;.&lt;/mi&gt;&lt;mi mathvariant=&#34;normal&#34;&gt;h&lt;/mi&gt;&lt;mi mathvariant=&#34;normal&#34;&gt;i&lt;/mi&gt;&lt;/mrow&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mrow&gt;&lt;mi mathvariant=&#34;normal&#34;&gt;o&lt;/mi&gt;&lt;mi mathvariant=&#34;normal&#34;&gt;l&lt;/mi&gt;&lt;mi mathvariant=&#34;normal&#34;&gt;d&lt;/mi&gt;&lt;mi mathvariant=&#34;normal&#34;&gt;.&lt;/mi&gt;&lt;mi mathvariant=&#34;normal&#34;&gt;h&lt;/mi&gt;&lt;mi mathvariant=&#34;normal&#34;&gt;i&lt;/mi&gt;&lt;/mrow&gt;&lt;/mrow&gt;&lt;annotation encoding=&#34;application/x-tex&#34;&gt;\delta = \mathrm{new.hi} -&#xA;\mathrm{old.hi}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class=&#34;katex-html&#34; aria-hidden=&#34;true&#34;&gt;&lt;span class=&#34;base&#34;&gt;&lt;span class=&#34;strut&#34; style=&#34;height:0.6944em;&#34;&gt;&lt;/span&gt;&lt;span class=&#34;mord mathnormal&#34; style=&#34;margin-right:0.03785em;&#34;&gt;δ&lt;/span&gt;&lt;span class=&#34;mspace&#34; style=&#34;margin-right:0.2778em;&#34;&gt;&lt;/span&gt;&lt;span class=&#34;mrel&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;mspace&#34; style=&#34;margin-right:0.2778em;&#34;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;base&#34;&gt;&lt;span class=&#34;strut&#34; style=&#34;height:0.7778em;vertical-align:-0.0833em;&#34;&gt;&lt;/span&gt;&lt;span class=&#34;mord&#34;&gt;&lt;span class=&#34;mord mathrm&#34;&gt;new.hi&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;mspace&#34; style=&#34;margin-right:0.2222em;&#34;&gt;&lt;/span&gt;&lt;span class=&#34;mbin&#34;&gt;−&lt;/span&gt;&lt;span class=&#34;mspace&#34; style=&#34;margin-right:0.2222em;&#34;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;base&#34;&gt;&lt;span class=&#34;strut&#34; style=&#34;height:0.6944em;&#34;&gt;&lt;/span&gt;&lt;span class=&#34;mord&#34;&gt;&lt;span class=&#34;mord mathrm&#34;&gt;old.hi&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;，让它改指向新栈的对应位置。难点不在「怎么加」，而在「怎么知道哪些字是指针」。&#xA;栈上的字没有类型标签，一个 8 字节的字到底是指针还是恰好长得像地址的整数，运行时无从分辨。&#xA;答案来自编译器：它为每个函数在每个安全点（&lt;a href=&#34;../../ch13gc/safe&#34;&gt;13.7&lt;/a&gt;）生成了&lt;strong&gt;栈映射&lt;/strong&gt;&#xA;（stack map），用位图精确记录该函数栈帧里哪些槽位是指针。GC 扫描栈靠它，栈拷贝调整指针也靠它。&lt;/p&gt;&#xA;&lt;p&gt;这一节就讲这件事：&lt;code&gt;copystack&lt;/code&gt; 如何在搬家之后，借栈映射走遍新栈，把所有指向旧栈的指针、加上&#xA;那些不在栈帧里、却同样指向栈的运行时结构（&lt;code&gt;gobuf&lt;/code&gt;、&lt;code&gt;sudog&lt;/code&gt;、&lt;code&gt;defer&lt;/code&gt;、&lt;code&gt;panic&lt;/code&gt;）一并调整正确。&#xA;读完会看到，正是「栈可以移动」这条性质，反过来约束了语言：哪些指针允许指向栈、逃逸分析&#xA;（&lt;a href=&#34;../../../part5toolchain/ch15compile/escape&#34;&gt;15.5&lt;/a&gt;）为何必须把外部持有的地址搬到堆上。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1441-为什么不能只-memmove&#34;&gt;14.4.1 为什么不能只 memmove&lt;/h2&gt;&#xA;&lt;p&gt;先把问题摆清楚。考虑一段普通的 Go 代码：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;f&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#x9;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;x&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#x9;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;x&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// p 指向本栈帧里的 x&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#x9;&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;g&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;         &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 把栈上地址传下去&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;&lt;code&gt;p&lt;/code&gt; 的值是 &lt;code&gt;x&lt;/code&gt; 在栈上的地址，落在 &lt;code&gt;[old.lo, old.hi)&lt;/code&gt; 区间内。当 &lt;code&gt;g&lt;/code&gt; 或其更深的调用触发栈增长，&#xA;整个栈被搬到新地址，&lt;code&gt;x&lt;/code&gt; 随之搬走，但 &lt;code&gt;p&lt;/code&gt; 这个字里存的还是旧地址。若不修正，&lt;code&gt;p&lt;/code&gt; 指向的已是&#xA;一段即将释放、或将被别的 goroutine 栈复用的内存。&lt;/p&gt;</description>
    </item>
    <item>
      <title>14.5 栈的收缩与演进</title>
      <link>/zh-cn/part4memory/ch14stack/shrink/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>/zh-cn/part4memory/ch14stack/shrink/</guid>
      <description>&lt;h1 id=&#34;145-栈的收缩与演进&#34;&gt;14.5 栈的收缩与演进&lt;/h1&gt;&#xA;&lt;p&gt;&lt;a href=&#34;.././copy&#34;&gt;14.4&lt;/a&gt; 讲的是栈的「长大」：当函数序言（prologue）发现栈快要用尽，运行时把整段栈&#xA;拷贝到一块更大的内存上，栈指针随之平移。这是一条由执行本身驱动的单向力，它只会让栈越来越大。&#xA;可是一个 goroutine 的栈深并不单调。一段递归走到很深，再层层返回到浅处，它真正需要的栈早已&#xA;缩回，但那块为深递归而扩出的大栈仍挂在它名下。若只有增长没有归还，每个 goroutine 的栈最终都&#xA;会停在它历史上的最深点。对一个动辄持有百万 goroutine 的程序，这笔账无法承受。&lt;/p&gt;&#xA;&lt;p&gt;所以栈也会「缩小」。增长由序言触发，发生在执行的热路径上；收缩则交给垃圾回收，在扫描栈的间隙&#xA;顺手完成。两股力一推一收，让每个 goroutine 的栈始终贴着它当下的真实需要。这一节先看收缩的&#xA;时机与判据，再说清这套「自我调节」为何是轻量并发的地基，最后把它放回语言演进与跨系统的坐标里。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1451-收缩的时机与判据&#34;&gt;14.5.1 收缩的时机与判据&lt;/h2&gt;&#xA;&lt;p&gt;收缩不是随时能做的。拷贝栈要改写栈上所有指向栈内的指针（&lt;a href=&#34;.././copy&#34;&gt;14.4&lt;/a&gt;），这要求运行时&#xA;此刻对该 goroutine 的栈拥有精确的指针信息，并且没有别的执行流正踩在这段栈上。能同时满足这两点&#xA;的最自然的窗口，正是垃圾回收扫描栈的时候：GC 为了标记存活对象，本就要停住每个 goroutine、&#xA;逐帧扫描它的栈（&lt;a href=&#34;../../ch13gc/mark&#34;&gt;13.4&lt;/a&gt;、&lt;a href=&#34;../../ch13gc/mark&#34;&gt;13.7&lt;/a&gt;）。栈既然已经停稳、&#xA;指针图也已备齐，扫描完顺手判断要不要收缩，几乎是零额外成本。&lt;/p&gt;&#xA;&lt;p&gt;于是 &lt;code&gt;shrinkstack&lt;/code&gt; 由 &lt;code&gt;scanstack&lt;/code&gt; 调用。扫描在确认栈安全可缩后才动手；若当下不安全，就把意图&#xA;记在 &lt;code&gt;gp.preemptShrink&lt;/code&gt; 上，留到下一个同步安全点再补做：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// scanstack：GC 扫描某个 goroutine 的栈（节选自 runtime/mgcmark.go）&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;scanstack&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;gp&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;g&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;gcw&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;gcWork&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int64&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// ... 统计本次要扫描的栈大小，用于初始栈尺寸的自适应估计 ...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;isShrinkStackSafe&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;gp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;shrinkstack&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;gp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;         &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 不安全的窗口之外，顺手收缩&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;else&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;gp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;preemptShrink&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 否则推迟到下一个同步安全点&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// ... 继续扫描栈帧，标记其中的指针 ...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;什么叫「安全」？&lt;code&gt;isShrinkStackSafe&lt;/code&gt; 把几类不能动栈的处境一一排除：goroutine 正陷在系统调用里&#xA;（&lt;code&gt;syscallsp != 0&lt;/code&gt;，系统调用可能持有指向栈的裸指针，且最内层栈帧没有精确指针图）；停在异步&#xA;抢占的安全点上（同样缺精确指针图）；正处在「已调用 &lt;code&gt;gopark&lt;/code&gt; 准备挂到 channel、但 &lt;code&gt;activeStackChans&lt;/code&gt;&#xA;尚未置位」的窗口；以及为了配合 &lt;code&gt;suspendG&lt;/code&gt; 而停在 &lt;code&gt;_Gwaiting&lt;/code&gt; 的 goroutine。任何一条命中，本轮&#xA;就不缩。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
