<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="https://www.gentoo-zh.org/extern.php?action=feed&amp;tid=475&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / linux源码解读（二十八）：通过epoll实现协程（二）]]></title>
		<link>https://www.gentoo-zh.org/viewtopic.php?id=475</link>
		<description><![CDATA[linux源码解读（二十八）：通过epoll实现协程（二） 最近发表的帖子。]]></description>
		<lastBuildDate>Wed, 12 Oct 2022 02:33:58 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[linux源码解读（二十八）：通过epoll实现协程（二）]]></title>
			<link>https://www.gentoo-zh.org/viewtopic.php?pid=482#p482</link>
			<description><![CDATA[<p>1、协程只是一种思路，并且没有操作系统层面的参与，所以全靠3环的应用开发人员自己实现。市面上有各种协程框架，这里以微信的libco库为例，看看协程到底是怎么落地实现的！libco 是微信后台开发和使用的协程库，号称可以调度千万级协程；从使用上来说，libco 不仅提供了一套类 pthread 的协程通信机制，同时可以零改造地将三方库的阻塞 IO 调用进行协程化；正式介绍libco的源码前，先直观感受一下libco的效果，demo代码如下：</p><div class="codebox"><pre><code>void A() {
   cout &lt;&lt; 1 &lt;&lt; &quot; &quot;;
   cout &lt;&lt; 2 &lt;&lt; &quot; &quot;;
   cout &lt;&lt; 3 &lt;&lt; &quot; &quot;;
}

void B() {
   cout &lt;&lt; &quot;x&quot; &lt;&lt; &quot; &quot;;
   cout &lt;&lt; &quot;y&quot; &lt;&lt; &quot; &quot;;
   cout &lt;&lt; &quot;z&quot; &lt;&lt; &quot; &quot;;
}

int main(void) {
  A();
  B();
}</code></pre></div><p>这个代码很简单，刚开始学编程的人都能看懂，结果如下：</p><p>1 2 3 x y z</p><p>&#160; 如果用libco的协程api在A和B函数之间切换（注意这是简化后的伪代码，目的是抓住主干，避免被细枝末节的代码干扰），如下：</p><div class="codebox"><pre><code>void A() {
   cout &lt;&lt; 1 &lt;&lt; &quot; &quot;;
   cout &lt;&lt; 2 &lt;&lt; &quot; &quot;;
   co_yield_ct();  // 切出到主协程
   cout &lt;&lt; 3 &lt;&lt; &quot; &quot;;
}

void B() {
   cout &lt;&lt; &quot;x&quot; &lt;&lt; &quot; &quot;;
   co_yield_ct();  // 切出到主协程
   cout &lt;&lt; &quot;y&quot; &lt;&lt; &quot; &quot;;
   cout &lt;&lt; &quot;z&quot; &lt;&lt; &quot; &quot;;
}

int main(void) {
  ...  // 主协程
  co_resume(A);  // 启动协程 A
  co_resume(B);  // 启动协程 B
  co_resume(A);  // 从协程 A 切出处继续执行
  co_resume(B);  // 从协程 B 切出处继续执行
}</code></pre></div><p>&#160; 这时的结果就变了：</p><p>1 2 x 3 y z</p><br /><p>&#160; 可以看到代码在A和B函数之间来回切换执行，整个切换的顺序完全依靠co_yield_ct和co_resume两个函数人为控制！这样就实现了A函数阻塞时人为切换到B函数执行；B函数阻塞时再切换到A函数继续执行，不浪费一点CPU时间片！</p><p>&#160; &#160; &#160;2、之前解读linux源码时，遇到某些功能时都是先看结构体，再阅读函数功能，原因很简单：重要的字段和数据都会放在结构体中统一管理（本质是能快速寻址，利于读写），函数的所有代码都是围绕结构体中这些数据读写展开的！libco重要的结构体之一就是stCoRoutine_t（从名称包含routine就能大概才出来结构体和执行的函数相关），定义如下：</p><div class="codebox"><pre><code>/*协程跳转（也即是yield、resume）执行的函数结构体，
  包含了协程运行最重要的3要素：
  协程运行环境、执行函数的入口、函数参数
*/
struct stCoRoutine_t
{
    stCoRoutineEnv_t *env;//协程运行环境
    pfn_co_routine_t pfn;// 协程跳转执行的函数
    void *arg;//函数的参数
    coctx_t ctx;//保存协程的下文环境 

    char cStart;//协程是否开始
    char cEnd;//协程是否结束
    char cIsMain;//当前是main函数吗
    char cEnableSysHook; //是否运行系统 hook，即非侵入式逻辑
    char cIsShareStack;//多个协程之间是否共享栈
    void *pvEnv;

    //char sRunStack[ 1024 * 128 ];
    stStackMem_t* stack_mem;// 协程运行时的栈空间

    //save stack buffer while confilct on same stack_buffer;
    char* stack_sp; 
    unsigned int save_size;
    char* save_buffer;
    stCoSpec_t aSpec[1024];
};</code></pre></div><p>&#160; &#160;看吧，这个结构体几乎包含了协程子重要的几个元素：协程的调度环境、协程要运行的函数及参数，协程切换时要保存的上下文等！这些结构体之间的关系如下：<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202203/2052730-20220304173102763-688545040.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160;结构体有了，接下来就是初始化这个结构体了，再co_create_env函数中做的，核心是把传入的函数入口、参数、env等变量纳入stCoRoutine_t统一管理，同时初始化栈和其他变量！</p><div class="codebox"><pre class="vscroll"><code>/*创建协程
env：协程跳转执行函数的结构体，也可以理解为函数的环境
pfn：协程跳转执行的函数入口
初始化stCoRoutine_t *lp结构体
*/
struct stCoRoutine_t *co_create_env( stCoRoutineEnv_t * env, const stCoRoutineAttr_t* attr,
        pfn_co_routine_t pfn,void *arg )
{

    stCoRoutineAttr_t at;
    if( attr )
    {
        memcpy( &amp;at,attr,sizeof(at) );
    }
    if( at.stack_size &lt;= 0 )
    {
        at.stack_size = 128 * 1024;//协程栈128k
    }
    else if( at.stack_size &gt; 1024 * 1024 * 8 )//不能超过8M
    {
        at.stack_size = 1024 * 1024 * 8;
    }

    if( at.stack_size &amp; 0xFFF ) 
    {
        at.stack_size &amp;= ~0xFFF;//栈大小的低12bit清零，也就是页对齐
        at.stack_size += 0x1000;
    }

    stCoRoutine_t *lp = (stCoRoutine_t*)malloc( sizeof(stCoRoutine_t) );
    
    memset( lp,0,(long)(sizeof(stCoRoutine_t))); 

    /*stCoRoutine_t包含了协程运行最重要的3要素：环境、函数入口和参数*/
    lp-&gt;env = env;
    lp-&gt;pfn = pfn;
    lp-&gt;arg = arg;
    
    stStackMem_t* stack_mem = NULL;
    if( at.share_stack )//如果用共享内存
    {
        stack_mem = co_get_stackmem( at.share_stack);
        at.stack_size = at.share_stack-&gt;stack_size;
    }
    else//否则重新分配协程栈
    {
        stack_mem = co_alloc_stackmem(at.stack_size);
    }
    lp-&gt;stack_mem = stack_mem;

    lp-&gt;ctx.ss_sp = stack_mem-&gt;stack_buffer;
    lp-&gt;ctx.ss_size = at.stack_size;

    lp-&gt;cStart = 0;
    lp-&gt;cEnd = 0;
    lp-&gt;cIsMain = 0;
    lp-&gt;cEnableSysHook = 0;
    lp-&gt;cIsShareStack = at.share_stack != NULL;

    lp-&gt;save_size = 0;
    lp-&gt;save_buffer = NULL;

    return lp;
}</code></pre></div><p>&#160; 协程结构体初始化完成后就该使用了吧，还记得文章开头的那个demo案例么？main里面直接调用的co_resume函数，如下：</p><div class="codebox"><pre><code>void co_resume( stCoRoutine_t *co )
{
    stCoRoutineEnv_t *env = co-&gt;env;
    // 获取当前正在运行的协程的结构
    // 每次有新协程产生就放入数组管理
    stCoRoutine_t *lpCurrRoutine = env-&gt;pCallStack[ env-&gt;iCallStackSize - 1 ];
    if( !co-&gt;cStart )
    {
        // 为将要运行的 co 布置上下文环境：初始化协程的栈，并把函数参数、返回地址入栈
        coctx_make( &amp;co-&gt;ctx,(coctx_pfn_t)CoRoutineFunc,co,0 );
        co-&gt;cStart = 1;
    }
    //需要恢复的协程加入数组，表示正在运行
    env-&gt;pCallStack[ env-&gt;iCallStackSize++ ] = co;
    co_swap( lpCurrRoutine, co );
}</code></pre></div><p>&#160; &#160;唯一的参数就是协程结构体；前面做的都是各种准备工作，最关键的就是最后一个co_swap函数了，从名字和参数看就知道是协程结构体（本质上就是函数）互相切换！</p><div class="codebox"><pre class="vscroll"><code>void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co)
{
     stCoRoutineEnv_t* env = co_get_curr_thread_env();

    //get curr stack sp
    char c;
    /*记录curr协程栈位置，后续切换回curr时可用于恢复栈内容*/
    curr-&gt;stack_sp= &amp;c;

    if (!pending_co-&gt;cIsShareStack)
    {
        env-&gt;pending_co = NULL;
        env-&gt;occupy_co = NULL;
    }
    else //共享栈模式
    {
        env-&gt;pending_co = pending_co;
        //get last occupy co on the same stack mem
        //取出共享栈中已有的协程
        stCoRoutine_t* occupy_co = pending_co-&gt;stack_mem-&gt;occupy_co;
        //set pending co to occupy thest stack mem;
        //在共享栈中记录挂起的协程
        pending_co-&gt;stack_mem-&gt;occupy_co = pending_co;
        /*env记录pending和occupy两个协程，便于切换后仍然能找到*/
        env-&gt;occupy_co = occupy_co;
        if (occupy_co &amp;&amp; occupy_co != pending_co)
        {
            /*换个地方保存协程*/
            save_stack_buffer(occupy_co);
        }
    }

    //swap context
    /*协程切换最核心的函数：切换通用寄存器、esp+ebp、eip;*/
    coctx_swap(&amp;(curr-&gt;ctx),&amp;(pending_co-&gt;ctx) );

    //stack buffer may be overwrite, so get again;
    stCoRoutineEnv_t* curr_env = co_get_curr_thread_env();
    stCoRoutine_t* update_occupy_co =  curr_env-&gt;occupy_co;
    stCoRoutine_t* update_pending_co = curr_env-&gt;pending_co;
    
    if (update_occupy_co &amp;&amp; update_pending_co &amp;&amp; update_occupy_co != update_pending_co)
    {
        //resume stack buffer
        if (update_pending_co-&gt;save_buffer &amp;&amp; update_pending_co-&gt;save_size &gt; 0)
        {
            memcpy(update_pending_co-&gt;stack_sp, update_pending_co-&gt;save_buffer, update_pending_co-&gt;save_size);
        }
    }
}</code></pre></div><p>&#160; co_swap最核心的莫过于coctx_swap了，由于涉及到寄存器操作，C语言已经无能为力，这里直接用汇编简单粗暴来做：先把curr的上下文push到curr的栈里，然后通过movq %rsi, %rsp把rsp切换到pending的栈，最后通过一系列的pop把pending的context赋值给寄存器，最后一句ret把此时栈顶的地址赋值给eip，由此完成切换！</p><div class="codebox"><pre class="vscroll"><code>.globl coctx_swap
.type  coctx_swap, @function
coctx_swap:
    leaq 8(%rsp),%rax  # rax=(*rsp) + 8；
                       # 此时栈顶元素是当前的%rip(即当前协程挂起后被再次唤醒时，需要执行的下一条指令
                       # 的地址),后面会把栈顶的这个地址保存到curr-&gt;ctx-&gt;regs[9]中，所以保存rsp的
                       # 时候就跳过这8个字节了
    leaq 112(%rdi),%rsp#rsp=(*rdi) + (8*14);
                       # %rdi存放的是函数第一个参数的地址，即curr-&gt;ctx的地址
                       # 然后加上需要保存的14个寄存器的长度，使rsp指向curr-&gt;ctx-&gt;regs[13]
    pushq %rax         # curr-&gt;ctx-&gt;regs[13] = rax;
                       # 保存rsp，看第一行代码的注释
    pushq %rbx         # curr-&gt;ctx-&gt;regs[12] = rbx;
    pushq %rcx         # curr-&gt;ctx-&gt;regs[11] = rcx;
    pushq %rdx         # curr-&gt;ctx-&gt;regs[10] = rcx;
    pushq -8(%rax)     # curr-&gt;ctx-&gt;regs[9] = (*rax) - 8;
                       # 把协程挂起后被再次唤醒时，需要执行的下一条指令的地址保存起来
    pushq %rsi         # curr-&gt;ctx-&gt;regs[8] = rsi;
    pushq %rdi         # curr-&gt;ctx-&gt;regs[7] = rdi;
    pushq %rbp         # curr-&gt;ctx-&gt;regs[6] = rbp;
    pushq %r8          # curr-&gt;ctx-&gt;regs[5] = r8;
    pushq %r9          # curr-&gt;ctx-&gt;regs[4] = r9;
    pushq %r12         # curr-&gt;ctx-&gt;regs[3] = r12;
    pushq %r13         # curr-&gt;ctx-&gt;regs[2] = r13;
    pushq %r14         # curr-&gt;ctx-&gt;regs[1] = r14;
    pushq %r15         # curr-&gt;ctx-&gt;regs[0] = r15;

    movq %rsi, %rsp    # rsp = rsi; 
                       # rsi中存放的是函数的第二个参数的地址，即使rsp指向pending_co-&gt;ctx-&gt;regs[0]
    popq %r15          # r15 = pending_co-&gt;ctx-&gt;regs[0];
    popq %r14          # r14 = pending_co-&gt;ctx-&gt;regs[1];
    popq %r13          # r13 = pending_co-&gt;ctx-&gt;regs[2];
    popq %r12          # r12 = pending_co-&gt;ctx-&gt;regs[3];
    popq %r9           # r9 = pending_co-&gt;ctx-&gt;regs[4];
    popq %r8           # r8 = pending_co-&gt;ctx-&gt;regs[5];
    popq %rbp          # rbp = pending_co-&gt;ctx-&gt;regs[6];
    popq %rdi          # rdi = pending_co-&gt;ctx-&gt;regs[7];
    popq %rsi          # rsi = pending_co-&gt;ctx-&gt;regs[8];
    popq %rax          # rax = pending_co-&gt;ctx-&gt;regs[9];
                       # 对照前面，ctx-&gt;regs[9]中存放的是协程被唤醒后需要执行的下一条指令的地址
    popq %rdx          # rdx = pending_co-&gt;ctx-&gt;regs[10]; 
    popq %rcx          # rcx = pending_co-&gt;ctx-&gt;regs[11];
    popq %rbx          # rbx = pending_co-&gt;ctx-&gt;regs[12];
    popq %rsp          # rsp = pending_co-&gt;ctx-&gt;regs[13]; rsp += 8；
                       # 这句代码是理解整个过程的关键。和coctx_make函数中保存rsp时减8再保存相对应。
    pushq %rax         # rsp -= 8;*rsp = rax;
                       # 此时栈顶元素就是协程被唤醒后需要执行的下一条指令的地址了

    xorl %eax, %eax    # eax = 0;
                       # 使eax清零，eax中的内容作为函数的返回值 
    ret                # 相当于popq %rip 这样就可以唤醒上次挂起的协程，接着运行</code></pre></div><p>&#160; 自此，调用resume完成了函数执行的切换！resume的代码分析完了，轮到另一个yield了！其实这两个函数核心功能都是切换函数，所以底层调用的都是co_swap，如下：</p><div class="codebox"><pre><code>void co_yield_env( stCoRoutineEnv_t *env )
{
    //从数组从分别取出两个协程用于交换
    stCoRoutine_t *last = env-&gt;pCallStack[ env-&gt;iCallStackSize - 2 ];
    stCoRoutine_t *curr = env-&gt;pCallStack[ env-&gt;iCallStackSize - 1 ];

    env-&gt;iCallStackSize--;

    co_swap( curr, last);
}</code></pre></div><p>&#160; 看吧，代码是不是超级简单了？</p><p>&#160; 3、上述代码完美地在一个线程内部完成了不同代码之间的切换，不过都是人工手动掌控切换时机的，如果是网络IO了？开发人员是无法精准预测收发数据时机的，所以也没法人为精准“埋点”resume、yield做协程切换，这种业务场景该怎么处理了？之前用的是epoll来监听socket是否有事件到达，这里该怎么复用epoll了？在example_echosrv.c文件中，微信官方提供了服务端协程的用例，代码不多，但是涉及到main、readwrite、accept等多个方法之间的来回切换，并且方法内部还有for死循环，整个流程比较复杂， 我画了个简单的草图，如下：<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202203/2052730-20220306204732041-443794372.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; 可以看出：整个过程只有一个线程；里面有专门负责接受客户端连接的accept_co协程，也有监听事件的mian主协程，也有添加事件和负责读写的readwrite_so！epoll还是用于网络IO的事件监听和触发，接着就是通过yield、resume切换导到合适的协程处理这些io事件！整个过程逻辑上不算难，就是很繁杂，需要静下心来慢慢捋！为了实现整个流程，有几个关键的函数需要着重说明。</p><p>&#160; &#160; &#160; &#160;（1）libco为了对统一网络IO，条件变量需要超时管理的事件，实现了基于时间轮(timing wheel)的超时管理器； 时间轮为图中深红色的轮状数组，数组的每一个单元我们称为一个槽(slot)。单个slot里存储一定时间内注册的事件列表（图中黄色链表）。在libco中，单个slot的精度为1毫秒（刚好是jiffies），整个时间轮由60000个slot组成，对应的整个时间轮覆盖60秒的时间；<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202203/2052730-20220306212755084-2080976244.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160;为了实现时间轮，两个核心的方法如下：</p><div class="codebox"><pre class="vscroll"><code>/* 在时间轮中插入新项
 * @param 
 * apTimeout :时间轮结构
 * apItem    :新的超时项
 * allNow    :当前事件(timestamp in ms)
 * @return   :0成功, else失败行数
 */
int AddTimeout( stTimeout_t *apTimeout,stTimeoutItem_t *apItem ,unsigned long long allNow )
{
    if( apTimeout-&gt;ullStart == 0 )
    {
        apTimeout-&gt;ullStart = allNow;// 设置时间轮的最早时间是当前时间
        apTimeout-&gt;llStartIdx = 0;
    }
    /* 当前时间小于初始时间出错返回 */
    if( allNow &lt; apTimeout-&gt;ullStart )
    {
        co_log_err(&quot;CO_ERR: AddTimeout line %d allNow %llu apTimeout-&gt;ullStart %llu&quot;,
                    __LINE__,allNow,apTimeout-&gt;ullStart);

        return __LINE__;
    }
    /* 当前时间大于超时时间出错返回 */
    if( apItem-&gt;ullExpireTime &lt; allNow )
    {
        co_log_err(&quot;CO_ERR: AddTimeout line %d apItem-&gt;ullExpireTime %llu allNow %llu apTimeout-&gt;ullStart %llu&quot;,
                    __LINE__,apItem-&gt;ullExpireTime,allNow,apTimeout-&gt;ullStart);

        return __LINE__;
    }
    // 计算超时时间
    unsigned long long diff = apItem-&gt;ullExpireTime - apTimeout-&gt;ullStart;
    /* 超时时间大于时间轮的最长时间出错返回 */
    if( diff &gt;= (unsigned long long)apTimeout-&gt;iItemSize )
    {
        diff = apTimeout-&gt;iItemSize - 1;
        co_log_err(&quot;CO_ERR: AddTimeout line %d diff %d&quot;,
                    __LINE__,diff);

        //return __LINE__;
    }
    /* 将时间加入到时间轮中 */
    AddTail( apTimeout-&gt;pItems + ( apTimeout-&gt;llStartIdx + diff ) % apTimeout-&gt;iItemSize , apItem );

    return 0;
}
/* 在时间轮中取出所有超时项
 * @param 
 * apTimeout:时间轮结构
 * allNow   :当前时间(timestamp in ms)
 * apResult :超时事件结果链表
 */
inline void TakeAllTimeout( stTimeout_t *apTimeout,unsigned long long allNow,stTimeoutItemLink_t *apResult )
{
    if( apTimeout-&gt;ullStart == 0 )
    {
        apTimeout-&gt;ullStart = allNow;
        apTimeout-&gt;llStartIdx = 0;
    }

    if( allNow &lt; apTimeout-&gt;ullStart )
    {
        return ;
    }
    int cnt = allNow - apTimeout-&gt;ullStart + 1;
    if( cnt &gt; apTimeout-&gt;iItemSize )
    {
        cnt = apTimeout-&gt;iItemSize;
    }
    if( cnt &lt; 0 )
    {
        return;
    }
    for( int i = 0;i&lt;cnt;i++)
    {
        int idx = ( apTimeout-&gt;llStartIdx + i) % apTimeout-&gt;iItemSize;
        Join&lt;stTimeoutItem_t,stTimeoutItemLink_t&gt;( apResult,apTimeout-&gt;pItems + idx  );
    }
    apTimeout-&gt;ullStart = allNow;
    apTimeout-&gt;llStartIdx += cnt - 1;


}</code></pre></div><p>&#160; （2）每个main函数都需要调用的方法，用于不停的监听是否有事件发生，如下：</p><div class="codebox"><pre class="vscroll"><code>/* 事件循环：不停的监听是否有事件发生
 * @param 
 * ctx:epoll句柄
 * pfn:退出事件循环检查函数
 * arg:pfn参数
 */
void co_eventloop( stCoEpoll_t *ctx,pfn_co_eventloop_t pfn,void *arg )
{
    if( !ctx-&gt;result )
    {
        ctx-&gt;result =  co_epoll_res_alloc( stCoEpoll_t::_EPOLL_SIZE );
    }
    co_epoll_res *result = ctx-&gt;result;
    for(;;)
    {
        int ret = co_epoll_wait( ctx-&gt;iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, 1 );
        stTimeoutItemLink_t *active = (ctx-&gt;pstActiveList);
        stTimeoutItemLink_t *timeout = (ctx-&gt;pstTimeoutList);
        memset( timeout,0,sizeof(stTimeoutItemLink_t) ); //清空超时队列
        for(int i=0;i&lt;ret;i++)//遍历有事件的fd
        {
            //获取event里数据指向的stTimeoutItem_t
            stTimeoutItem_t *item = (stTimeoutItem_t*)result-&gt;events[i].data.ptr;
            if( item-&gt;pfnPrepare )//如果有预处理函数，执行，由其加入就绪列表
            {
                item-&gt;pfnPrepare( item,result-&gt;events[i],active );
            }
            else//手动加入就绪列表
            {
                AddTail( active,item );
            }
        }
        unsigned long long now = GetTickMS();
        /*时间轮中取出所有的超时项，并插入超时列表*/
        TakeAllTimeout( ctx-&gt;pTimeout,now,timeout );

        stTimeoutItem_t *lp = timeout-&gt;head;
        while( lp )
        {
            //printf(&quot;raise timeout %p\n&quot;,lp);
            lp-&gt;bTimeout = true;//设置为超时
            lp = lp-&gt;pNext;
        }
        //将超时列表合并入就绪列表
        Join&lt;stTimeoutItem_t,stTimeoutItemLink_t&gt;( active,timeout );

        lp = active-&gt;head;
        while( lp )
        {
            PopHead&lt;stTimeoutItem_t,stTimeoutItemLink_t&gt;( active );
            if (lp-&gt;bTimeout &amp;&amp; now &lt; lp-&gt;ullExpireTime) 
            {    //还未达到超时时间但已经标记为超时的，加回时间轮 
                int ret = AddTimeout(ctx-&gt;pTimeout, lp, now);
                if (!ret) 
                {
                    lp-&gt;bTimeout = false;
                    lp = active-&gt;head;
                    continue;
                }
            }
            /*调用stTimeoutItem_t项的执行函数，也就是OnPollProcessEvent
            里面有co_resume*/
            if( lp-&gt;pfnProcess )
            {
                lp-&gt;pfnProcess( lp );
            }

            lp = active-&gt;head;
        }
        if( pfn )//用于用户控制跳出事件循环
        {
            if( -1 == pfn( arg ) )
            {
                break;
            }
        }
    }
}</code></pre></div><p>&#160; （3）将 fd 交由 Epoll 管理，待 Epoll 的相应的事件触发时，再切换回来执行 read 或者 write 操作，从而实现由 Epoll 管理协程，如下：</p><div class="codebox"><pre class="vscroll"><code>/* poll内核：将 fd 交由 Epoll 管理，待 Epoll 的相应的事件触发时，
             再切换回来执行 read 或者 write 操作，
             从而实现由 Epoll 管理协程的功能
 * @param 
 * ctx:epoll句柄
 * fds:fd数组
 * nfds:fd数组长度
 * timeout:超时时间ms
 * pollfunc:默认poll 
 */
typedef int (*poll_pfn_t)(struct pollfd fds[], nfds_t nfds, int timeout);
int co_poll_inner( stCoEpoll_t *ctx,struct pollfd fds[], nfds_t nfds, int timeout, poll_pfn_t pollfunc)
{
    if (timeout == 0)
    {
        //调用系统原生poll(其实上层poll已经做过检查了，此处无需再做)
        return pollfunc(fds, nfds, timeout);
    }
    if (timeout &lt; 0)
    {
        timeout = INT_MAX;
    }
    int epfd = ctx-&gt;iEpollFd;
    stCoRoutine_t* self = co_self();

    //1.struct change
    /* 1. 初始化poll相关的数据结构 */
    stPoll_t&amp; arg = *((stPoll_t*)malloc(sizeof(stPoll_t)));
    memset( &amp;arg,0,sizeof(arg) );

    arg.iEpollFd = epfd;
    arg.fds = (pollfd*)calloc(nfds, sizeof(pollfd));//分配nfds个pollfd
    arg.nfds = nfds;

    stPollItem_t arr[2];
    //nfds少于2且未使用共享栈的情况下
    if( nfds &lt; sizeof(arr) / sizeof(arr[0]) &amp;&amp; !self-&gt;cIsShareStack)
    {    // 栈中分配
        arg.pPollItems = arr;
    }    
    else
    {
        arg.pPollItems = (stPollItem_t*)malloc( nfds * sizeof( stPollItem_t ) );
    }
    memset( arg.pPollItems,0,nfds * sizeof(stPollItem_t) );
    //调用co_resume(arg.pArg), 唤醒参数arg.pArg所指协程
    arg.pfnProcess = OnPollProcessEvent; 
    arg.pArg = GetCurrCo( co_get_curr_thread_env() );//得到当前运行的协程
    
    
    //2. add epoll把事件加入到epoll中监控
    for(nfds_t i=0;i&lt;nfds;i++)
    {
        arg.pPollItems[i].pSelf = arg.fds + i;
        arg.pPollItems[i].pPoll = &amp;arg;

        arg.pPollItems[i].pfnPrepare = OnPollPreparePfn;// 预处理回调函数
        struct epoll_event &amp;ev = arg.pPollItems[i].stEvent;

        if( fds[i].fd &gt; -1 )
        {
            ev.data.ptr = arg.pPollItems + i;
            ev.events = PollEvent2Epoll( fds[i].events );

            int ret = co_epoll_ctl( epfd,EPOLL_CTL_ADD, fds[i].fd, &amp;ev ); //事件加入epoll的监听
            if (ret &lt; 0 &amp;&amp; errno == EPERM &amp;&amp; nfds == 1 &amp;&amp; pollfunc != NULL)
            {
                if( arg.pPollItems != arr )
                {
                    free( arg.pPollItems );
                    arg.pPollItems = NULL;
                }
                free(arg.fds);
                free(&amp;arg);
                return pollfunc(fds, nfds, timeout);
            }
        }
        //if fail,the timeout would work
    }

    //3.add timeout 给时间轮添加超时时间
    unsigned long long now = GetTickMS();
    arg.ullExpireTime = now + timeout;
    int ret = AddTimeout( ctx-&gt;pTimeout,&amp;arg,now );
    int iRaiseCnt = 0;
    if( ret != 0 )
    {
        co_log_err(&quot;CO_ERR: AddTimeout ret %d now %lld timeout %d arg.ullExpireTime %lld&quot;,
                ret,now,timeout,arg.ullExpireTime);
        errno = EINVAL;
        iRaiseCnt = -1;

    }
    else
    {    // 把执行权交给调用此协程的协程，也就是main线程
        co_yield_env( co_get_curr_thread_env() );
        iRaiseCnt = arg.iRaiseCnt;
    }
    /*当 main 协程的事件循环 co_eventloop 中触发了对应的监听事件时，会恢复执行
    此时，将开始执行下半段，即将上半段添加的句柄 fds 从 epoll 中移除，
    清理残留的数据结构*/
    {
        //clear epoll status and memory 
        // 将该项从时间轮中删除
        RemoveFromLink&lt;stTimeoutItem_t,stTimeoutItemLink_t&gt;( &amp;arg );
        for(nfds_t i = 0;i &lt; nfds;i++)
        {
            int fd = fds[i].fd;
            if( fd &gt; -1 )
            {
                co_epoll_ctl( epfd,EPOLL_CTL_DEL,fd,&amp;arg.pPollItems[i].stEvent );
            }
            fds[i].revents = arg.fds[i].revents;
        }


        if( arg.pPollItems != arr )
        {
            free( arg.pPollItems );
            arg.pPollItems = NULL;
        }

        free(arg.fds);
        free(&amp;arg);
    }

    return iRaiseCnt;
}</code></pre></div><p>总结：</p><p>1、协程：在各个不同的方法之间切换，从汇编层面看，就是jmp到不同的代码执行</p><p> </p><p>参考：</p><p>1、https://www.cyhone.com/articles/analysis-of-libco/&#160; 微信 libco 协程库源码分析</p><p>2、https://github.com/tencent/libco&#160; libco源码</p><p>3、https://www.infoq.cn/article/CplusStyleCorourtine-At-Wechat&#160; &#160; C/C++ 协程库 libco：微信怎样漂亮地完成异步化改造</p><p>4、https://zhuanlan.zhihu.com/p/27409164&#160; libco协程上下文切换原理</p><p>5、https://cloud.tencent.com/developer/article/1459729&#160; libco的设计与实现</p><p>6、https://nifengz.com/libco_context_swap/ </p><p>7、http://kaiyuan.me/2017/07/10/libco/&#160; libco分析</p><p>8、https://www.changliu.me/post/libco-auto/ 自动切换</p><p>9、https://blog.csdn.net/MOU_IT/article/details/115033799 事件注册poll</p><p>10、http://kaiyuan.me/2017/10/20/libco2/&#160; 协程的管理</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Wed, 12 Oct 2022 02:33:58 +0000</pubDate>
			<guid>https://www.gentoo-zh.org/viewtopic.php?pid=482#p482</guid>
		</item>
	</channel>
</rss>
