ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] ## 概述 https://zhuanlan.zhihu.com/p/254883122 ## 前期知识准备 1. 现代操作系统是分时操作系统,资源分配的基本单位是进程,CPU调度的基本单位是线程。 2. C++程序运行时会有一个运行时栈,一次函数调用就会在栈上生成一个record 3. 运行时内存空间分为全局变量区(存放函数,全局变量),栈区,堆区。栈区内存分配从高地址往低地址分配,堆区从低地址往高地址分配。 4. 下一条指令地址存在于指令寄存器IP,ESP寄存值指向当前栈顶地址,EBP指向当前活动栈帧的基地址。 5. 发生函数调用时操作为:将参数从右往左依次压栈,将返回地址压栈,将当前EBP寄存器的值压栈,在栈区分配当前函数局部变量所需的空间,表现为修改ESP寄存器的值。 6. 协程的上下文包含属于他的栈区和寄存器里面存放的值。 ## 何时挂起,唤醒协程? 如开始介绍时所说,协程是为了使用异步的优势,异步操作是为了避免IO操作阻塞线程。那么协程挂起的时刻应该是当前协程发起异步操作的时候,而唤醒应该在其他协程退出,并且他的异步操作完成时。 ## 如何挂起、唤醒协程,如何保护协程运行时的上下文? 协程发起异步操作的时刻是该挂起协程的时刻,为了保证唤醒时能正常运行,需要正确保存并恢复其运行时的上下文。 所以这里的操作步骤为: * 保存当前协程的上下文(运行栈,返回地址,寄存器状态) * 设置将要唤醒的协程的入口指令地址到IP寄存器 * 恢复将要唤醒的协程的上下文 ### 什么是上下文切换 即使是单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)。 CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换。 ### 线程上下文切换和进程上下文切换的区别 进程切换分两步 1.切换页目录以使用新的地址空间 2.切换内核栈和硬件上下文。 对于`linux`来说,线程和进程的最大区别就在于地址空间。 对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。所以明显是进程切换代价大 线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。 另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。 ### 上线文切换的实质 大概的切换流程如下: ``` 1. X86 32 Bists 2. SS --> 选择子--->段描述表-->(段限,段基址) 3. CR3 --->页目录,页表 4. ESP--> 5. EBP--> ``` 这其实和参数传递有点相似,只需要传递地址就够了。 `ESP,EBP` 正式 堆栈指针寄存器。 变量通过`ESP,EBP` 两个指针 加偏移量访问。 ## 开始动手制作一个协程库 业内实现的C/C++协程基本都采用`非对称`的协程方式。 glibc库的`context` ``` getcontext() : 获取当前context setcontext() : 切换到指定context makecontext() : 设置 函数指针 和 堆栈 到对应context保存的sp和pc寄存器中,调用之前要先调用 getcontext() swapcontext() : 保存当前context,并且切换到指定context ```