标签 Memory 下的文章

Go语言是如何处理栈的

Go 1.4Beta1刚刚发布,在Go 1.4Beta1中,Go语言的stack处理方式由之前的"segmented stacks"改为了"continuous stacks"。关于Go语言对stack的处理机制、发展历史、存在问题等,CloudFlare的一篇官方blog进行了系统的阐述,这里的内容就是 翻译自CloudFlare的那篇blog:《How Stacks are Handled in Go》。

在CloudFlare,我们使用Go语言实现各种服务和应用。在这篇博文中,我们将带领大家深入挖掘一些Go的某些纷繁复杂的技术细节。

Go语言的重要特性之一是goroutines。它们是代价低廉、协同调度的执行线程,被用于实现各种操作,诸如timeout、生成器、相互竞 争的后端程序。为了使goroutines可以适应更多地任务,我们不仅需要保证每个goroutines的内存最小占用量,还要保证人们可以使 用最低配置将它们启动起来。

为了实现这个目标,Go语言采用了栈管理,这一与其他编程语言类似的方案,但在具体实现层面,又与其他语言有着较大的不同。

一、线程栈(thread stacks)介绍

在我们研究Go的栈处理方式之前,我们先来看看传统语言,比如C是如何进行栈管理的。

当你启动一个C实现的thread时,C标准库会负责分配一块内存作为这个线程的栈。标准库分配这块内存,告诉内核它的位置并让内核处理这个线程 的执行。不过当这块内存不够用时,问题就来了,我们来看一下下面这个函数:

int a(int m, int n) {
    if (m == 0) {
        return n + 1;
    } else if (m > 0 && n == 0) {
        return a(m – 1, 1);
    } else {
        return a(m – 1, a(m, n – 1));
    }
}

这个函数大量使用递归,执行a(4, 5)就会降所有栈内存耗尽。要解决这个问题,你可以调整标准库给线程栈分配的内存块的大小。但是全线提高栈大小意味着每个线程都会提高栈的内存使用量,即 便它们不是大量采用递归方式的。这样一来,你将用光所有内存,即便你的程序还尚未使用栈上的内存。

另外一种可选的解决方法则是为每个线程单独确定栈大小。这样一来你就不得不完成这样的任务:根据每个线程的需要,估算它们的栈内存的大小。这将是 创建线程的难度超出我们的期望。想搞清楚一般情况下一个线程栈需要多少内存是不可行的,即便是通常情况也是非常困难的。

二、Go是如何应对这个问题的

Go运行时会试图按需为goroutine提供它们所需要的栈空间,而不是为每个goroutine分配一个固定大小的栈空间。这样可以把程序员 们从决定栈空间大小的烦心事中解脱了出来。不过Go核心团队正在尝试切换到另外一种方案,这里我将尝试阐述旧方案以及它的缺点,新方案以及为何要 做出如此改变。

三、分段栈(Segmented Stacks)

分段栈(segmented stacks)是Go语言最初用来处理栈的方案。当创建一个goroutine时,Go运行时会分配一段8K字节的内存用于栈供goroutine运行使 用,我们让goroutine在这个栈上完成其任务处理。

当我们用光这8K字节的栈空间后,问题随之而来。为了解决这个问题,每个go函数在函数入口处都会有一小段代码(called prologue),这段代码会检查是否用光了已分配的栈空间,如果用光了,这段代码会调用morestack函数。

morestack函数会分配一段新内存用作栈空间,接下来它会将有关栈的各种数据信息写入栈底的一个struct中(译注:下图中Stack info),包括上一段栈的地址。有点我们拥有了一个新的栈段(stack segment),我们将重启goroutine,从导致栈空间用光的那个函数(译注:下图中的Foobar)开始执行。这就是所谓的“栈分裂 (stack split)”。

下面的栈示意图刚好是我们进行栈分裂后的情形:

在新栈的底部,我们插入了一个栈入口函数lessstack。我们不会调用该函数,设置这个函数就是用于我们从那个导致我们用光栈空间的函数(译 注:Foobar)返回时用的。当那个函数(译注:Foobar)返回时,我们回到lessstack(这个栈帧),lessstack会查找 stack底部的那个struct,并调整栈指针(stack pointer),使得我们返回到前一段栈空间。这样做之后,我们就可以将这个新栈段(stack segment)释放掉,并继续执行我们的程序了。

四、分段栈(Segmented stacks)的问题

分段栈给了我们具备按需伸缩能力的栈。程序员们无需担心计算栈的大小了,启动一个新的goroutine代价低廉并且程序员不会知道栈将增长多 大。

这就是直到目前Go语言处理stack增长的方法,但是这个方法有个瑕疵。那就是栈缩小会是一个相对代价高昂的操作。如果你在一个循环遇到栈分裂 (stack split),你会最有感触。一个函数会增加栈空间,做栈分裂,返回并释放栈段(stack segment)。如果你在一个循环中进行这些,你会付出很大的代价(性能方面)。

这就是所谓的“hot split”问题。它也是Go核心开发组更换到一个新的栈管理方案-栈拷贝(stack copying)的主要原因。

五、栈拷贝(stack copying)

栈拷贝初始阶段与分段栈类似。goroutine在栈上运行着,当用光栈空间,它遇到与旧方案中相同的栈溢出检查。但是与旧方案采用的保留一个返 回前一段栈的link不同,新方案创建一个两倍于原stack大小的新stack,并将旧栈拷贝到其中。这意味着当栈实际使用的空间缩小为原先的 大小时,go运行时不用做任何事情。栈缩小是一个无任何代价的操作。此外,当栈再次增长时,运行时也无需做任何事情,我们只需要重用之前分配的空 闲空间即可。

六、栈是怎么拷贝的

拷贝栈听起来简单,但实际上它是一件有难度的事情。因为Go中栈上的变量都有自己的地址,一旦你拥有指向栈上变量的指针,这种情况下你就无法如你 所愿。当你移动栈时,指向原栈的指针都将变为无效指针。

幸运的是,只有在栈上分配的指针才能指向栈上的地址。这点对于内存安全是极其必要的,否则,程序可能会访问到已不再使用了的栈上的地址。

由于我们需要知道那些需要被垃圾收集器回收的指针的位置,因此我们知道栈上哪些部分是指针。当我们移动栈时,我们可以更新栈里地指针使其指向新的 目标地址,并且所有相关的指针都要被照顾到。

由于我们使用垃圾回收的信息来协助完成栈拷贝,因此所有出现在栈上的函数都必须具备这些信息。但事情不总是这样的。因为Go运行时的大部分代码是 用C编写的,大量的运行时调用没有指针信息可用,这样就无法进行拷贝。一旦这种情况发生,我们又不得不退回到分段栈方案,并接受为其付出的高昂代 价。

这就是当前Go运行时开发者大规模重写Go runtime的原因。那些无法用Go重写的代码,比如调度器和垃圾收集器的内核,将在一个特殊的栈上执行,这个特殊栈的size由runtime开发者 单独计算确定。

除了让栈拷贝成为可能之外,这个方法还会使得我们在未来能够实现出并发垃圾回收等特性。

七、关于虚拟内存

另外一种不同的栈处理方式就是在虚拟内存中分配大内存段。由于物理内存只是在真正使用时才会被分配,因此看起来好似你可以分配一个大内存段并让操 作系统处理它。下面是这种方法的一些问题

首先,32位系统只能支持4G字节虚拟内存,并且应用只能用到其中的3G空间。由于同时运行百万goroutines的情况并不少见,因此你很可 能用光虚拟内存,即便我们假设每个goroutine的stack只有8K。

第二,然而我们可以在64位系统中分配大内存,它依赖于过量内存使用。所谓过量使用是指当你分配的内存大小超出物理内存大小时,依赖操作系统保证 在需要时能够分配出物理内存。然而,允许过量使用可能会导致一些风险。由于一些进程分配了超出机器物理内存大小的内存,如果这些进程使用更多内存 时,操作系统将不得不为它们补充分配内存。这会导致操作系统将一些内存段放入磁盘缓存,这常常会增加不可预测的处理延迟。正是考虑到这个原因,一 些新系统关闭了对过量使用的支持。

八、结论

为了使goroutine使用代价更加低廉,更快速,适合更多task情况,Go开发组做出了很多努力。栈管理只是其中一小部分。如果你想了解更 多关于栈拷贝的细节,可以参考其设计文档。此外,如果你想了解更多有关Go运行 时重写的细节,这里有一个mail list

Outline 'memory layout'

So far we have arrived at the gate leading to the real kernel. And we’d better stop for a short break in order that we would have more energy to go ahead. Now let’s examine what we do to memory these days. 

Virtually what we want to do is drawing some pictures to describe the layout of the memory in various phases. For the layout is related to the bootloader, we’d better make our work based on the following assumption:
The machine has two systems installed (Windows XP and Linux) and uses LILO as the bootloader. Let us look at the LILO configuration:

/* LILO Configuration – /etc/lilo.conf */
boot=/dev/hda
map=/boot/map
install=/boot/boot.b
prompt
timeout=100
compact
default=Linux
image=/boot/vmlinuz-2.6.15.3
         label=Linux
         root=/dev/hda2
         read-only
other=/dev/hda1
         label=WindowsXP

Here the ‘boot=/dev/hda’ indicates it installed the LILO on the MBR of first hard disk. ‘root=/dev/had2′ indicates it installs linux system on the second partition of the first disk and ‘other=/dev/hda1′ indicates it installs windows system on the first partition of the first disk. Since lilo.conf is not read at boot time, the MBR needs to be "refreshed" when this is changed. If you do not do this upon rebooting, none of your changes to lilo.conf will be reflected at startup. Like getting LILO into the MBR in the first place, you need to run: ‘$ /sbin/lilo -v -v’. The ‘-v -v’ flags give you very verbose output.

Now we could switch on our machine! (‘<->’ means ‘begin from … end before …’)
1. Power on <-> BIOS routine
Chaos, that is the character of memory at this time.

2. BIOS routine <–> Bootloader 1st stage(MBR)
BIOS routine runs over and prepares to execute the code loaded from MBR. MBR contains the 1st stage bootloader of the LILO.

       |                        |
0A0000 +————————+
       |                        |
010000 +————————+
       |          MBR           | <- MBR (07C00 ~ 07E00)
001000 +————————+
       |                        |
000600 +————————+ 
       |      BIOS use only     |
000000 +————————+

3. Bootloader 1st stage(MBR) <-> Bootloader 2nd stage
The bootloader 1st stage moves itself to 0×090000, sets up the Real Mode stack (ranging from 0x09b000 to 0x09a200) and loads the 2nd stage of the LILO from 0x09b000.

          |                        |
0A0000 +————————+
       |      2nd bootloader    |
09b000 +————————+
       |     Real mode stack    |
09A200 +————————+
       |     1st bootloader     |
09A000 +————————+
       |                        |
010000 +————————+
       |      MBR(useless)      | <- MBR (07C00 ~ 07E00)
001000 +————————+
       |  Reserved for MBR/BIOS |
000800 +————————+
       |  Typically used by MBR |
000600 +————————+ 
       |      BIOS use only     |
000000 +————————+

4. Bootloader 2nd stage <-> setup.S
The 2nd bootloader copies the integrated boot loader of the kernel image to address 0×090000, the setup() code to address 0×090200, and the rest of the kernel image to address 0×00010000(called ‘low address’ for small Kernel Images compiled with ‘make zImage’) or 0×00100000(‘high address’ for big Kernel Images compiled with ‘make bzImage’).

zImage:

       |                        |
0A0000 +————————+
       |      2nd bootloader    |
09b000 +————————+
       |     Real mode stack    |
09A200 +————————+
       |     1st bootloader     |
09A000 +————————+
       |  Stack/heap/cmdline    | For use by the kernel real-mode code.
098000 +————————+ 
       |         Kernel setup   | The kernel real-mode code.
090200 +————————+
       |    Kernel boot sector  | The kernel legacy boot sector.
090000 +————————+
       |          zImage        | The bulk of the kernel image.
010000 +————————+
       |       MBR(useless)     | <- MBR (07C00 ~ 07E00)
001000 +————————+
       |  Reserved for MBR/BIOS |
000800 +————————+
       |  Typically used by MBR |
000600 +————————+
       |      BIOS use only     |
000000 +————————+

bzImage:

       +————————+
       |          bzImage       |
0100000+————————+
       |                        |
0A0000 +————————+
       |      2nd bootloader    |
09b000 +————————+
       |     Real mode stack    |
09A200 +————————+
       |     1st bootloader     |
09A000 +————————+
       |    Stack/heap/cmdline  | For use by the kernel real-mode code.
098000 +————————+ 
       |        Kernel setup    | The kernel real-mode code.
090200 +————————+
       |  Kernel boot sector    | The kernel legacy boot sector.
090000 +————————+
       |                        |
010000 +————————+
       |       MBR(useless)     | <- MBR (07C00 ~ 07E00)
001000 +————————+
       |  Reserved for MBR/BIOS |
000800 +————————+
       |  Typically used by MBR |
000600 +————————+
       |      BIOS use only     |
000000 +————————+

5. setup.S <-> head.S
The setup() checks the position of the Kernel Image loaded in RAM. If loaded "low" in RAM (when using zImage, at physical address 0×00010000) it is moved to "high" in RAM (at physical address 0×00001000). But, if the Kernel image is a "bzImage" loaded in "high" of RAM already, then it’s NOT moved anywhere. It also move the system to its rightful place (0×00000 ~ [<0x090000]). Some system parameters were placed from 0×090000 to 0×090200, which stores the legecy boot sector.

           +————————+
       |         bzImage        |
0100000+————————+
       |                        |
098000 +————————+ 
>       |      Kernel setup      |
090200 +————————+
       |     System parameters  | collected by setup()
090000 +————————+
       |                        |
       |                        |
       |          System        |
       |                        |
       |                        |
000000 +————————+

OK, it is much clear. and now we can walk through the door to the real kernel!

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats