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));

 

上一篇:libco源码阅读(三):协程的创建和运行
下一篇:libco源码阅读(一):Hello World

发表评论

最新留言

路过按个爪印,很不错,赞一个!
[***.219.124.196]2025年03月27日 12时59分27秒