【问题标题】:Stack allocation for C++ green threadsC++ 绿色线程的堆栈分配
【发布时间】:2016-05-09 11:23:03
【问题描述】:

我正在研究 C++ 绿色线程,主要是 boost::coroutine2 和类似的 POSIX 函数,如 makecontext()/swapcontext(),并计划在 boost::coroutine2 之上实现 C++ 绿色线程库。两者都需要用户代码为每个新函数/协程分配一个堆栈。

我的目标平台是 x64/Linux。我希望我的绿色线程库适合一般用途,因此堆栈应根据需要扩展(合理的上限很好,例如 10MB),如果未使用太多内存时堆栈可以收缩(不需要),那就太好了)。我还没有找到合适的算法来分配堆栈。

经过一番谷歌搜索,我自己想出了几个选项:

  1. 使用编译器实现的拆分堆栈(gcc -fsplit-stack),但拆分堆栈有性能开销。由于性能原因,Go 已经放弃了拆分堆栈。
  2. mmap()分配一大块内存希望内核足够聪明,让物理内存保持未分配状态,仅在访问堆栈时分配。在这种情况下,我们受内核的支配。
  3. 使用mmap(PROT_NONE) 保留一个大内存空间并设置SIGSEGV 信号处理程序。在信号处理程序中,当SIGSEGV是由堆栈访问引起时(访问的内存在预留的大内存空间内),用mmap(PROT_READ | PROT_WRITE)分配需要的内存。这是这种方法的问题:mmap() 不是异步安全的,不能在信号处理程序中调用。它仍然可以实现,非常棘手:在程序启动期间创建另一个线程用于内存分配,并使用pipe() + read()/write() 将内存分配信息从信号处理程序发送到线程。

关于选项 3 的更多问题:

  1. 我不确定这种方法的性能开销,当内存空间由于数千个mmap() 调用而极度碎片化时,内核/CPU 的性能如何?
  2. 如果在内核空间中访问未分配的内存,这种方法是否正确?例如何时调用read()

对于绿色线程的堆栈分配还有其他(更好的)选项吗?在其他实现中如何分配绿色线程堆栈,例如围棋/Java?

【问题讨论】:

  • 虽然根据 POSIX,mmap 不是异步安全的,但它实际上在 Linux 和几乎所有合理、可用的 UNIX 变体中都是异步安全的。
  • @ChrisDodd 我能问一下为什么mmap 对绿色线程有好处吗?我不是专家,但我想知道。
  • @ChrisDodd 我没有找到任何手册页/链接,你介意给我一个链接吗?
  • FWIW,我不知道 Linux 共享内存是否适合您的需求。但是几年前我将它用于谷歌地图应用程序的高性能后端,并且性能非常好。
  • @ErikAlapää 绿色线程都运行在同一个内核线程上,因此它们共享同一个地址空间。 en.wikipedia.org/wiki/Green_threads

标签: c++ memory-management green-threads boost-coroutine


【解决方案1】:

glibc 为普通 C 程序分配堆栈的方式是使用专门为此目的设计的以下 mmap 标志来 mmap 一个区域:

   MAP_GROWSDOWN
          Used for stacks.  Indicates to the kernel virtual memory  system
          that the mapping should extend downward in memory.

为了兼容性,您可能也应该使用MAP_STACK。这样你就不用自己写 SIGSEGV 处理程序了,堆栈会自动增长。可以按照此处所述设置边界What does "ulimit -s unlimited" do?

如果你想要一个有界的堆栈大小,这通常是人们想要调用 sigaltstack(2) 时为信号处理程序所做的,只需发出一个普通的 mmap 调用。

Linux 内核总是映射支持虚拟页面的物理页面,在第一次访问页面时捕获页面错误(可能不是在实时内核中,但肯定在所有其他配置中)。有兴趣可以使用/proc/<pid>/pagemap接口(或者我写的这个工具https://github.com/dwks/pagemap)来验证一下。

【讨论】:

  • 我听说MAP_GROWSDOWN 会导致一些问题。今天仍然如此吗?
【解决方案2】:

为什么选择 mmap?当您使用 new(或 malloc)进行分配时,内存不会被触及,也绝对不会被映射。

const int STACK_SIZE = 10 * 1024*1024;
char*p = new char[STACK_SIZE*numThreads];

p 现在有足够的内存用于您想要的线程。当你需要内存时,开始访问 p + STACK_SIZE * i

【讨论】:

  • 这绝对不能保证被取消映射,或者特别是初始化为任何值。使用 GNU libc malloc,那么大的分配最终还是会调用 mmap()。
猜你喜欢
  • 2014-07-05
  • 2011-10-09
  • 2011-08-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-29
  • 2018-06-29
相关资源
最近更新 更多