
libco源码阅读(二):协程关键数据结构
发布日期:2021-05-07 13:28:35
浏览次数:22
分类:原创文章
本文共 4218 字,大约阅读时间需要 14 分钟。
协程的创建和切换都是由用户控制的,那么协程切换时是如何保存上下文信息的呢,这一节我们介绍一下libco实现协程的关键数据结构。
1、协程实体:stCoRoutine_t
这个结构实际上就是就是协程的主体结构,存储着一个协程相关的数据,每个协程对应一个stCoRoutine_t,它保存这协程的私有数据和协程切换时的上下文信息。每当调用co_create()创建一个协程时,都会初始化这个结构体。
struct stCoRoutine_t{ stCoRoutineEnv_t *env; // 协程的执行环境, 运行在同一个线程上的各协程是共享该结构 pfn_co_routine_t pfn; // 一个函数指针, 指向实际待执行的协程函数 void *arg; // 函数的参数 coctx_t ctx; // 用于协程切换时保存CPU上下文(context)的,即 esp、ebp、eip 和其他通用寄存器的值 char cStart; // 协程是否执行过resume char cEnd; // 协程是否执行结束 char cIsMain; // 是否为主协程修改 char cEnableSysHook; // 此协程是否hook库函数,即用自己实现的函数替代库函数 char cIsShareStack; // 是否开启共享栈模式 void *pvEnv; // 保存程序系统环境变量的指针 //char sRunStack[ 1024 * 128 ]; stStackMem_t* stack_mem; // 协程运行时的栈内存 //save satck buffer while confilct on same stack_buffer; char* stack_sp; // 保存栈顶指针sp unsigned int save_size; // 保存协程的栈的buffer的大小 char* save_buffer; // 使用共享栈模式时,用于保存该协程的在共享栈中的内容 stCoSpec_t aSpec[1024];};
2、协程上下文信息:coctx_t
这个结构保存协程的上下文,实际就是寄存器的值,不管是C还是C++都没有函数可以直接接触寄存器,所以操作这个参数的时候需要嵌入一点汇编代码。
struct coctx_t{#if defined(__i386__) void *regs[ 8 ]; // X86架构下有8个通用寄存器#else void *regs[ 14 ]; // x64位下有16个寄存器,这里保存14个#endif size_t ss_size; // 栈的大小 char *ss_sp; // 栈顶指针esp };// 32 bit// | regs[0]: ret |// | regs[1]: ebx |// | regs[2]: ecx |// | regs[3]: edx |// | regs[4]: edi |// | regs[5]: esi |// | regs[6]: ebp |// | regs[7]: eax | = esp// 64 bit//low | regs[0]: r15 |// | regs[1]: r14 |// | regs[2]: r13 |// | regs[3]: r12 |// | regs[4]: r9 |// | regs[5]: r8 | // | regs[6]: rbp |// | regs[7]: rdi |// | regs[8]: rsi |// | regs[9]: ret | //ret func addr// | regs[10]: rdx |// | regs[11]: rcx | // | regs[12]: rbx |//hig | regs[13]: rsp |
x86-64的16个64位寄存器分别是:%rax, %rbx, %rcx, %rdx, %esi, %edi, %rbp, %rsp, %r8-%r15。其中:
- %rax 作为函数返回值使用;
- %rsp栈指针寄存器,指向栈顶;
- %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数;
- %rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者保护规则,简单说就是随便用,调用子函数之前要备份它,以防被修改;
- %r10,%r11 用作数据存储,遵循调用者保护规则,简单说就是使用之前要先保存原值;
我们来看看两个陌生的名词调用者保护&被调用者保护:
- 调用者保护:表示这些寄存器上存储的值,需要调用者(父函数)自己想办法先备份好,否则过会子函数直接使用这些寄存器将无情的覆盖。如何备份?当然是实现压栈(pushl),等子函数调用完成,再通过栈恢复(popl);
- 被调用者保护:即表示需要由被调用者(子函数)想办法帮调用者(父函数)进行备份;
3、私有栈和共享栈:stack_mem&stShareStack_t
stack_mem是运行协程私有栈的结构,stShareStack_t则是共享栈的结构。libco有两种协程栈的策略:
- 一种是一个协程分配一个栈,这也是默认的配置,不过缺点十分明显,因为默认大小为128KB,如果1024个协程就是128MB,1024*1024个协程就是128GB,好像和协程“千万连接”相差甚远。且这些空间中显然有很多的空隙,可能很多协程只用了1KB不到,这显然是一种极大的浪费。
- 另一种策略为共享栈,即所有协程使用同一个栈,然后每个协程使用一个buffer来保存自己的栈内容,这个buffer大小不固定,因此可以节省内存。libco在进行协程切换的时候,先把共享栈的内容复制到要换出的协程实体的结构体buffer中,把即将换入的协程实体的结构体中的buffer内容复制到共享栈中。这样一个线程所有的协程在运行时使用的确实是同一个栈,也就是我们所说的共享栈了。使用共享栈模式时,需要我们在创建协程的时候在co_create中指定第二个参数,这种方法是多个协程共用一个栈,但是在协程切换的时候需要拷贝已使用的栈空间。
struct stStackMem_t{ stCoRoutine_t* ocupy_co; // 执行时占用的那个协程实体,也就是这个栈现在是那个协程在用 int stack_size; // 当前栈上未使用的空间 char* stack_bp; // stack_buffer + stack_size char* stack_buffer; // 栈的起始地址,当然对于主协程来说这是堆上的空间};struct stShareStack_t{ unsigned int alloc_idx; // stack_array中我们在下一次调用中应该使用的那个共享栈的index int stack_size; // 共享栈的大小,这里的大小指的是一个stStackMem_t*的大小 int count; // 共享栈的个数,共享栈可以为多个,所以以下为共享栈的数组 stStackMem_t** stack_array; // 栈的内容,这里是个数组,元素是stStackMem_t*};
4、线程环境:stCoRoutineEnv_t
stCoRoutineEnv_t是一个非常关键的结构,这个结构是所有数据中最特殊的一个,因为它是一个线程内所有协程共享的结构,也就是说同一个线程创建的所有协程的此结构指针指向同一个数据。其中存放了一些协程调度相关的数据,当然叫调度有些勉强,因为libco实现的非对称式协程实际上没有什么调度策略,完全就是协程切换会调用这个协程的协程或者线程。
struct stCoRoutineEnv_t{ stCoRoutine_t *pCallStack[ 128 ]; // 协程的调用栈 int iCallStackSize; // 调用栈的栈顶指针 stCoEpoll_t *pEpoll; // epoll的一个封装结构 //for copy stack log lastco and nextco stCoRoutine_t* pending_co; // 目前占用共享栈的协程 stCoRoutine_t* ocupy_co; // 与pending在同一个共享栈上的上一个协程};
- pCallStack : 如果将协程看成一种特殊的函数,那么这个 pCallStack 就时保存这些函数的调用链的栈。非对称协程最大特点就是协程间存在明确的调用关系;甚至在有些文献中,启动协程被称作 call,挂起协程叫 return。非对称协程机制下的被调协程只能返回到调用者协程,这种调用关系不能乱,因此必须将调用链保存下来。
- pending_co和ocupy_co:对上次切换挂起的协程和嵌套调用的协程栈的拷贝,为了减少共享栈上数据的拷贝。在不使用共享栈模式时 pending_co 和 ocupy_co 都是空指针。
5、协程属性:stCoRoutineAttr_t
协程属性的结构体stCoRoutineAttr_t标记了栈的大小和是否使用共享栈。
struct stCoRoutineAttr_t{ int stack_size; // 协程的私有栈或者共享栈大小 stShareStack_t* share_stack; // 指向协程的共享栈 stCoRoutineAttr_t() { stack_size = 128 * 1024; share_stack = NULL; }}__attribute__ ((packed));
发表评论
最新留言
路过按个爪印,很不错,赞一个!
[***.219.124.196]2025年03月27日 12时59分27秒
关于作者

喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!
推荐文章
Python 面向对象进阶
2021-05-09
Linux常用统计命令之wc
2021-05-09
Git安装及使用以及连接GitHub方法详解
2021-05-09
docker容器与虚拟机的区别
2021-05-09
shell脚本里使用echo输出颜色
2021-05-09
Python2跟Python3的区别
2021-05-09
并发编程——IO模型详解
2021-05-09
Java之封装,继承,多态
2021-05-09
wait()与notify()
2021-05-09
使用js打印时去除页眉页脚
2021-05-09
Spring security OAuth2.0认证授权学习第二天(基础概念-RBAC)
2021-05-09
ORA-00904: "FILED_TYPE": 标识符无效
2021-05-09
Redis系统学习之Redis性能测试工具
2021-05-09
数据仓库系列之维度建模
2021-05-09
Scala教程之:函数式的Scala
2021-05-09
java中DelayQueue的使用
2021-05-09
java程序员从小工到专家成神之路(2020版)-持续更新中,附详细文章教程
2021-05-09
线程stop和Interrupt
2021-05-09
Android中定时执行任务的3种实现方法
2021-05-09
nodejs中npm常用命令
2021-05-09