libco源码阅读(四):协程的上下文环境

 1. 函数栈的实现原理

    1.1 函数栈帧

    1.2 函数调用实例

2. 创建协程上下文环境:coctx_make

3.  协程的上下文环境切换:co_swap 

    3.1 上下文环境切换:coctx_swap


    在调用co_resume函数执行一个协程,需要调用两个函数,分别是:coctx_make和co_swap,前者用于创建协程的上下文环境,后者用于切换上下文环境。这两个函数是实现协程的关键。

在介绍创建协程的上下文环境和上下文切换时,我们先来介绍一下函数栈的实现原理。

1. 函数栈的实现原理

1.1 函数栈帧

    当一个程序运行的时候,Linux会为一个程序分配内存,内存区域包括代码段,数据段,堆,栈等,堆的地址从低地址向高地址增长,而栈的地址从高地址向低地址增长。下图是一个栈的结构。

            

      图中有两个画出了具体结构的栈帧,分别是函数 A 和函数 B。函数 A 的栈帧最上面有一块省略号标识的区域,该区域保存的是上一个栈帧的寄存器值以及函数 A 自己内部创建的局部变量。下面的参数 n 到参数 1 则是函数 A 要传给函数 B 的调用参数。那么函数 B 如何获取?答案是用寄存器。

    CPU 计算时会把很多变量放在寄存器中,根据硬件体系的不同,寄存器数量和作用也不同。一般在 x86 32位中,寄存器 %esp 保存了栈帧的栈顶指针值,而 %ebp 则保存栈帧栈底指针的值,所以通过 %esp 和 %ebp 就可以知道当前栈帧的头跟尾。除了这两个寄存器,还有其它一些通用寄存器(%eax%edx等),用于保存程序执行的临时值。在执行pushl指令时,表示往栈中压入一个值,此时%esp的值会往下减去4个字节。而执行popl指令时,表示从栈中弹出一个值,此时%esp的值会增加4个字节。总的来时%esp寄存器的值一直指向栈顶。

    了解了寄存器的基本知识后,下面我们就可以知道函数 B 如何获取到函数 A 传给它的参数了。参数 1 的地址是 %ebp + 8,参数 2 的地址是 %ebp + 12,参数 n 的地址是 %ebp + 4 + 4 * n。相信大家已经看明白,通过栈底指针往上找就可以取得这些参数,而这些参数之所以在这里当然是函数 A 预先准备好的。另外在所有参数的最下面保存着返回地址,这个是在函数 B 返回之后接下来要执行的指令的地址。

    看了函数 A 之后,再看看函数 B。在函数 B 的栈帧最上面是 被保存的 %ebp,这个指的是函数 A 的栈底指针,毕竟 %ebp 这个寄存器就一个,所以新的函数入栈的时候要先把老的保存起来,等函数出栈再恢复。在这个老的栈底指针下面则是其它需要保存的寄存器变量以及函数 B 自己内部用到的局部变量。再往下是 参数构造区域,也就是函数 B 即将调用另一个函数,在这里先把参数准备好。可以看出,函数 B 与函数 A 的栈帧结构是类似的。

1.2 函数调用实例

int caller()
{
    int arg1 = 534;
    int arg2 = 1057;

    int sum = swap_add(&arg1, &arg2);
    int diff = arg1 - arg2;

    return sum * diff;
}

int swap_add(int *xp, int *yp)
{
    int x = *xp;
    int y = *yp;

    *xp = y;
    *yp = x;

    return x + y;
}

    接下来我们一行一行分析这个程序,首先是caller函数,如下图所示,左边是汇编代码,右边是函数的调用栈。 

         

  首先看前三行代码:

pushl %ebp      // 保存旧的 %ebp
movl %esp, %ebp // 将 %ebp 设置为 %esp
subl $24, %esp  // 将 %esp 减 24 开辟栈空间

    这三行其实是为栈帧做准备工作。第一行保存旧的 %ebp,也就是caller外层函数的栈帧的栈底指针。此时新的栈空间还没有创建,但保存旧的 %ebp 的这一行空间将作为新栈帧的栈底,也就是栈帧的栈底指针,因此第二行将栈指针 %esp(永远指向栈顶)的值设置到 %ebp 上。 第三行将 %esp 下移 24 个字节,这一行其实就是为函数 caller 开辟栈空间了。从图中可以看出,下面的空间用于保存 caller 中的局部变量arg1和arg2,以及传给下个函数的参数。有部分空间未使用,这个是为了地址对齐,不影响我们的分析,可以忽略。

    在开辟了栈帧之后,就开始执行 caller 内部的逻辑了,caller 首先创建了两个局部变量(arg1arg2。对应的汇编代码为:

movl $534, -4(%ebp)
movl $1057, -8(%ebp)

    其中 -4(%ebp) 表示 %ebp - 4 的位置,也就是图中 arg1 所在的位置, arg2 的位置则是 %ebp - 8 的位置。这两行是把 534 和 1057 保存到传送到这两个位置上。继续往下是这几行:

leal -8(%ebp), %eax  // 把 %ebp - 8 这个地址保存到 %eax 
movl %eax, 4(%esp)   // 把 %eax 的值保存到 %esp + 4 这个位置上
leal -4(%ebp), %eax  // 把 %ebp - 4 这个地址保存到 %eax 
movl %eax, ($esp)    // 把 %eax 的值保存到 %esp 这个位置上

     第一行把 %ebp - 8 这个地址保存到 %eax 中,而 %ebp - 8 是 arg2 的地址,下一行把这个地址放到 %esp + 4 这个位置上,也就是图中 &arg2 的那个区域块。其实这一行是在为函数 swap_add 准备参数 &arg2,而下面两行则是准备参数 &arg1

    再下面一行是 call swap_add。这一行就是调用函数 swap_add 了,该指令会把函数的返回地址压入栈中,并将程序计数器PC设置为函数 swap_add的起始地址。这里的返回地址是函数 swap_add 返回后要接着执行的代码的地址,也就是 int diff = arg1 - arg2 地址。我们先进入 swap_add 函数,下面是对应的代码执行图:

    

pushl %ebp      // 保存旧的 %ebp
movl %esp, %ebp // 将 %ebp 设置为 %esp
pushl %ebx      // 保存 %ebx

    swap_add 对应的汇编代码的前三行与 caller 类似,同样是保存旧的帧指针,但是因为 swap_add 不需要保存额外的变量,只需要多用一个寄存器 %ebx,所以这里保存了这个寄存器的旧值,但是没有将 %esp 直接下移一段长度的操作。

movl 8(%ebp), %edx // 从 %ebp + 8 取值保存到 %edx
movl 12(%ebp), %ecx // 从 %ebp + 12 取值保存到 %ecx

 这两行分别是从 caller 中保存参数 &arg1 和 &arg2 的地方取得地址值,并根据地址取得 arg1arg2 的实际数值。

mov1 %edx, %ebx
mov1 %ecx, %eax
mov1 %eax, %edx
mov1 %ebx, %exc

    这 4 行是交换操作 。再看下面几行:

addl %ebx, %eax // 将返回值保存到寄存器 %eax 
pop %ebx
pop %ebp
ret

   函数 swap_add 的返回值保存在 %eax 中,一会儿 caller 就是从这个寄存器获取的。 swap_add 的最后几行是出栈操作,将 %ebx 和 %ebp 分别恢复为 caller 中的值。最后执行 ret 返回到 caller 中,ret指令会把返回地址从栈中弹出,并把程序计数器PC的值设置为返回地址的值。
   下面我们继续回到 caller 中,刚才执行到 call swap_add,下面几行是执行 int diff = arg1 - arg2,结果保存在 %edx 中。最后一行是计算 sum * diff,对应的汇编代码为 imull %edx, %eax。这里是把 %edx 和 %eax 的值相乘并且把结果保存到 %eax 中。在上面的分析中,我们知道 %eax 保存着 swap_add 的返回值,这里还是从 %eax 中取出返回值进行计算,并且把结果继续保存到 %eax 中,而这个值又是 caller 的返回值,这样调用 caller 的函数也可以从这个寄存器中获取返回值了。caller 函数的最后一行汇编代码是 ret,这会销毁 caller 的栈帧并且恢复相应寄存器的旧值。到此,caller 和 swap_add 这个函数的调用过程就全部分析完了。

2、协程函数:CoRoutineFunc

static int CoRoutineFunc( stCoRoutine_t *co,void * )
{
	if( co->pfn )
	{
		co->pfn( co->arg );
	}
	co->cEnd = 1; // 协程执行结束标识

	stCoRoutineEnv_t *env = co->env;

	co_yield_env( env );

	return 0;
}

// 协程执行结束,从线程环境栈减1,并切换到另外一个协程
void co_yield_env( stCoRoutineEnv_t *env )
{
	
	stCoRoutine_t *last = env->pCallStack[ env->iCallStackSize - 2 ];
	stCoRoutine_t *curr = env->pCallStack[ env->iCallStackSize - 1 ];

	env->iCallStackSize--;

	co_swap( curr, last);
}

3. 创建协程上下文环境:coctx_make

/* 用于分配coctx_swap两个参数内存区域的结构体,仅32位下使用,64位下两个参数直接由寄存器传递 */
struct coctx_param_t
{
	const void *s1;
	const void *s2;
};

int coctx_make( coctx_t *ctx,coctx_pfn_t pfn,const void *s,const void *s1 )
{
	//make room for coctx_param
    /*
    * ctx->ss_sp 对应的空间是在堆上分配的,在协程创建时初始化,地址是从低到高的增长,而栈是往低地址方向增长的,
    * 所以要使用这一块人为改变的栈帧区域,首先地址要调到最高位,即ss_sp + ss_size的位置
    */
	char *sp = ctx->ss_sp + ctx->ss_size - sizeof(coctx_param_t);
	sp = (char*)((unsigned long)sp & -16L); // 16字节对齐

    /* 栈中保存函数的参数 */
	coctx_param_t* param = (coctx_param_t*)sp ;
	param->s1 = s;
	param->s2 = s1;

	memset(ctx->regs, 0, sizeof(ctx->regs));

	ctx->regs[ kESP ] = (char*)(sp) - sizeof(void*); // 保存栈栈顶指针,kESP = 7
	ctx->regs[ kEIP ] = (char*)pfn; // 保存函数指针,kEIP = 0

  	//------- ss_sp + ss_size
	//|pading | 这里是对齐区域
	//|s2     |
	//|s1     |
	//|-------- <- 原esp 
	//|返回地址 |
	//|返回地址 |
	//|-------- <- sp(原esp - sizeof(void*) * 2)
	//|        |
	//--------- ss_sp

	return 0;
}

4.  协程的上下文环境切换:co_swap 

/* 当前准备让出CPU的协程叫做current协程,把即将调入执行的叫做 pending 协程 */
void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co)
{
 	stCoRoutineEnv_t* env = co_get_curr_thread_env();

	// 在函数头放一个局部变量,可以获取sp栈顶指针
	char c;
	curr->stack_sp= &c;

	if (!pending_co->cIsShareStack)
	{
		env->pending_co = NULL;
		env->ocupy_co = NULL;
	}
	else 
	{
		env->pending_co = pending_co;

		/* 获取当前占用共享栈的是哪个协程 */
		stCoRoutine_t* ocupy_co = pending_co->stack_mem->ocupy_co;

		/* 将共享栈的占用协程设置为即将换入的协程 */
		pending_co->stack_mem->ocupy_co = pending_co;

        /* 保存换出的协程 */
		env->ocupy_co = ocupy_co;
        
        /* 保存换出的协程的栈内容到协程实体的结构体中 */
		if (ocupy_co && ocupy_co != pending_co)
		{
			save_stack_buffer(ocupy_co);
		}
	}

	/* 切换协程的上下文 */
	coctx_swap(&(curr->ctx),&(pending_co->ctx) );

	// stack buffer may be overwrite, so get again;
	stCoRoutineEnv_t* curr_env = co_get_curr_thread_env();
	stCoRoutine_t* update_ocupy_co =  curr_env->ocupy_co;
	stCoRoutine_t* update_pending_co = curr_env->pending_co;
	
	if (update_ocupy_co && update_pending_co && update_ocupy_co != update_pending_co)
	{
		/* 将save_buffer中的栈内容复制到共享栈中 */
		if (update_pending_co->save_buffer && update_pending_co->save_size > 0)
		{
			memcpy(update_pending_co->stack_sp, update_pending_co->save_buffer, update_pending_co->save_size);
		}
	}
}

/* 将协程的共享栈内容保存到协程实体的结构体中 */
void save_stack_buffer(stCoRoutine_t* ocupy_co)
{
	///copy out
	stStackMem_t* stack_mem = ocupy_co->stack_mem;
	int len = stack_mem->stack_bp - ocupy_co->stack_sp;

	if (ocupy_co->save_buffer)
	{
		free(ocupy_co->save_buffer), ocupy_co->save_buffer = NULL;
	}

	ocupy_co->save_buffer = (char*)malloc(len); //malloc buf;
	ocupy_co->save_size = len;

	memcpy(ocupy_co->save_buffer, ocupy_co->stack_sp, len);
}

    coctx_swap执行完以后,CPU就跑去执行pendding中的代码了,也就是说执行完执行coctx_swap的这条语句后,下一条要执行的语句不是stCoRoutineEnv_t* curr_env = co_get_curr_thread_env();,而是pedding中的语句。这一点要尤其注意。那么什么时候执行coctx_swap这条语句之后的语句呢?就是在协程被其他地方执行co_resume了以后才会继续执行这里。后面就简单啦,切换出去的时候要把栈内容拷贝下来保存到一个buffer中,切换回来的时候把buffer中的内容拷贝到栈里面,这就是协程的执行过程。

4.1 上下文环境切换:coctx_swap

.globl coctx_swap
#if !defined( __APPLE__ )
.type  coctx_swap, @function
#endif
coctx_swap:

#if defined(__i386__)
	leal 4(%esp), %eax // 把%esp + 4的地址保存到%eax中
	movl 4(%esp), %esp // %esp 保存 %esp + 4地址指向的值
	leal 32(%esp), %esp // %esp = %esp + 32,此时%esp指向parm a : &regs[7] + sizeof(void*)

    // 接下来把所有的寄存器值保存到当前协程的8个寄存器数组中
	pushl %eax // esp ->parm a 
	pushl %ebp
	pushl %esi
	pushl %edi
	pushl %edx
	pushl %ecx
	pushl %ebx
	pushl -4(%eax)

    // 更新%esp的值
	movl 4(%eax), %esp // parm b -> &regs[0]

    // 把即将运行的协程的寄存器值从内存中弹出保存到CPU的寄存器中
	popl %eax  //ret func addr
	popl %ebx  
	popl %ecx
	popl %edx
	popl %edi
	popl %esi
	popl %ebp
	popl %esp
	pushl %eax //set ret func addr

	xorl %eax, %eax
	ret

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值