libco协程库源码解析

一、什么是协程?
    进程:操作系统资源分配的最小单位,进程有独立的地址空间,每个进程都有自己的数据段、代码段和堆栈段。进程的上下文切换需要对寄存器、内核堆栈、虚拟内存、文件句柄等进行切换,因此切换开销比较大,但相对比较稳定安全。
    线程:操作系统调度的最小单位,拥有自己独立的程序计数器、寄存器和线程栈。因为同一个进程内的线程共享同一个虚拟内存空间,因此线程的切换不需要对虚拟内存空间和内核堆栈进行切换,只需要对程序计数器、寄存器、线程栈等资源进行切换。上下文切换快,资源开销较小。
    协程:一种用户态的线程,协程是一种在用户态模拟线程上下文切换的轻量级线程,协程拥有自己的寄存器上下文和栈。协程切换时,将寄存器上下文和栈保存到其他地方,在切回来时,恢复先前保存的寄存器上下文和栈,直接操作栈不需要陷入内核,因此所消耗的资源更少,切换的效率更高。

二、libco是如何实现协程的?
    通过hook socket系统调用函数,将阻塞的系统IO调用改为异步调用。基于epoll/kqueue等IO复用方式进行网络事件驱动。
    把协程的让出与恢复作为异步网络IO中的一次事件注册与回调。当业务处理遇到同步网络请求的时候,libco层会把本次网络请求注册为异步事件,当前的协程让出CPU占用,CPU交给其它协程执行。在网络事件发生或者超时的时候,libco会自动的恢复协程执行。

1、libco的整体框架
libco由三层组成:libco接口层、系统函数hook层、事件驱动层。
接口层:主要提供对外使用的接口函数,创建、删除、启动、挂起、切换协程等。
hook层:将阻塞的网络io系统函数改为异步的调用。
事件驱动层:注册、删除网络事件驱动,时间轮盘定时器。

2、主要的数据结构

struct stCoRoutine_t{};      //表示一个协程
struct stCoRoutineEnv_t{};   //一个线程内协程环境的总体信息
struct stCoRoutineAttr_t{};  //协程栈的属性
struct coctx_t{};            //协程切换时保存的协程上下文
struct stStackMem_t{};       //协程栈的内存

3、主要方法

// co_create()创建一个协程
int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg );
@ppco:保存生成协程
@attr:协程栈大小(128KB~8M),如果为NULL,默认为128KB
@pfn:协程实际执行的函数
@arg:函数的参数

// 协程启动
void co_resume( stCoRoutine_t *co );
// 协程挂起
void co_yield( stCoRoutine_t *co );
void co_yield_ct(); //ct:current thread
// 协程切换
void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co);  //调用coctx_swap()
// 协程删除
void co_release( stCoRoutine_t *co );

4、协程的启动和挂起
4.1、创建和开启协程

当前线程调用下面函数开启一个协程
co_create()
co_resume()
co_eventloop()

1、第一次调用co_create()时,执行如下:
co_init_curr_thread_env()
[1.1] 创建协程环境stCoRoutineEnv_t *env,存放在g_arrCoEnvPerThread[ pid ]
[1.2] 创建当前线程的主协程
[1.3] 将主协程存放在env->pCallStack[0]
[1.4] 创建epoll,并将epoll存放在env->pEpoll
co_create_env()
[1.5] 创建第一个协程stCoRoutine_t *lp
[1.6] 将协程的lp->env指向g_arrCoEnvPerThread[ pid ]存放的env

2、调用co_resume()
[2.1] 取出当前协程块控制指针(这里就是主协程)
[2.2] 当该协程是第一次启动,则构造该协程CPU上下文coctx_t ctx
[2.3] 将待启动的协程(这里指创建的第一个协程)存放在env->pCallStack[1]
co_swap()
[2.4] 将新协程(第一个协程)的CPU上下文换入到寄存器,开始执行

co_resume( stCoRoutine_t *co )执行流程
1、获取当前协程co->env->pCallStack[ env->iCallStackSize - 1 ]
2、将co保存到env->pCallStack[ env->iCallStackSize++ ]
3、通过coctx_swap()将当前协程挂起,将协程co加载



4.2、挂起协程

挂起协程
co_yield( stCoRoutine_t *co )  // 挂起指定协程co所在的协程队列中的最后一个协程,执行倒数第二个协程
co_yield_ct()  // 挂起当前协程所在的协程队列中的最后一个协程,执行倒数第二个协程
这两个函数都是调用co_yield_env() -> co_swap( curr, last);

co_yield_ct()执行流程
1、找出当前线程所对应协程的执行环境
2、取出当前协程,即env->pCallStack[ env->iCallStackSize - 1 ]
3、取出上一个协程,env->pCallStack[ env->iCallStackSize - 2 ]
4、通过coctx_swap()将当前协程挂起,将上一个协程加载

4.3、协程挂起的三种情况
协程挂起yield三种情况:
(1)主动调用co_yield_ct()
(2)调用poll()或co_cond_timedwait()陷入阻塞等待
(3)调用connect(),read(),write(),recv(),send()等系统调用陷入阻塞等待

4.4、协程重新启动的三种情况
协程重新启动resume三种情况:
(1)主动调用co_resume()
(2)poll()事件就绪或超时,co_cond_timedwait()等到了其他协程的co_cond_signal()通知信号或等待超时
(3)read(),write()等I/O接口成功读到或写入数据,或者读写超时

5、如何进行协程上下文切换
5.1、x86-64下函数调用栈帧原理

5.2、coctx_swap()流程详解
coctx_swap.S汇编语言实现coctx_swap( coctx_t *,coctx_t* ),
struct coctx_t
{
  void *regs[ 14 ];  // 用来保存寄存器信息的数组
  ... ...
};

coctx_t *cur_ctx, *new_ctx;
coctx_swap(cur_ctx, new_ctx),cur_ctx代表当前运行的协程上下文,new_ctx是将要切换的协程上下文

coctx_swap( coctx_t *,coctx_t* )
实现两个协程上下文的切换,每个coctx_t中有一个包含14个元素的数组,用来保存寄存器信息。
在进行协程上下文切换的时候,先把当前运行协程的寄存器信息保存到数组cur_ctx.regs中,
然后把将要切换的协程上下文信息从new_ctx.regs中拿出来保存到寄存器中,从new_ctx上次挂起的地方开始执行。

6、epoll和定时器
# 定时器
[1]向定时器中注册一个定时事件(指定事件的触发时间)
[2]到达触发时间点,收到定时器的通知
定时器包含两部分:
(1)保存已注册定时事件的数据结构(红黑树、时间轮等)
(2)定时通知机制

# co_eventloop() 使用epoll + 时间轮实现定时器
(1)调用epoll_wait()等待I/O就绪事件,最大等待时长设置为1ms
(2)处理I/O就绪事件
(3)从时间轮取出超时事件,放到timeout队列
(4)处理超时事件,循环处理timeout队列中的定时任务
(5)如果timeout队列为空,跳转到(1),继续事件循环

7、libco中example分析
7.1、程序主动调用yield和resume协程,相当于libco使用者承担了部分的协程调度工作。(example_echosvr.cpp)

7.2、生产者和消费者协程在poll和co_cond_timedwait()中切换。(example_cond.cpp)
    [1] Consumer():当消费者协程首先启动时,它发现任务队列是空的,于是调用co_cond_timedwait()在条件变量cond上阻塞等待。当任务队列不为空时,env->task_queue.pop()。
    co_cond_timedwait():设置回调函数pfnProcess = OnSignalProcessEvent,用于未来唤醒当前的协程将当前协程挂入条件变量的等待队列上,如果wait的timeout大于0,还要向当前执行环境的定时器上注册一个定时事件(即挂到时间轮上AddTimeout),co_yield_ct()挂起协程。

    [2] Producer():当生产者协程启动时,它会向任务队列里投放一个任务task_queue.push(task)并调用co_cond_signal()通知消费者,然后再调用poll()在原地阻塞等待1000ms。
    co_cond_signal():将条件变量等待队列里的协程拿出来,然后挂到当前执行环境的stTimeoutItemLink_t队列pstActiveList。
    poll():设置回调函数pfnProcess = OnPollProcessEvent,用于未来唤醒当前的协程将自己作为一个定时事件注册到当前执行环境的定时器(epoll add),设置回调函数pfnPrepare = OnPollPreparePfn设置1000ms的超时时间
    co_yield_env()将CPU让给主协程

    [3] CPU控制权回到了主协程手中。主协程就是事件循环co_eventloop()函数。
    co_eventloop():循环调用epoll_wait(),当有就绪I/O事件就处理I/O事件[调用回调函数pfnPrepare()/AddTail()将事件加入stTimeoutItemLink_t队列active],当定时器上有超时的事件就处理超时事件[将超时事件放到队列active],当active(pstActiveList)队列中有活跃事件就处理活跃事件[调用回调函数pfnProcess()重新启动协程co_resume()]

7.3、调用read(),write()等I/O操作陷入阻塞,然后再恢复执行,与第二种方法类似。(example_echocli.cpp & example_echosvr.cpp)
    SetNonBlock():将socket文件描述符设置为非阻塞,防止当前协程所在的线程被内核挂起。如果当前协程所在的线程在内核态被阻塞,其他协程也得不到执行的机会,整个程序将停止运行。
    co_enable_hook_sys():开启read(),write()等hook函数
    read():调用poll(),调用glibc中真正的read()系统调用函数

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值