bthread 是用户态协程,每个 bthread 需要自己独立的执行栈。与 pthread 的栈由操作系统分配不同,bthread 的栈由框架自行管理——分配、复用、回收。这是实现高并发(百万级 bthread)的关键:如果每个 bthread 都分配独立的栈且不复用,内存开销将是灾难性的。bthread 是如何高效管理这些栈的?让我们从内存布局开始。
一、栈的内存布局
bthread 的栈从高地址向低地址增长(x86/ARM 的默认方向)。bottom 指向栈空间的最高地址端(栈顶),bthread 从此处开始使用栈向下增长。
带 guard page 的布局(推荐方式):
低地址 高地址┌──────────────────┬──────────────────────────────┐│ guard page │ 栈空间 ││ (PROT_NONE) │ (PROT_READ|PROT_WRITE) ││ 不可读写 │ bthread 实际使用 │└──────────────────┴──────────────────────────────┘↑ ↑ ↑mmap 返回地址 栈底 栈顶 (bottom)
guard page 被设为 PROT_NONE(不可读/写/执行)。当 bthread 的栈用完了,继续向下写就会进入 guard page,CPU 立刻触发 SIGSEGV(段错误)。如果没有 guard page,栈溢出会静默覆盖相邻内存(比如其他 bthread 的栈),导致极难排查的 bug。
无 guard page 的布局:
低地址 高地址┌──────────────────────────────────────────────┐│ 栈空间 (malloc 分配) ││ 无溢出保护 │└──────────────────────────────────────────────┘↑ ↑mem bottom = mem + stacksize
不推荐——栈溢出时不会触发段错误,而是静默破坏相邻内存。
二、数据结构
2.1 StackStorage:栈内存的底层描述
struct StackStorage { unsigned stacksize; // 栈空间大小(不含 guard page) unsigned guardsize; // guard page 大小(0 = 无保护页) void* bottom; // 栈顶指针(高地址端) unsigned valgrind_stack_id; // Valgrind 栈注册 ID voidzeroize(){ // 清零(标记为无效状态) stacksize = 0; guardsize = 0; bottom = NULL; valgrind_stack_id = 0; }};
StackStorage 描述一块栈内存的元信息,不包含上下文(fcontext)。bottom 指向栈的最高地址,stacksize 是可用空间大小,guardsize 是保护页大小。
2.2 StackType:栈类型
enum StackType { STACK_TYPE_MAIN = 0, // 主栈:TaskGroup 的主栈(无实际内存) STACK_TYPE_PTHREAD, // pthread 栈:非 bthread 上下文(返回 NULL) STACK_TYPE_SMALL, // 小栈:默认 32KB STACK_TYPE_NORMAL, // 普通栈:默认 1MB(大多数 RPC 使用) STACK_TYPE_LARGE // 大栈:默认 8MB(深递归/大量局部变量)};
大小通过 GFlags 配置:FLAGS_stack_size_small、FLAGS_stack_size_normal、FLAGS_stack_size_large。
2.3 ContextualStack:带上下文的栈
struct ContextualStack { virtual ~ContextualStack() = default; bthread_fcontext_t context; // fcontext:CPU 寄存器状态 StackType stacktype; // 栈类型(决定归还到哪个 ObjectPool) StackStorage storage; // 底层栈内存};
每个 bthread 关联一个 ContextualStack。context(typedef void* bthread_fcontext_t)保存 CPU 寄存器状态——指令指针(IP)、栈指针(SP)、基址指针(BP)等。virtual ~ContextualStack() 使得子类 Wrapper 能被正确析构(ObjectPool 复用时需要)。
三、allocate_stack_storage:分配栈内存

intallocate_stack_storage(StackStorage* s, int stacksize_in, int guardsize_in){ const static int PAGESIZE = getpagesize(); const int PAGESIZE_M1 = PAGESIZE - 1; const int MIN_STACKSIZE = PAGESIZE * 2; // 最小栈:2 页 // 栈大小向上对齐到页大小倍数 const int stacksize = (std::max(stacksize_in, MIN_STACKSIZE) + PAGESIZE_M1) & ~PAGESIZE_M1; if (guardsize_in <= 0) { // ===== 无保护页路径:malloc ===== void* mem = malloc(stacksize); s->bottom = (char*)mem + stacksize; s->stacksize = stacksize; s->guardsize = 0; } else { // ===== 有保护页路径:mmap + mprotect ===== const int guardsize = (std::max(guardsize_in, PAGESIZE) + PAGESIZE_M1) & ~PAGESIZE_M1; const int memsize = stacksize + guardsize; // mmap 分配匿名私有内存 void* mem = mmap(NULL, memsize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); // 页对齐 void* aligned_mem = (void*)(((intptr_t)mem + PAGESIZE_M1) & ~PAGESIZE_M1); const int offset = (char*)aligned_mem - (char*)mem; // 保护页设为 PROT_NONE if (guardsize <= offset || mprotect(aligned_mem, guardsize - offset, PROT_NONE) != 0) { munmap(mem, memsize); return -1; } s->bottom = (char*)mem + memsize; s->stacksize = stacksize; s->guardsize = guardsize; } return 0;}
两条路径,由 guardsize 是否为 0 决定:
无保护页(malloc):简单直接,malloc(stacksize) 然后设置 bottom = mem + stacksize。没有溢出保护。
有保护页(mmap + mprotect):分配 stacksize + guardsize 的内存,将前 guardsize 字节设为 PROT_NONE。需要注意 mmap 返回地址可能不是页对齐的——通过 aligned_mem 对齐后计算 offset,实际保护页大小为 guardsize - offset。如果 offset >= guardsize,说明对齐偏移占满了保护页区域,放弃分配。
栈大小至少为 2 页(MIN_STACKSIZE = PAGESIZE * 2),并向上对齐到页大小的倍数。
四、deallocate_stack_storage:释放栈内存
voiddeallocate_stack_storage(StackStorage* s){ if (RunningOnValgrind()) { VALGRIND_STACK_DEREGISTER(s->valgrind_stack_id); } const int memsize = s->stacksize + s->guardsize; if ((uintptr_t)s->bottom <= (uintptr_t)memsize) { return; // bottom 为 NULL 或无效值(zeroize 后的状态) } s_stack_count.fetch_sub(1, butil::memory_order_relaxed); if (s->guardsize == 0) { free((char*)s->bottom - memsize); // malloc → free } else { munmap((char*)s->bottom - memsize, memsize); // mmap → munmap }}
释放是分配的逆操作,根据 guardsize 选择 free 或 munmap。bottom - memsize 计算出原始分配地址:
- malloc 路径:
bottom = mem + stacksize,所以 mem = bottom - stacksize = bottom - memsize(因为 guardsize = 0) - mmap 路径:
bottom = mem + memsize,所以 mem = bottom - memsize
安全检查 bottom <= memsize 防止对 zeroize 后的 StackStorage 进行释放操作——zeroize 将 bottom 设为 NULL。
全局栈计数器 s_stack_count 通过 bvar "bthread_stack_count" 暴露,便于监控。
五、StackFactory 模板体系
栈的管理采用模板工厂模式。每种栈类型(Small/Normal/Large)对应一个 StackClass,StackFactory<StackClass> 负责创建和回收。
5.1 StackClass:栈大小类
struct MainStackClass {}; // 主栈,无实际内存struct SmallStackClass { static int* stack_size_flag; // → FLAGS_stack_size_small (32KB) static const int stacktype = STACK_TYPE_SMALL;};struct NormalStackClass { static int* stack_size_flag; // → FLAGS_stack_size_normal (1MB) static const int stacktype = STACK_TYPE_NORMAL;};struct LargeStackClass { static int* stack_size_flag; // → FLAGS_stack_size_large (8MB) static const int stacktype = STACK_TYPE_LARGE;};
每种 StackClass 通过 stack_size_flag 指向对应的 GFlag,运行时可以通过命令行调整栈大小。
5.2 StackFactory::Wrapper:ObjectPool 管理的栈对象
template <typename StackClass> struct StackFactory { struct Wrapper : public ContextualStack { explicitWrapper(void (*entry)(intptr_t)){ // 1. 分配栈内存 if (allocate_stack_storage(&storage, *StackClass::stack_size_flag, FLAGS_guard_page_size) != 0) { storage.zeroize(); context = NULL; return; } // 2. 在新栈上创建初始上下文 context = bthread_make_fcontext( storage.bottom, storage.stacksize, entry); stacktype = (StackType)StackClass::stacktype; // 3. ASan 标记为"中毒"(归还后不应访问) BTHREAD_ASAN_POISON_MEMORY_REGION(storage); } ~Wrapper() { if (context) { context = NULL; BTHREAD_ASAN_UNPOISON_MEMORY_REGION(storage); deallocate_stack_storage(&storage); storage.zeroize(); } } }; // ...};
Wrapper 继承 ContextualStack,构造时完成两件事:
- 分配栈内存:调用
allocate_stack_storage - 创建 fcontext:调用
bthread_make_fcontext(bottom, stacksize, entry),在新栈上设置初始上下文。entry 是首次 jump 时的入口函数(通常是 task_runner)
构造时将栈标记为 ASan "中毒"——因为 ObjectPool 获取对象时会调用构造函数,此时栈内存还没有被 bthread 使用。
5.3 get_stack 与 return_stack
// 获取栈:从 ObjectPool 取(复用)或新建static ContextualStack* get_stack(void (*entry)(intptr_t)) { ContextualStack* cs = butil::get_object<Wrapper>(entry); BTHREAD_ASAN_UNPOISON_MEMORY_REGION(cs->storage); // 标记为可访问 return cs;}// 归还栈:放回 ObjectPool,不释放内存static void return_stack(ContextualStack* cs) { BTHREAD_ASAN_POISON_MEMORY_REGION(cs->storage); // 标记为不可访问 butil::return_object(static_cast<Wrapper*>(cs));}
栈的复用通过 ObjectPool(参见本系列第二篇)实现:get_object<Wrapper> 优先从 TLS 缓存取,取不到才触发 Wrapper 构造(分配新栈)。return_object 将 Wrapper 放回 TLS 缓存——不释放内存,等待下一个 bthread 复用。
关键细节:get_stack 的 entry 参数只在首次创建时使用。复用时栈上已有 fcontext(指向创建时的 entry)。新 bthread 可能有不同的 entry,这个问题通过 jump_stack 的机制解决——jump_stack 不跳到 entry,而是跳到 fcontext 中保存的 IP(上次 yield 的位置)。新 bthread 的首次跳转通过 TaskGroup 中的特殊处理实现。
5.4 MainStackClass 特化
template <> struct StackFactory<MainStackClass> { static ContextualStack* get_stack(void (*)(intptr_t)) { ContextualStack* s = new ContextualStack; s->context = NULL; // 无 fcontext s->stacktype = STACK_TYPE_MAIN; s->storage.zeroize(); // 无栈内存 return s; } static void return_stack(ContextualStack* s) { delete s; // 不走 ObjectPool }};
主栈是 TaskGroup 在 pthread 原生栈上运行的"虚拟栈"——不需要分配实际内存,不需要 fcontext。用 new/delete 管理,不经过 ObjectPool。
六、get_stack/return_stack:按类型分发
inline ContextualStack* get_stack(StackType type, void (*entry)(intptr_t)){ switch (type) { case STACK_TYPE_PTHREAD: return NULL; case STACK_TYPE_SMALL: return StackFactory<SmallStackClass>::get_stack(entry); case STACK_TYPE_NORMAL: return StackFactory<NormalStackClass>::get_stack(entry); case STACK_TYPE_LARGE: return StackFactory<LargeStackClass>::get_stack(entry); case STACK_TYPE_MAIN: return StackFactory<MainStackClass>::get_stack(entry); } return NULL;}inlinevoidreturn_stack(ContextualStack* s){ if (NULL == s) return; switch (s->stacktype) { case STACK_TYPE_PTHREAD: assert(false); return; case STACK_TYPE_SMALL: return StackFactory<SmallStackClass>::return_stack(s); case STACK_TYPE_NORMAL: return StackFactory<NormalStackClass>::return_stack(s); case STACK_TYPE_LARGE: return StackFactory<LargeStackClass>::return_stack(s); case STACK_TYPE_MAIN: return StackFactory<MainStackClass>::return_stack(s); }}
按 StackType 分发到对应的 StackFactory。关键点:不同类型的栈在不同的 ObjectPool 中管理——Small 栈不会混入 Normal 栈的池,避免大小不匹配。pthread 类型返回 NULL(使用 pthread 自身栈)。
七、jump_stack:上下文切换
inlinevoidjump_stack(ContextualStack* from, ContextualStack* to){bthread_jump_fcontext(&from->context, to->context,0);}
这是 bthread 上下文切换的核心操作,底层是汇编实现(boost.context 的移植版本)。执行过程:
- 保存当前状态:将当前 CPU 寄存器(IP、SP、BP、callee-saved 寄存器)保存到
from->context - 恢复目标状态:从
to->context 恢复 CPU 寄存器 - 跳转执行:跳转到
to 栈上次保存的位置继续执行 - 返回:当
to 栈后续跳回 from 栈时,从本函数返回
第三个参数 0 表示"不跳过剩余代码"。整个切换过程是纯用户态的——不涉及系统调用,代价极低(约几十纳秒)。
八、ASan 纤维追踪
当启用 ASan(AddressSanitizer)时,栈切换需要特殊处理。ASan 默认假设每个线程只有一个栈,但 bthread 在同一个 pthread 上切换多个协程栈。如果不通知 ASan,它会误报"use-after-scope"等错误。
bthread 通过宏注解解决:
// 归还栈时:标记栈内存为"中毒"(不可访问)BTHREAD_ASAN_POISON_MEMORY_REGION(storage);// 获取栈时:标记栈内存为"解毒"(可访问)BTHREAD_ASAN_UNPOISON_MEMORY_REGION(storage);// 切换栈时:通知 ASan 切换到新纤程ScopedASanFiberSwitcher switcher(next_storage);
未启用 ASan 时,这些宏是空操作,零开销。
九、ObjectPool 配置特化
三种栈类型各有独立的 ObjectPool 配置:
// 每个 Block 最多 64 个栈对象(三种类型相同)template <> struct ObjectPoolBlockMaxItem<...Wrapper> { static const size_t value = 64;};// TLS 缓存上限(不同类型不同)template <> struct ObjectPoolFreeChunkMaxItem<SmallStackClass::Wrapper> { staticsize_tvalue(){ return FLAGS_tc_stack_small; } // 默认 32};template <> struct ObjectPoolFreeChunkMaxItem<NormalStackClass::Wrapper> { staticsize_tvalue(){ return FLAGS_tc_stack_normal; } // 默认 8};template <> struct ObjectPoolFreeChunkMaxItem<LargeStackClass::Wrapper> { staticsize_tvalue(){ return 1; } // 大栈只缓存 1 个};// 验证器:从池中取出时检查 context 是否有效template <> struct ObjectPoolValidator<...Wrapper> { staticboolvalidate(const Wrapper* w){ return w->context != NULL; }};
设计考量:
- 小栈缓存多(32 个):小栈只有 32KB,缓存 32 个才 1MB,内存开销小但减少了大量分配
- 普通栈缓存较少(8 个):普通栈 1MB,缓存 8 个就是 8MB,适度
- 大栈只缓存 1 个:大栈 8MB,多缓存会浪费大量内存
- 验证器:
allocate_stack_storage 失败时 context 为 NULL,从池中取出时跳过无效对象
十、总结
bthread 栈管理的设计可以归纳为一句话:通过 StackFactory 模板体系将不同大小的栈分离到各自的 ObjectPool 中,用 mmap+mprotect 实现 guard page 防溢出,用 fcontext 实现用户态上下文切换。
四个关键设计:
1. 三级栈大小 + 独立 ObjectPool。 Small(32KB)、Normal(1MB)、Large(8MB)三种大小满足不同场景。每种类型有独立的 ObjectPool,不会大小混用。栈被 bthread 结束后归还到 ObjectPool,新 bthread 直接复用——避免频繁的 mmap/munmap。
2. Guard page 防溢出。 用 mmap 分配 stacksize + guardsize 的内存,将 guard page 设为 PROT_NONE。栈溢出时访问 guard page 触发 SIGSEGV,防止静默破坏相邻内存。无保护页的 malloc 路径虽然简单,但不推荐。
3. StackFactory 模板体系。 通过模板参数 StackClass 将栈大小和类型参数化,StackFactory<StackClass>::Wrapper 在构造时完成"分配栈内存 + 创建 fcontext",在析构时释放栈内存。get_stack/return_stack 通过 ObjectPool 实现栈的复用。
4. fcontext 用户态切换。jump_stack 通过汇编实现(boost.context 移植),保存/恢复 CPU 寄存器,纯用户态操作,不涉及系统调用。这使得 bthread 的上下文切换代价极低(几十纳秒),是实现百万级并发的基础。
本文基于 Apache brpc 源码(src/bthread/stack.h、src/bthread/stack_inl.h、src/bthread/stack.cpp)撰写。