libco源码阅读(二):协程关键数据结构

    协程的创建和切换都是由用户控制的,那么协程切换时是如何保存上下文信息的呢,这一节我们介绍一下libco实现协程的关键数据结构。

1、协程实体:stCoRoutine_t

2、协程上下文信息:coctx_t

3、私有栈和共享栈:stack_mem&stShareStack_t

4、线程环境:stCoRoutineEnv_t

5、协程属性:stCoRoutineAttr_t


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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值