文章目录
-
- 一、Linux线程创建的核心操作
- 二、线程栈在进程地址空间的位置
- 三、线程栈是否在栈区/动态内存区域
- 四、协程栈的位置
- 五、有栈协程和无栈协程的数据分布、创建&切换逻辑
- 六、有栈/无栈协程设计的原因
- 七、无栈协程无法在嵌套函数中暂停的原因
- 八、无栈协程需要编译器支持、有栈协程不需要的原因
-
- 核心需求
- 核心差异:执行模型对编译器的需求
- [1. 无栈协程依赖编译器的原因](#1. 无栈协程依赖编译器的原因)
- [2. 有栈协程无需编译器的原因](#2. 有栈协程无需编译器的原因)
- [特殊案例(Golang Goroutine)](#特殊案例(Golang Goroutine))
- 总结
一、Linux线程创建的核心操作
核心需求
了解Linux系统中创建线程的具体操作及核心逻辑。
核心操作(基于POSIX线程库pthreads)
- 环境准备 :引入
pthread.h头文件,编译时需链接-lpthread库(gcc 文件名.c -o 输出名 -lpthread)。 - 定义线程执行函数 :函数需符合
void* func(void*)格式,包含线程核心逻辑。 - 创建线程 :调用
pthread_create(),传入线程ID、线程属性(默认NULL)、执行函数、函数参数,需检查创建结果。 - 资源回收 :通过
pthread_join()等待子线程结束并回收资源(未设置分离状态时必须执行,否则产生僵尸线程);也可通过pthread_attr_setdetachstate()设置分离线程,自动回收资源。
底层原理
Linux无单独"线程"概念,线程本质是轻量级进程(LWP):内核为线程分配LWP,共享主线程地址空间/文件描述符等资源,分配独立栈空间(默认8MB),加入调度队列;线程结束后需pthread_join()通知内核回收LWP资源。
总结
- 线程创建需完成头文件引入、函数定义、
pthread_create()创建、pthread_join()回收核心步骤; - 编译必须链接
pthread库,未分离线程需手动回收资源; - Linux线程本质是共享资源的轻量级进程。
二、线程栈在进程地址空间的位置
核心需求
明确线程栈在Linux进程地址空间中的具体位置及分布特点。
进程地址空间整体布局(高→低地址)
内核空间 → 栈区(主线程栈) → 内存映射区(子线程栈/动态库/共享内存) → 堆区 → BSS段 → 数据段 → 代码段。
线程栈具体位置
- 主线程栈:位于进程默认栈区(内核空间下方),内核在进程启动时分配,默认8MB,向下增长。
- 子线程栈 :由pthread库通过
mmap从内存映射区分配,位于主线程栈下方、内存映射区上方,每个子线程有独立栈空间,默认8MB,可通过pthread_attr_setstacksize()修改。
关键特性
- 独立性:每个线程栈互不重叠,局部变量私有;
- 增长方向:从高地址向低地址增长,溢出触发
SIGSEGV段错误; - 保护机制:栈末尾预留Guard Page,防止栈覆盖其他内存区域。
总结
- 主线程栈在进程默认栈区,子线程栈在内存映射区(动态内存区域);
- 线程栈独立且向下增长,有大小限制,溢出会触发段错误。
三、线程栈是否在栈区/动态内存区域
核心需求
明确线程栈(尤其是子线程栈)是否属于进程默认栈区,还是动态内存区域。
核心结论
- 主线程栈:在进程默认栈区(内核分配);
- 子线程栈:由pthread库通过
mmap从内存映射区(动态内存区域)分配,属于动态内存,非传统栈区。
关键原因
进程默认栈区连续且向下增长,若子线程栈共用该区域,会导致栈溢出相互影响;内存映射区离散、可按需申请释放,适配多线程独立栈需求。
验证方式
通过pmap命令查看进程内存映射:主线程栈标注为stack,子线程栈标注为anon(匿名映射,属于内存映射区)。
总结
- 子线程栈是pthread库在内存映射区分配的动态内存,而非进程默认栈区;
- 主线程栈属于传统栈区,子线程栈与堆/动态库同属动态内存区域。
四、协程栈的位置
核心需求
明确协程栈在进程地址空间中的位置,及与线程栈的区别。
核心结论
协程是用户态轻量级线程,内核无感知,协程栈完全由协程库在用户态从堆区/内存映射区(动态内存) 分配,非进程默认栈区。
不同协程栈的分布
- 有栈协程 :协程库通过
malloc(堆)/mmap(内存映射区)分配独立动态栈(KB级),存储局部变量、栈帧等; - 无栈协程:无独立栈,执行状态(局部变量、暂停点)序列化到堆上的状态对象,复用调用者栈执行。
特殊案例(Golang Goroutine)
初始栈从内存映射区分配2KB匿名内存,栈不足时自动扩容(纯用户态操作),始终位于动态内存区域。
总结
- 协程栈位于堆/内存映射区(动态内存),由协程库用户态分配;
- 有栈协程有独立动态栈,无栈协程仅依赖堆上状态对象,复用调用者栈。
五、有栈协程和无栈协程的数据分布、创建&切换逻辑
核心需求
对比有栈/无栈协程的数据分布、创建流程、切换逻辑的核心差异。
1. 数据分布(进程地址空间视角)
| 类型 | 核心数据区 | 控制数据/执行依赖 |
|---|---|---|
| 有栈协程 | 独立动态栈(堆/mmap),存储局部变量、栈帧 | 堆上控制块(协程ID、栈指针、寄存器快照) |
| 无栈协程 | 堆上状态对象(存储局部变量、暂停点) | 复用调用者栈,无独立栈 |
2. 创建逻辑
有栈协程(以libco为例)
分配独立动态栈 → 初始化协程控制块(绑定栈/执行函数/参数) → 设置栈指针/程序计数器 → 加入就绪队列。
无栈协程(以Python生成器为例)
定义含暂停关键字的函数 → 调用函数创建堆上状态对象(存储字节码、变量槽、暂停点) → 初始化变量槽 → 进入暂停状态。
3. 切换逻辑
有栈协程
保存当前协程上下文(PC/SP/寄存器)→ 切换到调度器栈 → 恢复目标协程上下文 → 切换到目标协程独立栈继续执行(纯用户态上下文切换)。
无栈协程
执行到暂停点 → 将局部变量写入堆状态对象 → 记录暂停点 → 函数返回释放调用者栈 → 恢复时从状态对象读取变量到调用者栈 → 跳转到暂停点执行(状态序列化/反序列化)。
核心对比表
| 维度 | 有栈协程 | 无栈协程 |
|---|---|---|
| 数据分布 | 独立动态栈+堆控制块 | 仅堆状态对象,复用调用者栈 |
| 创建核心 | 分配栈+初始化执行上下文 | 创建状态对象+预分配变量槽 |
| 切换粒度 | 完整CPU上下文(PC/SP/寄存器) | 状态对象序列化/反序列化 |
| 执行独立性 | 支持嵌套函数暂停 | 仅能在自身函数内暂停 |
| 内存开销 | KB级(独立栈) | 字节级(仅状态对象) |
总结
- 有栈协程有独立动态栈,切换时保存完整上下文,支持嵌套暂停;
- 无栈协程无独立栈,依赖堆状态对象序列化,仅能在自身函数暂停;
- 二者核心差异是"是否有独立栈"和"切换粒度(上下文vs状态)"。
六、有栈/无栈协程设计的原因
核心需求
解释有栈/无栈协程两种设计出现的根本原因(场景权衡)。
1. 无栈协程设计初衷
为极致轻量性,解决简单异步场景(迭代、单次IO)的回调地狱问题:
- 优势:内存开销极低(百万级协程仅占几十MB),语法贴近同步代码;
- 取舍:牺牲执行灵活性,仅能在自身函数内暂停。
2. 有栈协程设计初衷
为执行灵活性,解决复杂异步场景(嵌套调用、多层IO)的并发问题:
- 优势:独立栈支持任意嵌套函数暂停,切换开销(几十ns)远低于线程;
- 取舍:容忍KB级内存开销,适配高并发、复杂业务逻辑。
设计演进逻辑
回调函数 → 无栈协程(简化回调,轻量) → 有栈协程(补充灵活性,适配复杂场景)。
总结
- 无栈协程为"轻量"而生,适配简单异步场景;
- 有栈协程为"灵活"而生,适配复杂异步场景;
- 二者互补,覆盖不同层级异步编程需求。
七、无栈协程无法在嵌套函数中暂停的原因
核心需求
解释无栈协程不支持嵌套函数暂停的根本原因。
核心前提:函数栈帧机制
函数调用时在调用者栈压入栈帧,执行完返回后栈帧销毁,栈帧仅临时存在。
关键原因
- 无独立栈:无栈协程复用调用者栈,嵌套函数栈帧随函数返回销毁,无法跨暂停点保留;
- 状态保存有限:仅能序列化当前函数的状态到堆对象,无法记录嵌套函数的栈帧、执行位置等信息。
对比有栈协程
有栈协程有独立动态栈,嵌套函数栈帧完整保存在该栈中,暂停时只需保存栈指针/寄存器,恢复时可直接回到嵌套函数暂停点。
总结
- 无栈协程复用调用者栈,嵌套函数栈帧临时且无法保留;
- 状态对象仅保存当前函数信息,无法捕获嵌套函数暂停点;
- 有栈协程的独立栈天然支持嵌套函数暂停。
八、无栈协程需要编译器支持、有栈协程不需要的原因
核心需求
解释无栈协程依赖编译器、有栈协程无需编译器的根本原因。
核心差异:执行模型对编译器的需求
| 类型 | 执行模型核心 | 编译器需求 |
|---|---|---|
| 无栈协程 | 拆分函数为执行片段,序列化状态到堆 | 必须改造函数执行逻辑 |
| 有栈协程 | 模拟线程执行流,切换上下文(SP/PC) | 无需改造函数执行逻辑 |
1. 无栈协程依赖编译器的原因
编译器需完成3项核心工作(运行时库无法实现):
- 拆分函数执行流:将单函数拆为暂停点分隔的多个执行片段;
- 自动序列化状态:生成堆状态对象,插入变量拷贝指令(栈→堆/堆→栈);
- 绑定协程框架:生成
promise_type/coroutine_handle等核心结构。
2. 有栈协程无需编译器的原因
- 函数执行逻辑无需修改:执行函数为普通函数,编译规则与普通函数一致;
- 切换依赖系统接口:通过
swapcontext等用户态接口切换上下文,无需编译器介入; - 独立栈是运行时行为:协程库在运行时分配动态栈,编译器无需感知。
特殊案例(Golang Goroutine)
需编译器支持(插入栈溢出检查指令),但这是栈扩容的特殊优化,非有栈协程通用需求。
总结
- 无栈协程需编译器拆分函数、序列化状态、绑定框架,属于"编译期改造";
- 有栈协程复用普通函数编译规则,仅运行时切换上下文,无需编译器支持;
- 核心差异是"是否需要改造函数执行逻辑"。