# Mini_Coroutine_library **Repository Path**: mnlife/Mini_Coroutine_library ## Basic Information - **Project Name**: Mini_Coroutine_library - **Description**: No description available - **Primary Language**: C - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-10-26 - **Last Updated**: 2023-10-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Mini_Coroutine_library **基于 Linux ucontext 函数族实现的 简易的,非对称的 协程库** --------------------------------------------------------------------------------------------------------------------------------------------------------------------- ## 前情说明 2023/5/2 1:42 终于完工了呜呜呜 ... 写个 README 然后休息了 ### 为什么要搞这个协程库呢 ...可能基于兴趣吧,搞点小玩具hhh 学协程的时候,觉得挺有意思的,研究了一下腾讯微信后台的libco,感觉很有东西,然后就开始写了hhh; --------------------------------------------------------------------------------------------------------------------------------------------------------------------- ## 项目介绍 balabala...还没想好... ### 说明一下,这个协程库有三个版本: **1.最终版(main分支)**     这个版本在中间版本上,我自己实现了一个事件驱动,这样就不用libevent库了; **2.中间版(v2分支)**     这个版本hook了系统调用,封装了异步;     但是设计到事件驱动,这里我用的是libevent事件通知库;     也就是说,要用这个版本还得装个libevent... **3.最早版(v3分支)**     这个版本只有协程体的切换,并没有封装异步,无法体现出协程的真正作用。 --------------------------------------------------------------------------------------------------------------------------------------------------------------------- ## 使用说明 在最终版中,我们扒下来之后,需要包含 "coroutine.h" 和 "co_hook.cpp" 文件;然后就能使用协程库了; 在文件中我写了一个测试 sleep 的 "test_sleep_hook.cpp" 文件 以及 一个回射服务器 "client.cpp" "server.cpp" ; 具体可以看看里面的代码。 --------------------------------------------------------------------------------------------------------------------------------------------------------------------- ## 整体架构 一个进程 <=> 多个线程     一个线程 <=> 多个协程 如何一个线程中用一个调度器来控制所有协程,所以这里需要有一个 (线程号 <=> 调度器) 的映射关系:这里我用的是一个全局的 map (ps:当然这里就设计到线程安全的问题...我没搞hhh,就 一个进程 <=> 多个协程 吧hhh,要搞的话可能得封装线程...) 用户只需要关系 协程体 和 调度器;一个线程和一个调度器绑定,所有关于协程的操作都需要通过这个调度器来控制; (ps:其实这个调度器是可以不用的,但是之前设计了...所以就直接沿用了...) 项目架构方面,可以分为三个部分: 1. 协程体、调度器、协程切换 方面 2. hook 系统调用,封装异步 方面 3. 事件驱动 方面 这三点在下面的 具体设计 会详细介绍 --------------------------------------------------------------------------------------------------------------------------------------------------------------------- ## 底层逻辑 底层逻辑其实非常简单: 就是首先我们在底层有一个事件监听器(封装在调度器中),底层一直在事件循环检测; 接着我们创建协程去完成不同的功能,当我们同步调用系统调用的时候,那么会走我们 hook 后的系统调用; 在这其中,我们会先创建一个事件,并将事件加入当事件源(事件监听器)中,然后切出当前协程; 然后当事件循环检测到有事件发生的时候,那么就会切回这个事件对应的协程(唤醒协程),然后协程就能继续执行了! --------------------------------------------------------------------------------------------------------------------------------------------------------------------- ## 具体设计 ### 协程体、调度器、协程切换 方面 对应 "co_event.h" "co_event.cpp" #### 概述 其实在这三个版本之前还有一个版本,但是那个版本及其简陋,有很大的问题... 就是最早的协程库切换时是通过协程的上下文和调度器中的那个上下文进行交换; 但是这样有很大的问题,这样也就是说我们只能在主线程中去唤醒协程,不能在协程中去唤醒其他的协程(这样的话会把调度器中的上下文给覆盖掉,所以会有问题); **所以我改进了一下:** 这个调度关系我们直接在协程体中维护,也就是说,在协程体结构体中还需要保存调取这个协程(父协程)的协程体指针; 这样子的话,在切换的时候就能够做到真正的非对称调度,而且更加灵活! **这里具体是基于 Linux ucontext 函数族实现的** ucontext 函数族主要有 ucontext_t 结构体 以及 几个函数; ucontext_t 结构体是用来存放当前上下文环境的(后继上下文,栈的信息)。 由于协程的切换需要保存上下文信息嘛,其实不只是协程,线程,进程的切换也需要保存上下文信息; 只不过是操作系统帮我们解决了,而协程是用户及线程,需要我们手动的进行上下文信息的设置。 协程的上下文切换需要借助下面几个函数,ucontext函数族,**下面是几个函数**: ```C++ int getcontext(ucontext_t* ucp); 将当前程序的上下文信息保存到 ucp 中; ``` ```C++ int setcontext(const ucontext_t *ucp); 将ucontext_t结构体变量ucp中的上下文信息重新恢复到cpu中并执行; ``` ```C++ void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...) 修改上下文信息,参数ucp就是我们要修改的上下文信息结构体;func是上下文的入口函数; argc是入口函数的参数个数,后面的…是具体的入口函数参数,该参数必须为整形值。 ``` ```C++ int swapcontext(ucontext_t *oucp, ucontext_t *ucp) 功能:将当前cpu中的上下文信息保存带oucp结构体变量中,然后将ucp中的结构体的上下文信息恢复到cpu中。 这里可以理解为调用了两个函数,第一次是调用了getcontext(oucp)然后再调用setcontext(ucp)。 ``` #### 具体设计 在具体项目的设计方面,所以我设计了两个类,一个是协程体类,一个是调度器类: 协程类 里面: ```C++ ucontext_t ctx; //当前协程的上下文环境 Fun func; //当前协程要执行的函数 void *arg; //func的参数 enum CoroutineState state; //表示当前协程的状态 char stack[DEFAULT_STACK_SZIE]; //每个协程的独立的栈 int id; //当前协程的编号 coroutine *fa_co; //调用当前协程的父协程体 ``` 调度器类 里面: ```C++ int running_thread; //当前正在运行的协程编号 coroutine *coroutine_pool; //协程队列(协程池) int max_index; // 曾经使用到的最大的index + 1 //封装一个事件源(事件监听器) struct co_event_base *base; ``` 然后整体的架构是一个线程创建一个调度器,然后所有的有关协程的控制(创建,挂起,唤醒)都要由这个调度器来操作; 下面是一些主要函数: 协程的创建:co_create ```C++ 在调度器里面的协程池里面找到一个空位置; 然后设置一下这个协程的部分参数(函数,以及函数的参数等) 这里并没有直接就执行协程。 ``` 协程的挂起:co_yield ```C++ 将当前协程挂起,回到调度这个协程的父协程中; 通过 swapcontext(&(t->ctx),&(t->fa_co->ctx)); 保存上下文到 t->ctx,切换到父协程中 ``` 协程的唤醒:co_resume ```C++ 这个就稍微复杂了一点: 判断协程是否是第一次唤醒(协程是 就绪,还是 挂起状态;只有这两个状态才能被resume) 如果是挂起状态(不是第一次唤醒) 则直接切换到协程的上下文就行; 如果是就绪状态(第一次唤醒) 由于我们在创建的时候并没有设置上下文信息; 所以在这里我们需要设置一下上下文信息(栈空间,栈空间大小,设置后继上下文); 并设置要被调用协程的父协程信息; 同时设置协程的入口函数 makecontext(&(t->ctx),(void (*)(void))(co_body),1,&schedule); 接着切换到协程的上下文就行。 ``` 还有一些辅助函数: ```C++ // 判断schedule中所有的协程是否都执行完毕,是返回1,否则返回0(其实没什么用hhh) int schedule_finished(const schedule_t &schedule); ``` ```C++ // 协程执行的入口函数 static void co_body(schedule_t *ps); ``` ### hook 系统调用,封装异步 方面 **对应 "co_hook.cpp" 文件** 对于协程,不仅仅是一个挂起唤醒的函数,**更重要的是得封装异步,这样才能保证当一个协程阻塞的时候不会阻塞整个线程,从而体现出协程的真正作用。** **那么如何通过封装异步 使得调用同步函数达到协程异步的效果呢?(这个是核心)** 更加具体的:比如我一个协程调用read,没有数据可以读时我们怎么切换到其他协程上面去?当有数据可以读的时候我们怎么切换回来呢? 分为两部分: (1)我们如何在系统调用中增加自己想要的逻辑代码? (2)如何切回来?      (1)这里我们可以用hook技术(简单来说,hook是一种截取信息、更改程序执行流向、添加新功能的技术)      第一步:声明一个想要hook的函数指针,比如 hook read 函数:typedef ssize_t (*read_fun_ptr)(int __fd, void *__buf, size_t __nbytes);      第二部:动态获取到这个系统调用编译后的入口函数符号,并赋值给上面的指针,read_fun_ptr g_sys_read_fun = (read_fun_ptr)dlsym(RTLD_NEXT, "read");      第三步:重写对应的系统调用就行了,ssize_t read(int __fd, void *__buf, size_t __nbytes) { ... }      这里我 hook 了:accpet, connect, read, write, sleep(实际上都大同小异) (2)在这之前我参考了微信后台在13年开源的 libco 协程库,里面用到了事件驱动,事件循环;      我发现libco底层是用epoll、kqueue实现的;具体设计可以看下面;      在hook函数中注册事件到事件源,然后切出协程,事件源检测到事件触发那么就切回来。 这里最重要的是hook了一个epoll_wait(),用于实现事件循环。 ### 事件驱动 方面 这一部分是**核心**,我搞了好久(没办法,太菜了...) #### 具体设计 这里我设计了三个结构体: (1)事件 ```C++ int fd; //文件描述符 cb_ptr handler; //事件对应的回调函数 void *arg; //回调函数的参数(调度器 + 协程号) ``` (2)定时器 ```C++ time_t timeout; //超时时间 cb_ptr handler; //事件对应的回调函数 void *arg; //回调函数的参数(调度器 + 协程号) ``` (3)事件源(事件监听器) ```C++ int epoll_fd; //epoll实例 epoll_event events[MAX_EVENT_SIZE]; // 结构体数组,接收检测后的数据 map fd_to_event; // fd 到 事件体 的映射 priority_queue,cmp> time_heap; //时间堆 ``` 下面是一些函数 ```C++ //创建一个事件 co_event* creat_event(int fd,cb_ptr handler,void *arg); ``` ```C++ //创建一个超时事件(定时器) co_time* creat_time_event(time_t timeout,cb_ptr handler,void *arg); ``` ```C++ //将事件加入事件源 void add_event(co_event *event,co_event_base *base,EPOLL_EVENTS ty); ``` 这个事件源是内嵌在调度器里面的,主要思想就是: 创建调度器时会创建这个事件源,然后调用事件循环一直跑就行; 这里的事件循环其实就是一直 while(1) { epoll_wait() }; 那么问题就来了,epoll只能检测IO文件描述符,那对于不是IO操作引起的阻塞(比如sleep),我们要怎么检测呢? 所以这里得分为两个部分: 1. IO事件      遇到IO事件时,我们可以创建一个事件来承接这个fd;fd 和 事件 做映射;      然后将事件加入到事件源中,检测就行了。 2. 超时事件      由于epoll不能检测,所以我们要单独搞一个;      我们将超时任务封装成一个超时事件;然后这里用一个时间堆来保存这些超时事件(小根堆,最先超时的在最前面)      那么如何将超时事件和epoll结合起来呢???这里借用了之前写webserver时的思想:      我们创建一对管道(用socket创建,这样epoll才能检测到),接着将管道的读端加入epoll待检测;      然后我们利用arlrm()函数(是一个闹钟,时间到了就会发送信号,是异步的),接着捕捉信号(设置回调函数),往管道里面写东西;      那么我们管道的读端在epoll就能被检测到了,检测到了说明也就说明有超时任务发生;      此时我们就可以从时间堆中取出超时事件,然后执行对应的回调函数(切回协程);这样就实现了对超时任务的检测。      这里需要注意的是,闹钟只会响一次,当我们处理完后发现时间堆里面还有超时事件时,我们需要重新设置闹钟,不然后面的超时事件都检测不到了! --------------------------------------------------------------------------------------------------------------------------------------------------------------------- ## 不足、缺点 以及 改进 1. 首先就是上面提到的多线程的线程安全问题; 2. 是一个非对称协程库,不够灵活; 3. 在调度方面是1:N的调度方式,而向libco之类的是N:M,也就是我的无法跨线程调度(可能得涉及到汇编吧); 也是因为无法跨线程调度,我处理时间堆的时候搞了好久好久... 4. 这个调度器有点繁琐...         这里的这个调度器本质上并没有什么作用...只是起到一个封装的作用...         我们完全可以不用封装成协程池,当创建一个协程的时候,我们直接new一个协程结构体就行了;         那么未来避免内存泄漏,我们同时要引入相应的释放协程体的函数 co_free() (libco就是这么干的) 5. 当然当然,epoll本质上还是同步的,真正实现异步只能用异步IO(这个暂时还不会...) 6. 我只hook了几个函数,很多都没搞...不够全面 7. 编码方面不够规范... 8. 改进         由于我们在创建协程的时候是在协程数组里面找一个空闲的位置分给协程,这样的话时间复杂度就比较高;         我们可以参考 LRU 的改进算法:         维护两条协程链表,一条是创建好的协程(活跃链表),一条是空闲链表;         (当然这里还需要对 协程的编号 以及 协程体这个节点 做一个双向映射)         当我们创建协程的时候,直接在空闲链表里面取一个空闲节点;初始化 并 加入到第一条链表就行;         当协程运行完成,我们就将它从第一条链表中删除,然后将这个节点加入空闲链表就行;         这样的时间复杂度会降低很多,由于需要对 协程号 和 协程体节点 做映射;         这里我考虑是用map,所以时间复杂度是O(logn)的;         当然用unordered_map的话,平均时间复杂应该是O(1)的。         (这个我就暂时还没改hhh) 9. 当然项目里面还有很多很多的细节,大部分都写在注释里了! --------------------------------------------------------------------------------------------------------------------------------------------------------------------- ## 参考文献 **非常非常感谢下面这些文章 以及 开源项目的帮助!!!** [Tencent - libco](https://github.com/Tencent/libco) [微信开源C++Libco介绍与应用(一)](https://zhuanlan.zhihu.com/p/51078499) [微信开源C++Libco介绍与应用(二)](https://zhuanlan.zhihu.com/p/51081816) [当谈论协程时,我们在谈论什么](https://mp.weixin.qq.com/s/IO4ynnKEfy2Rt-Me7EIeqg) [从无栈协程到 C++异步框架](https://mp.weixin.qq.com/s/QVXE7QbxEchl8ue4SoijiQ) [微信终端自研C++协程框架的设计与实现](https://mp.weixin.qq.com/s/c17DaD7JbKlDFT6J8haEFw) [tinyrpc](https://github.com/Gooddbird/tinyrpc#4-%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B) [C++实现的协程异步 RPC 框架 TinyRPC(一)-- 协程封装](https://zhuanlan.zhihu.com/p/466349082) [C++实现的协程异步 RPC 框架 TinyRPC(二)-- 协程Hook](https://zhuanlan.zhihu.com/p/474353906) [协程篇(三)-- 协程Hook](https://zhuanlan.zhihu.com/p/466995546) [Libevent深入浅出 - Aceld(刘丹冰)](https://aceld.gitbooks.io/libevent/content/) --------------------------------------------------------------------------------------------------------------------------------------------------------------------- 2023/5/2 3:32 结束,休息了! 我是一个大三找实习的鼠鼠,加油吧!!!