协程实现原理——以libco为例

目录

☀️0.前言

🌤️1.协程概述

🌤️2.协程设计要点

⛅2.1 协程上下文切换

⛅2.2 协程栈

⛅2.3 协程调度

🌤️3. 解析libco

⛅3.1 libco概述

⛅3.2 libco核心结构体剖析

🌥️3.2.1 libco核心结构体关系示意图

🌥️3.2.2 libco核心结构体及其参数详解

⛅3.3 libco核心函数解析

🌥️3.3.1 初始化函数

🌦️3.3.1.1 co_create()

🌦️3.3.1.2 co_create_env()

🌥️3.3.2 恢复(resume)函数

🌦️3.3.2.1 co_resume()

🌦️3.3.2.2 co_swap()

🌦️3.3.2.3 coctx_swap()

🌥️3.3.3 挂起(yield)函数

🌦️3.3.3.1 co_yield_ct() && co_yield()

🌦️3.3.3.2 co_yield_env()

🌤️4. libco使用实例——基于epoll实现echo

⛅4.1 main()函数

⛅4.2 accept_routine()

⛅4.3 readwrite_routine()

🪐5. 结束语


☀️0.前言

协程是个很重要的概念,对于向我一样的在校生而言,或者对进程和线程比较熟悉,对协程一般不太理解,协程也确实不好理解,本篇文章希望以通俗易懂的方式讲述协程,并且以著名的libco为例,看看核心的协程实现代码。

🌤️1.协程概述

协程的概念非常不好理解,虽然网上文章众多,但大多数都给人一种云里雾里的感觉,对协程的理解也有一些不同的观点,我个人认为协程就是一种可以“暂停和恢复执行”的特殊函数,我们知道一般的函数执行类似于原子操作,一旦调用之后就一直执行直到 return,而协程则允许函数执行到一半停下来,过一段时间再接着执行,乍一看似乎没有什么意义。

大家可以想象这样一种场景,大家在做暑假家庭作业的时候,美术作业需要用到画笔,而你目前家里还没有画笔,你需要在淘宝上购买并等待两天才能到,这个时候你有两种选择,一种是在这等待期间啥也不干,另一种是在等待期间做其他作业,很明显第二种效率更高,可以让你更快的完成作业。

同样的道理,假设我们在编写程序时,某个协程需要等待外部资源(例如网络请求的响应、磁盘 I/O 等),而这些操作可能需要很长时间。普通函数或线程在这种情况下会阻塞,无法继续执行其他任务,导致资源浪费。而协程则允许程序在等待期间做其他事情,等到资源准备好后再继续恢复执行,从而大大提高了程序的并发性和效率。

举个例子:假设我们编写一个程序去下载多个文件。如果使用普通函数或线程,程序会一次下载一个文件,等到这个文件下载完成后,才会开始下载下一个文件。这意味着我们必须等待网络 I/O 结束才能继续。而协程允许我们在等待某个文件下载完成的同时,可以去启动其他文件的下载任务。当第一个文件下载完成时,我们再回来处理它,从而实现了高效的并发执行。

因此,协程的意义在于,它使得程序能够处理耗时操作(如 I/O 或网络请求)时不被阻塞,从而让程序能够利用等待的时间去做其他有用的事情。这种模型在需要处理大量 I/O 密集型任务时(如网络爬虫、并发服务器等)非常有效,因为它最大化了资源的利用率。

在linux下,进程和线程可以认为是一样的,可以不做严格的区分,所以在有了线程之后为什么还需要协程呢?难道线程完成不了协程的事情吗?这个还真完成不了,核心在于线程是由操作系统调度的,用户无法控制线程的具体执行顺序或时间而协程的调度则由程序员自己控制,可以根据任务的重要性或优先级自由切换。另一方面,如果用线程来完成协程的事情,未免太“小题大做”了,线程创建消耗资源大,在linux下,默认占用8MB,而如果只是实现函数切换,根本不需要这么大的资源消耗,所以选择了更为轻量的协程,一般只占用几KB,用这几KB就可以完成任务,所以资源利用率高,可以实现更高效的上下文切换,也减少了内核态与用户态切换的开销(线程调度),所以也可以认为协程是“用户态的可控的轻量级线程”。

🌤️2.协程设计要点

下面来探讨一下协程设计的要点,不管是哪个协程框架,都可以从下面这些角度来剖析。

⛅2.1 协程上下文切换

协程上下文切换的意思是保存当前协程的上下文,切换到待运行协程的上下文,具体来说,栈保存协程执行过程中产生的局部变量,函数调用信息以及返回地址等,寄存器保存协程当前的执行状态,如程序计数器(PC)、栈指针(SP)以及其他一些处理器寄存器的状态。

上下文切换有多种形式,如使用操作系统提供的API:ucontent;使用setjump/longjump;用汇编语言实现(libco采用,性能较好但兼容性差);采用boost库的boost.coroutine或者boost.context

⛅2.2 协程栈

通常会创建数量庞大的协程来支持高并发,协程栈内存占用就需要考虑;栈内存不可太大(浪费空间),也不可太小(容易栈溢出),这其实是很难权衡的,一旦定死,就只能针对特定的场景,无法做到通用。

libco采用的是固定栈 + 共享栈的方式,固定栈的大小默认为128KB,共享栈的大小最大为8MB;除此之外,还可以采取分段栈、拷贝栈、虚拟内存栈等。

⛅2.3 协程调度

libco采取的调度策略是栈式调度,协程队列是一个栈式的结构,每次创建的协程都置于栈顶,会立即暂停当前协程并切换至子协程中运行,子协程运行结束(或其他原因导致切换出来)后,继续切换回来执行父协程。

🌤️3. 解析libco

⛅3.1 libco概述

libco是微信后台大规模使用的c/c++协程库,2013年至今稳定运行在微信后台的数万台机器上。libco通过仅有的几个函数接口 co_create/co_resume/co_yield 再配合 co_poll,可以支持同步或者异步的写法,如线程库一样轻松。同时库里面提供了socket族函数的hook,使得后台逻辑服务几乎不用修改逻辑代码就可以完成异步化改造。

⛅3.2 libco核心结构体剖析

要剖析libco,首先从核心结构体看起,先了解这些结构体的关系,再看函数,函数其实就是对这些结构体数据进行读写。

🌥️3.2.1 libco核心结构体关系示意图

🌥️3.2.2 libco核心结构体及其参数详解

1、stCoRoutine_t:这是Libco协程的核心结构体,表示一个协程。该结构体包含协程的执行环境、运行状态、上下文、栈等信息。

env: 协程运行环境,指向stCoRoutineEnv_t

pfn: 协程执行函数;

arg: 函数参数;

ctx: 协程的上下文信息,指向coctx_t

cStart:协程是否开始;

cEnd:协程是否结束;

cIsMain:当前是否为main函数;

cEnableSysHook:是否运行系统hook;

cIsShareStack:是否使用共享栈;

stack_mem: 协程运行时的栈空间,指向stStackMem_t

stack_spsave_sizesave_buffer:保存协程栈指针和大小,便于恢复;

aSpec: 协程的本地存储,类似于线程的thread-local存储。

2、stCoRoutineEnv_t:这是协程的执行环境,类似于线程的调度器。它管理所有协程的调用栈,并维护协程之间的切换关系。

pCallStack[128]: 协程调用栈,最后一位是当前运行的协程,前一位是当前协程的父协程(即,resume该协程的协程),只支持不超过128个协程嵌套调用;

iCallStackSize: 当前调用栈长度;

pEpoll: 协程调度器,主要是epoll,指向stCoEpoll_t

pending_co: 共享栈模式下,表示被挂起(待运行)的协程;

occupy_co:共享栈模式下,表示正在执行的协程。

stCoRoutineEnv_t维护了当前线程中所有协程的执行状态和切换信息,每个线程都有一个协程环境。stCoRoutine_t协程会通过这个结构体进行切换。

3、stCoEpoll_t:这是协程的事件管理器,负责协程的IO事件调度。它基于epoll实现,是协程的底层调度机制。

iEpollFd: epoll的fd;

pTimeout:超时管理器;

pstTimeoutList:目前已超时的事件,作为中转使用,最后会合并到active;

pstActiveList:正在处理的活跃事件;

result: epoll_wait()返回的事件。

stCoEpoll_t用于管理协程的IO事件。当协程进行IO操作(如网络请求)时,stCoEpoll_t会将协程挂起,等待事件完成后再恢复该协程。

4、coctx_t:这是用于保存协程上下文的结构体,类似于线程的上下文。它保存协程的寄存器状态以及栈的信息。

regs: 保存寄存器状态,386架构是8个,其余架构的为14个;

ss_size:协程栈大小;

ss_sp: 协程栈指针。

每个stCoRoutine_t都有一个coctx_t结构体,用于保存当前协程的上下文。当协程被挂起或恢复时,这个上下文会被切换。

5、stStackMem_t:表示协程的栈信息,每个协程在运行时需要一块栈内存。

occupy_co: 使用该栈的协程;

stack_size: 栈大小;

stack_bp:stack_buffer + stack_size;

stack_buffer:栈底。

一个stCoRoutine_t结构体中的stack_mem指向一个stStackMem_t,表示该协程使用的栈。每个协程的执行依赖于栈内存,用于存储局部变量、函数调用信息等。

6、stShareStack_t:是共享栈模式的支持结构体。当协程切换时,不是每个协程都有独立的栈,而是多个协程可以共享一块大的栈空间。这在需要大量协程时可以节省内存。

alloc_idx: 当前分配的栈的索引;

stack_size: 每块栈的大小;

count: 栈的数量;

stack_array: 指向stStackMem_t数组,表示多块共享的栈。

当使用共享栈模式时,多个stCoRoutine_t可以共享一个stShareStack_t结构体,而不是为每个协程都分配独立的stStackMem_t。stShareStack_t负责管理这些共享栈的分配和使用。

7、stCoRoutineAttr_t:是创建协程时的属性结构体,用于配置协程的栈大小以及是否使用共享栈。

stack_size: 协程的栈大小(非共享栈模式下需要指定);

share_stack: 是否使用共享栈。

在创建协程时,可以通过stCoRoutineAttr_t来指定协程的栈大小或选择共享栈模式。

⛅3.3 libco核心函数解析

🌥️3.3.1 初始化函数

有了这些关键结构体之后,首先要对其进行初始化,来看看创建用的初始化函数是如何操作的

🌦️3.3.1.1 co_create()
int co_create(stCoRoutine_t **ppco, const stCoRoutineAttr_t *attr, pfn_co_routine_t pfn, void *arg)
{
	if (!co_get_curr_thread_env()) // 检查协程环境是否已初始化
	{
		co_init_curr_thread_env();
	}

	stCoRoutine_t *co = co_create_env(co_get_curr_thread_env(), attr, pfn, arg);

	*ppco = co;
	return 0;
}

可以看到co_create函数主要是调用co_create_env()来进行初始化

🌦️3.3.1.2 co_create_env()
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(&at, attr, sizeof(at));
    }

    if (at.stack_size <= 0)
    {
        at.stack_size = 128 * 1024; // 默认128KB
    }
    else if (at.stack_size > 1024 * 1024 * 8)
    {
        at.stack_size = 1024 * 1024 * 8; // 最大8MB
    }

    if (at.stack_size & 0xFFF) // 对齐处理,确保大小为4KB的倍数
    {
        at.stack_size &= ~0xFFF;
        at.stack_size += 0x1000;
    }

    stCoRoutine_t *lp = (stCoRoutine_t *)malloc(sizeof(stCoRoutine_t));

    memset(lp, 0, (long)(sizeof(stCoRoutine_t)));

    lp->env = env;
    lp->pfn = pfn;
    lp->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->stack_size;
    }
    else
    {
        // 如果没有采用共享栈,则分配内存
        stack_mem = co_alloc_stackmem(at.stack_size);
    }

    lp->stack_mem = stack_mem;

    // 设置协程上下文
    lp->ctx.ss_sp = stack_mem->stack_buffer; // 栈地址
    lp->ctx.ss_size = at.stack_size;		 // 栈大小

    // 初始化协程状态
    lp->cStart = 0; // 默认未启动
    lp->cEnd = 0; // 默认未结束
    lp->cIsMain = 0; // 默认非主协程
    lp->cEnableSysHook = 0; // 默认不开启hook
    lp->cIsShareStack = at.share_stack != NULL;

    // 如果使用共享栈,则初始化save_size和save_buffer
    lp->save_size = 0;
    lp->save_buffer = NULL;

    return lp;
}

创建函数主要是对协程结构体参数进行赋初值,默认是不开启hook的,默认栈大小为128KB,最大栈大小为8MB。总体函数是比较简单的,对照着注释可以很容易看懂

🌥️3.3.2 恢复(resume)函数

初始化结束后我们来看看如何启用某个协程

🌦️3.3.2.1 co_resume()
void co_resume(stCoRoutine_t *co)
{
	stCoRoutineEnv_t *env = co->env; // 获取当前协程的运行环境
	stCoRoutine_t *lpCurrRoutine = env->pCallStack[env->iCallStackSize - 1]; // 获取当前正在执行的协程
	if (!co->cStart) // 如果目标协程还没有启动
	{
		coctx_make(&co->ctx, (coctx_pfn_t)CoRoutineFunc, co, 0); // 为目标协程设置上下文
		co->cStart = 1; // 标记协程已启动
	}
	env->pCallStack[env->iCallStackSize++] = co; // 将目标协程推入调用栈,准备执行
	co_swap(lpCurrRoutine, co); // 切换到目标协程的上下文
}

co_resume函数内部调用co_swap()来完成上下文切换

🌦️3.3.2.2 co_swap()
/*
* 1. 将当前的运行上下文保存到curr中
* 2. 将当前的运行上下文替换为pending_co中的上下文
*/
void co_swap(stCoRoutine_t *curr, stCoRoutine_t *pending_co)
{
    stCoRoutineEnv_t *env = co_get_curr_thread_env(); // 获取当前线程的协程环境

    char c;
    curr->stack_sp = &c; // 保存当前协程的栈顶位置

    if (!pending_co->cIsShareStack) // 如果目标协程没有使用共享栈
    {
        env->pending_co = NULL;
        env->occupy_co = NULL;
    }
    else // 如果目标协程使用了共享栈
    {
        env->pending_co = pending_co; // 将目标协程标记为待执行协程
        stCoRoutine_t *occupy_co = pending_co->stack_mem->occupy_co; // 获取当前占用共享栈的协程
        pending_co->stack_mem->occupy_co = pending_co; // 将目标协程设为占用共享栈的协程
        env->occupy_co = occupy_co;

        if (occupy_co && occupy_co != pending_co) // 如果共享栈已经被别的协程占用
        {
            save_stack_buffer(occupy_co); // 将占用共享栈的协程的栈内容保存到其他位置
        }
    }

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

    // 恢复共享栈的内容
    stCoRoutineEnv_t *curr_env = co_get_curr_thread_env();
    stCoRoutine_t *update_occupy_co = curr_env->occupy_co;
    stCoRoutine_t *update_pending_co = curr_env->pending_co;

    if (update_occupy_co && update_pending_co 
        && update_occupy_co != update_pending_co) // 共享栈模式下恢复栈数据
    {
        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); // 将保存的栈数据恢复
        }
    }
}

co_swap()还是比较复杂的,需要好好理解,其实保存当前协程的栈顶位置需要解释一下:

char c;
curr->stack_sp = &c; // 保存当前协程的栈顶位置

当你调用一个函数时,栈顶会随着函数的局部变量和函数调用信息的增加而移动。局部变量 c 是在 co_swap 函数中定义的,它会被分配在当前栈帧的栈顶位置。通过取 c 的地址 &c,可以得到当前栈顶的位置,因为此时 c 是栈上最靠近栈顶的变量。curr->stack_sp = &c 这行代码的作用是将当前协程的栈指针保存起来,这样当协程被挂起时(即上下文切换后),可以记录它在栈上的位置,以便后续恢复。

co_swap()调用coctx_swap()来完成真正的上下文切换

🌦️3.3.2.3 coctx_swap()

用汇编语言编写的协程上下文切换函数,负责保存当前协程的 CPU 寄存器状态,并加载目标协程的寄存器状态,从而实现协程的切换,分32位系统和64位系统。

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

#if defined(__i386__)
movl 4(%esp), %eax      # 从栈中取出第一个参数,存储在 eax 中
movl %esp,  28(%eax)    # 将当前的栈指针 (%esp) 保存到当前协程上下文中的 esp
movl %ebp, 24(%eax)     # 将当前基指针 (%ebp) 保存到当前协程上下文中的 ebp
movl %esi, 20(%eax)     # 保存其他寄存器 esi
movl %edi, 16(%eax)     # 保存其他寄存器 edi
movl %edx, 12(%eax)     # 保存其他寄存器 edx
movl %ecx, 8(%eax)      # 保存其他寄存器 ecx
movl %ebx, 4(%eax)      # 保存其他寄存器 ebx

movl 8(%esp), %eax      # 从栈中取出第二个参数,存储在 eax 中
movl 4(%eax), %ebx      # 恢复目标协程上下文的 ebx
movl 8(%eax), %ecx      # 恢复目标协程上下文的 ecx
movl 12(%eax), %edx     # 恢复目标协程上下文的 edx
movl 16(%eax), %edi     # 恢复目标协程上下文的 edi
movl 20(%eax), %esi     # 恢复目标协程上下文的 esi
movl 24(%eax), %ebp     # 恢复目标协程上下文的 ebp
movl 28(%eax), %esp     # 恢复目标协程上下文的 esp

ret                     # 返回到目标协程执行

#elif defined(__x86_64__)
leaq (%rsp), %rax        # 将当前的栈指针地址存储到 rax 中
movq %rax, 104(%rdi)     # 保存当前的栈指针 (%rsp) 到当前协程上下文中的 rsp
movq %rbx, 96(%rdi)      # 保存当前协程的 rbx 寄存器值
movq %rcx, 88(%rdi)      # 保存 rcx 寄存器
movq %rdx, 80(%rdi)      # 保存 rdx 寄存器
movq %rsi, 64(%rdi)      # 保存 rsi 寄存器
movq %rdi, 56(%rdi)      # 保存 rdi 寄存器
movq %rbp, 48(%rdi)      # 保存基指针寄存器 rbp
movq %r8, 40(%rdi)       # 保存 r8 寄存器
movq %r9, 32(%rdi)       # 保存 r9 寄存器
movq %r12, 24(%rdi)      # 保存 r12 寄存器
movq %r13, 16(%rdi)      # 保存 r13 寄存器
movq %r14, 8(%rdi)       # 保存 r14 寄存器
movq %r15, (%rdi)        # 保存 r15 寄存器

xorq %rax, %rax          # 将 rax 清零

movq 48(%rsi), %rbp      # 恢复目标协程的 rbp
movq 104(%rsi), %rsp     # 恢复目标协程的 rsp
movq (%rsi), %r15        # 恢复目标协程的 r15
movq 8(%rsi), %r14       # 恢复 r14
movq 16(%rsi), %r13      # 恢复 r13
movq 24(%rsi), %r12      # 恢复 r12
movq 32(%rsi), %r9       # 恢复 r9
movq 40(%rsi), %r8       # 恢复 r8
movq 56(%rsi), %rdi      # 恢复 rdi
movq 80(%rsi), %rdx      # 恢复 rdx
movq 88(%rsi), %rcx      # 恢复 rcx
movq 96(%rsi), %rbx      # 恢复 rbx
leaq 8(%rsp), %rsp       # 调整栈指针
pushq 72(%rsi)           # 将目标协程的返回地址压入栈
movq 64(%rsi), %rsi      # 恢复 rsi

ret                      # 返回到目标协程
#endif
🌥️3.3.3 挂起(yield)函数

看完恢复函数,我们再来看看挂起函数,其实主函数还是co_swap()

🌦️3.3.3.1 co_yield_ct() && co_yield()
void co_yield_ct()
{
	co_yield_env(co_get_curr_thread_env());
}

void co_yield (stCoRoutine_t *co)
{
	co_yield_env(co->env);
}
🌦️3.3.3.2 co_yield_env()
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); // 在当前协程和父协程之间进行上下文切换
}

至此,libco的核心函数我们都看完了,整个协程实现的核心就在这里,其实代码并不长。上述代码是在一个线程内部完成不同代码之间的切换,都是用户手动掌控切换时机的,如果是无法精准预测收发数据时机的网络IO呢?就需要复用epoll了。

或许有同学有个疑问是协程可以用在哪里,一般来说可以和epoll结合来实现TcpServer,我们下面就来看一下这个libco的使用示例。

🌤️4. libco使用实例——基于epoll实现echo

这段代码在example_echosrv.c文件。

⛅4.1 main()函数

我们从main函数看起,看看协程到底是如何运用在TcpServer上的。

int main(int argc, char *argv[])
{
    if (argc < 5)
    {
        printf("Usage:\n"
               "example_echosvr [IP] [PORT] [TASK_COUNT] [PROCESS_COUNT]\n"
               "example_echosvr [IP] [PORT] [TASK_COUNT] [PROCESS_COUNT] -d   # daemonize mode\n");
        return -1;
    }
    const char *ip = argv[1]; // IP地址
    int port = atoi(argv[2]); // 端口号
    int cnt = atoi(argv[3]);     // 协程数量
    int proccnt = atoi(argv[4]); // 进程数量
    bool deamonize = argc >= 6 && strcmp(argv[5], "-d") == 0;

    g_listen_fd = CreateTcpSocket(port, ip, true); // 创建监听fd并绑定
    listen(g_listen_fd, 1024); // 开始监听
    if (g_listen_fd == -1)
    {
        printf("Port %d is in use\n", port);
        return -1;
    }
    printf("listen %d %s:%d\n", g_listen_fd, ip, port);

    SetNonBlock(g_listen_fd); // 设置非阻塞模式

    for (int k = 0; k < proccnt; k++) // 多进程处理
    {
        pid_t pid = fork();
        if (pid > 0) continue;
        else if (pid < 0) break;
        for (int i = 0; i < cnt; i++) // 在每个子进程中,创建并启动多个协程,每个协程处理一个任务
        {
            task_t *task = (task_t *)calloc(1, sizeof(task_t));
            task->fd = -1;
            co_create(&(task->co), NULL, readwrite_routine, task); // 创建一个协程
            co_resume(task->co); // 启动协程
        }

        // 启动accept协程
        stCoRoutine_t *accept_co = NULL;
        co_create(&accept_co, NULL, accept_routine, 0);
        co_resume(accept_co);

        co_eventloop(co_get_epoll_ct(), 0, 0); // 启动事件循环
        exit(0);
    }
    if (!deamonize) wait(NULL);
    return 0;
}

相信配合着注释很好理解,在该函数中,两个核心的函数在于处理客户端连接的协程和数据读写协程,我们来分别看一下。

⛅4.2 accept_routine()

static void *accept_routine(void *) // 处理客户端连接的协程
{
    co_enable_hook_sys();
    printf("accept_routine\n");
    fflush(stdout);
    for (;;)
    {
        if (g_readwrite.empty()) // 如果没有空闲的协程可以处理连接请求
        {
            printf("empty\n"); 
            struct pollfd pf = {0};
            pf.fd = -1;
            poll(&pf, 1, 1000); // 睡眠一秒等待有空闲的协程
            continue;
        }

        struct sockaddr_in addr; 
        memset(&addr, 0, sizeof(addr));
        socklen_t len = sizeof(addr);
        int fd = co_accept(g_listen_fd, (struct sockaddr *)&addr, &len);

        if (fd < 0) // 如果accept失败,调用co_poll()暂时让出执行权
        {
            struct pollfd pf = {0};
            pf.fd = g_listen_fd;
            pf.events = (POLLIN | POLLERR | POLLHUP);
            co_poll(co_get_epoll_ct(), &pf, 1, 1000);

            continue;
        }

        if (g_readwrite.empty())
        {
            close(fd);
            continue;
        }

        SetNonBlock(fd); // 设置非阻塞

        // 处理连接
        task_t *co = g_readwrite.top();
        co->fd = fd;
        g_readwrite.pop();
        co_resume(co->co);
    }
    return 0;
}

⛅4.3 readwrite_routine()

static void *readwrite_routine(void *arg) // 处理客户端读写操作的协程
{
    co_enable_hook_sys();

    task_t *co = (task_t *)arg;
    char buf[1024 * 16];
    for (;;)
    {
        if (-1 == co->fd)
        {
            g_readwrite.push(co); // 当前协程没有任务,将其放入空闲队列
            co_yield_ct(); // 暂停当前协程,等待被唤醒
            continue;
        }

        int fd = co->fd;
        co->fd = -1;

        for (;;)
        {
            struct pollfd pf = {0};
            pf.fd = fd;
            pf.events = (POLLIN | POLLERR | POLLHUP);

            co_poll(co_get_epoll_ct(), &pf, 1, 1000); // 等待可读事件
            int ret = read(fd, buf, sizeof(buf)); // 当超时或者可读事件到达时,进行read

            if (ret > 0) ret = write(fd, buf, ret); // 将读取的数据写回客户端

            if (ret <= 0)
            {
                if (errno == EAGAIN) continue;
                close(fd);
                break;
            }
        }
    }
    return 0;
}

相信只要是熟悉常规的TcpServer的同学都可以很容易看懂上述代码。

🪐5. 结束语

这次暂时就分享到这里,只是介绍了libco中核心的部分,理解了这部分也就理解了协程到底是如何实现的,libco还有一些比较重要的东西,如时间轮等,以后有时间了再把这部分内容补上。本人目前还是个在校生,还比较小白,也刚刚开始写 CSDN 博客不久,可能写的也不是很好,如果有任何疑问或者发现我有哪里写的不对的地方,欢迎大家留言告诉我!我都会一一改正的。

如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持小占!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值