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