# 4-蒜头王霸 **Repository Path**: openeuler2020/team-1315222392 ## Basic Information - **Project Name**: 4-蒜头王霸 - **Description**: 来自重庆工商大学 - **Primary Language**: Unknown - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 2 - **Created**: 2020-12-25 - **Last Updated**: 2021-06-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 4-蒜头王霸 TOPIC-ID:4, TEAM_ID:1315222392, TEAM_NAME:蒜头王霸. [答辩ppt](./openeuler决赛答辩ppt.pptx) ## 简介 --- 本项目为重庆工商大学**蒜头王霸队**参加**2020 openEuler高校开发者大赛** 的作品。所参加的赛题为**LUTF - Linux Userspace Task Framework** 。 本框架是一个基于c语言的用户态任务管理框架,目前仅支持单线程模式,且只能在x86-64框架下的各个Linux发行版本中运行。该框架中基本的任务粒度为协程,任务的主体为c语言函数,每个任务可仅占据数KB内存空间,且进行任务切换时无需进行页表刷新等操作,所以无论是创建任务还是任务间上下文切换的代价都远小于在线程间执行相同的操作。 同时为了进行一些必要的同步、互斥操作,框架提供了信号量和协程事件功能,且在事件的基础上利用操作系统的信号(signal)实现中断。 ## 编译 --- ### Linux平台 本协程框架使用autotools工具对整个工程进行编译。要进行编译,首先在工程的根目录下执行该命令,生成configure文件。 ```bash ~/coroutine$ autoreconf -i ``` configure文件生成后,执行以下命令。 ```bash ~/coroutine$ ./configure ``` 然后就可以执行make命令对整个工程进行编译 ```bash ~/coroutine$ make ``` 在完成对整个工程的编译后,能得到以下文件: | path | description | | ------------------------------------------------------------ | ------------------------------------------------------------ | | ./simple_demo
./event_signal
./request_reply | automake生成的脚本文件,通过这些脚本文件可以直接执行例程。 | | ./.libs/simple_demo
./.libs/event_signal
./.libs/request_reply | 例程的可执行文件本体。如果当前系统还没有安装本框架,直接执行这些可执行文件会因为找不到动态库而出错。 | | ./.libs/libcoroutine.so | 一个指向./.libs/libcoroutine.so.0.0.0的链接 | | ./.libs/libcoroutine.so.0.0.0 | 最终生成的协程框架的动态库 | 最后通过以下命令可以将框架安装到系统中,该步骤需要root权限。 ```bash ~/coroutine$ sudo make install ``` ## 例程 --- 为了对协程框架的功能进行展示,以及对框架的使用方式进行示范,我们编写了三个例程存放在仓库的test目录中。在编译协程框架时默认会编译所有例程,完成编译后用户可以通过仓库根目录下生成的脚本运行对应的例程。 - [示例一,协程框架基本流程演示](./test/simple_demo.c) - [示例二,事件的等待与触发](./test/event_signal.c) - [示例三,基于信号量的等待与应答](./test/request_reply.c) ## 代码架构 --- 本框架大部分由c语言编写,仅有协程上下文切换的实现用的是汇编。我们采用GNU Autotools工具对代码进行构建,并采用的git进行版本控制。源代码在仓库中的分布大致如下: ```bash . # 仓库根目录 ├── external # 从linux内核源代码中移植到用户态的模块。 │   ├── include # 从linux内核源代码中移植的头文件。 │   └── src # 从linux内核源代码中移植的源文件。 ├── include # 协程框架核心的头文件。 ├── src # 协程框架核心的代码源文件。 └── test # 例程的源代码。 ``` ### 从Linux内核源代码中移植到用户态的模块 考虑到一个程序对数据结构可靠性的高要求,也为了减少在协程框架的开发过程中可能遇到的潜在的bug,我们选择从Linux的内核源代码里移植本框架所需要的数据结构,作为外部模块存放在```external``` 目录中。具体地,我们移植了Linux5.8的链表和红黑树。 ```bash . # 仓库根目录 ├── external # 存放从Linux内核中移植的代码 │   ├── include # 头文件 │   │   ├── export.h │   │   ├── list.h # 链表。 │   │   ├── list_types.h # 链表数据类型的定义。 │   │   ├── poison.h │   │   ├── porting.h │   │   ├── rbtree_augmented.h # 红黑树一些具体实现的定义。 │   │   ├── rbtree.h # 红黑树的接口头文件。 │   │   ├── rbtree_types.h # 红黑树数据类型的定义。 │   │   └── stringify.h │   └── src # 源文件 │   └── rbtree.c # 红黑树的一些具体实现的源代码。 ``` 我们选择从Linux内核源代码中移植这些模块的原因有以下几点: * Linux作为一个著名的开源软件,遵循GPL协议,而OpenEuler操作系统作为一个Linux的发行版本,同样遵循GPL协议。所以只要我们的代码最终同样遵循GPL协议,就可以对其代码进行任意的修改和使用。 * Linux的红黑树和链表模块提供的接口简单,符合开源社区Linux开发者的使用习惯。并且两者实现的功能专一但全面,不过分依赖Linux内核的其他特性,易于进行移植和在内核以外的环境中使用。 * Linux作为全世界最大的开源项目之一,在数十年间被无数开发者所改进,在大量不同的环境和应用中经受了考验,其稳定性和健壮性不言而喻。而红黑树和链表在内核中得到了广泛地应用,扮演了非常关键的角色,整个内核的表现足以证明Linux中这两个模块极高的可靠性。 不过需要注意的是,我们并没有对这两个模块中所用到的RCU锁等进行完整的移植,因为整个框架只使用到了两者最基本的功能。 ### 协程框架核心的头文件和用户接口 为了易于对协程框架的开发和维护,我们将整个框架划分成了多个相对独立的功能模块,通过以下几种头文件进行互动: * 定义框架所需数据结构与数据类型的头文件,对所有人可见。 * ```./include/cr_types.h``` * 定义各个该功能模块里用户无需关心的功能函数,只对协程框架内部可见。 * ```./include/cr_common.h``` * ```./include/cr_context.h``` * ```./include/cr_event.h``` * ```./include/cr_signal.h``` * ```./include/cr_task.h``` * 定义提供给用户使用协程框架的功能函数,对所有人可见。 * ```./include/coroutine.h``` * ```./include/cr_semaphore.h``` * ```./include/cr_spinlock.h``` * 为简化对框架的相关操作,提供给用户的宏定义。 * ```./include/cr_entry.h``` 并且为了避免头文件之间产生非常杂乱的依赖关系,所有的头文件都遵循了以下原则: * 所有对数据结构和数据类型的定义都必须放在单独的头文件内。 * 所有存放数据结构和数据类型的头文件都不允许存放对功能函数的定义。 * 除内联函数,宏定义的代码片段,头文件中只存在函数原型与全局变量的声明,不允许出现任何函数的具体实现。 * 头文件尽可能少地包含其他头文件,尽可能在**.c** 文件中包含实现当前模块所需的头文件。 最终,用户仅需要包含```coroutine.h``` 即可使用本框架提供的所有功能。 ## 基本宏定义 --- 为了方便对协程信息的传递,方便确定当前正在运行的任务,本框架定义了一些宏对相关操作进行简化。以下为对本框架宏定义的简介。 ### 模块初始化的宏 在启动协程框架前需要先对其进行初始化,包括对初始化全局协程表与主协程控制块,还有对响应操作系统信号的信号处理函数的声明。**本框架中除对框架本身进行初始化的接口外,所有的用户接口都只能在完成对框架的初始化后被调用**。对协程框架的初始化主要涉及到以下三个宏,且这三个宏都是必须的。 - ```CR_SIGNAL_HANDLER()``` 为了让信号处理函数能够找到其所在的全局协程表,需要在main函数之前通过这个宏定义定义信号处理函数。 - ```CR_INIT()``` 对全局协程表和主协程控制块的初始化,以及一些让main函数能够作为主函数的协程入口函数的变量的创建。在对协程以及事件等进行创建,以及启动调度器前,必须调用该宏定义对整个协程框架进行初始化。 - ```CR_WAIT_EVENT_LOOP()``` 在对协程框架的所有初始化操作,以及相关协程、事件等创建完成后,通过这个宏定义启动协程的调度器,开始执行所有的协程任务。在所有协程都退出,或者有协程申请退出调度器后,该宏定义会返回调度器最终的返回值。 **以下是对协程框架的初始化流程。** ```c #include ... /* 必须放在main函数之上。 */ CR_SIGNAL_HANDLER(); ... int main(void) { CR_INIT(); long ret; ... /* 在此处创建协程。 */ ... /* 启动调度器,等待调度器退出并获得调度器的返回值。 */ ret = CR_WAIT_EVENT_LOOP(); return ret; } ``` ### 定义入口函数的宏 由于c语言的语法限制,对协程信息的传递比较繁琐且易出错。为了简化在函数调用的过程中对协程信息的传递流程,本框架提供了以下的宏定义。 - ```__cr_task_func__``` 该宏定义在定义协程内调用的功能函数时,作为参数对协程信息进行传递,一般作为第一个参数。 - ```__cr_task__``` 在调用需要传递协程信息的功能函数时,将该宏作为参数传递给目标的函数。 - ```CR_TASK_ENTRYFUNC(name)``` 通过这个宏可以定义协程的入口函数和析构函数。 - ```CR_TASK_ENTRYHEADER``` 任何需要传递协程信息的函数都必须在其函数体最开始处通过这个宏对传递的信息进行初始化。 **以下是这几个宏的用法示例。** ```c /* 协程内调用的功能函数声明方式与普通函数无异,但需要通过 __cr_task_func__ 宏传递当前的协程控制块。 */ static int task_func(__cr_task_func__, int param) { /* 所有协程内调用的功能函数都需要在函数体的最开始通过该宏对传入的协程信息进行初始化。完成初始 化后,就可以调用任何协程相关的接口函数了。 */ CR_TASK_ENTRYHEADER; ... /* 直接通过return语句进行返回。 */ return 0; } /* 通过宏 CR_TASK_ENTRYFUNC 定义协程的入口函数和析构函数。 */ static CR_TASK_ENTRYFUNC(task_entry) { /* 具体功能同上 */ CR_TASK_ENTRYHEADER; ... /* 在调用协程内调用的功能函数时,通过__cr_task__将当前的协程信息传递到被调用的函数中。 */ task_func(__cr_task__, 0); /* 协程里面一般有一个大循环,在合适条件时不断地重复其负责的任务。 */ while (loop_condition) { ... /* 协程框架无法通过抢占的方式返回调度器,所以协程必须周期性地调用YIELD宏返回调度器。 */ YIELD(0); } ... /* 所有任务都完成后,通过 EXIT 宏退出协程,具体的析构工作将由调度器发起。 */ EXIT(0); } ``` 类似地,本框架提供了以下传递事件信息的宏。 - ```__cr_event_func__``` 该宏定义在定义事件处理函数内调用的功能函数时,作为参数对事件信息进行传递,一般作为第一个参数。 - ```__cr_event__``` 在调用需要传递事件信息的功能函数时,将该宏作为参数传递给目标的函数。 - ```CR_EVENT_ENTRYFUNC(name)``` 通过这个宏可以定义协程事件的处理函数。 - ```CR_EVENT_ENTRYHEADER``` 任何需要传递协事件信息的函数都必须在其函数体最开始处通过这个宏对传递的信息进行初始化。 **以下是这几个宏的用法示例。** ```c static int event_func(__cr_event_func__, int param) { CR_EVENT_ENTRYHEADER; ... return 0; } static CR_EVENT_ENTRYFUNC(event_entry) { CR_EVENT_ENTRYHEADER; ... event_func(__cr_event__, 0); ... return; } ``` ### 获取当前任务及事件信息的宏 本框架提供了一些方便在函数体内获得任务及事件信息的宏。这些宏必须在有对应信息传入的函数中使用,且使用前必须完成对传递信息的初始化。 ```c /* 获得由创建者传递给当前协程的参数。 */ #define CR_ARGS() __cr_args /* 获得指向当前协程协程控制块的指针。 */ #define CR_TASK() __cr_task /* 获得当前事件处理函数的事件控制块。 */ #define CR_EVENT() __cr_event /* 获得当前事件的EID编号。 */ #define CR_EID() (CR_EVENT()->eid) /* 获得当前协程的TID编号。 */ #define CR_TID() (CR_TASK()->tid) /* 获得指向全局协程表的指针。 */ #define CR_MAINTABLE() (CR_TASK()->parent) /* 获得指向主协程协程控制块的指针。 */ #define CR_MAIN_ROUTINE() (CR_TASK()->main_routine) ``` ### 其他主要的宏定义 本框架还提供了对于其他用户接口进行封装的宏定义,建议用户使用宏定义接口。这里只是列出所有的宏,具体功能在下文提及相关接口函数时再详述。 **注意:下文中若出现有宏定义封装的用户接口函数,在对其参数进行解释时只考虑在宏定义中存在的参数。** ```c /* 对协程相关的用户接口的封装 */ #define YIELD(ret) cr_yield(__cr_task__, ret) #define EXIT_ALL(ret) cr_exit_all(__cr_task__, ret) #define EXIT(ret) cr_exit(__cr_task__, ret) #define AWAIT(task, args) cr_await(__cr_task__, task, args) #define CREATE_TASK(entry_func, destructor, stack_size, args) \ cr_create(__cr_task__, entry_func, destructor, stack_size, args) /* 对协程信号量相关的用户接口的封装 */ #define SEM_UP(sem) cr_sem_up(__cr_task__, sem) #define SEM_DOWN(sem) cr_sem_down(__cr_task__, sem) /* 对事件相关的用户接口的封装 */ #define TRIGGER_EVENT(eid, args) cr_trigger_event(__cr_task__, eid, args) #define WAIT_EVENT(eid) cr_wait_event(__cr_task__, eid) #define CREATE_EVENT(eid, callback) cr_create_event(__cr_task__, eid, callback) #define EXIT_EVENT(eid) cr_exit_event(__cr_task__, eid) /* 对信号相关的任务接口的封装 */ #define ADD_SIGNAL(signum, eid) cr_signal_add(__cr_task__, signum, eid) #define DEL_SIGNAL(signum) cr_signal_del(__cr_task__, signum) #define SIGNAL_EID(signum) cr_signal_eid(__cr_task__, signum) ``` ## 调度器与协程 --- 本框架最基本的任务粒度是协程,每个协程有一个协程控制块保存着这个协程大部分关键的信息。一个程序中的所有协程都共享同一个**全局协程表** ,且每个协程在其生命周期内都以各自的TID作为key值储存在全局协程表的红黑树中。 协程的状态由两个因素决定,一个是协程控制块所在的队列,二是协程控制块中的状态字。在协程刚刚创建的时候,这个协程对应的协程控制块会被放入全局协程表的就绪队列中;如果中途协程申请等待某个资源,其协程控制块就会从就绪队列中移除,并放置于对应的等待队列中去,待条件满足后,重新被放回就绪队列中。使得协程进入等待状态的时机主要在协程申请获取信号量或者协程申请等待某个事件。正常情况下,只有一个协程自己可以让自己进入等待状态。 调度器是本框架的另外一个重要的组成部分,决定了该如何向协程分配CPU资源。和操作系统的进程或者线程类似,在单个CPU上同一时刻只能被一个协程占用,而调度器则决定某一时刻该让哪个协程占用CPU资源。由于协程间的竞争关系通常比线程或者进程间的要简单很多,所以本框架简单地采用一个双向链表,基于FIFO调度算法对协程进行调度。在此基础上,协程就可以通过加入、移出就绪队列完成不同任务间的同步与互斥。**需要注意的是,由于无法在单个用户态程序中实现中断,协程间不存在抢占调度,即没有周期性的中断触发调度器进行任务调度,所以每个协程都必须有意地在合适的时机调用YIELD或申请资源等待返回调度器,不然整个框架会完全被单个协程独占。** ### 主协程 大多数情况下,在完成协程框架的初始化后,从调度器返回前,整个程序都会处在某个协程上。调度器也不例外,而调度器所在的这个特殊的协程被称为**主协程**。 主协程一般是首个被创建的协程,创建于协程框架初始化后,结束于调度器返回时。主协程的生命周期就是整个协程框架的生命周期。 主协程的TID为0,和其他协程一样拥有一个单独的协程控制块。但和普通协程不同的是它没有被单独分配栈空间,而是沿用当前线程的栈空间。主协程也不会存在于全局协程表的协程队列或等待队列中,而是作为全局协程表中单独的一项存在,并且所有协程的协程控制块中都有指向主协程协程控制块的指针。所有其他协程在调用YIELD时都会返回主协程,而主协程调用YIELD只会返回其自身。 上文中提到的宏```CR_INIT()```,用于对协程框架进行初始化,其主要调用了如下接口函数: **全局协程表的创建与初始化** ```c global_cr_table_t *cr_init(void); ``` 这个函数会创建全局协程表和主协程的协程控制块,并最终返回全局协程表的指针。在得到全局协程表后,负责初始化的宏就会创建和初始化一些全局/局部变量,使得框架提供的协程接口可以被调用。在初始化宏执行完毕后,程序就已经位于主协程中,此时用户就可以开始进行协程的创建,并对所需要的资源进行进一步的初始化。 在主协程完成对所有协程的创建后,实际上并没有除主协程外的其他协程真正得到运行。要让这些协程真正开始工作,需要通过宏```CR_WAIT_EVENT_LOOP()```激活调度器。该宏主要调用了如下接口函数: **启动调度器并等待其返回** ```c long cr_wait_event_loop( __cr_task_func__, global_cr_table_t* main_table ); ``` 这个函数里面就包含了调度器本体。调度器采用了FIFO调度算法,负责在每次从普通协程返回到主协程时从全局协程表的就绪队列中选取合适的协程进行调度。所有除主协程外的协程都必须通过调度器得到CPU资源。 调度器在进行每次的调度之前,会尝试从就绪队列中查找下一个进行调度的协程。若成功从就绪队列中取得下一个进行调度的协程,调度器会进一步根据选到的协程对其进行恢复上下文或析构等操作。若当前仍未达到结束调度的条件,但就绪队列却为空,即所有普通协程都处于等待状态时,调度器就会向操作系统申请进行一段时间的睡眠,然后继续重复上面的操作。 因为调度器位于主协程中,所以调度器的生命周期直接与主协程绑定。在每次从普通协程返回后,调度器都会先检查主协程的存活情况,如果主协程被某个协程杀死,调度器就会停止任务调度。除此之外,若除主协程外已经没有任何存活的协程,调度器也会停止调度。 一旦调度器停止调度,它就会强制析构并删除掉当前所有的信号、事件、以及全局协程表的红黑树中所有的协程,最终释放掉全局协程表的内存空间,并将主协程的返回值返回给上一层调用者。至此,主协程正式结束,整个调度框架也彻底结束。**在调度器返回以后,因为所有协程框架所需的资源和空间都已经被释放,用户不应该再次调用协程框架的任何接口函数。** ### 停止调度器并退出整个协程框架 ```c #define EXIT_ALL(ret) cr_exit_all(__cr_task__, ret) void cr_exit_all(__cr_task_func__, long ret); ``` 将主协程设置为死亡状态并调用YIELD返回主协程开始进行全局的析构工作。该函数可以在任何协程中被调用。 - 参数 | name | description | input/output | | ---- | ---------------------- | ------------ | | ret | 协程框架退出时的返回值 | input | - 返回值 None ### 保存当前协程的上下文并返回调度器 ```c #define YIELD(ret) cr_yield(__cr_task__, ret) void *cr_yield(__cr_task_func__, long ret); ``` 保存当前协程的上下文,并恢复主协程离开时保存的上下文。该函数可以在任何协程中调用,大多数情况下是由普通协程调用。 - 参数 | name | description | input/output | | ---- | ---------------------------- | ------------ | | ret | 当前协程传递给调度器的返回值 | input | - 返回值 | value | description | | ----- | -------------------------------------- | | 所有 | 由调度器在下一次调度时传递给协程的参数 | ### 恢复某个协程的上下文 ```c #define AWAIT(task, args) cr_await(__cr_task__, task, args) long cr_await(__cr_task_func__, cr_task_struct_t *task, void* args); ``` 保存当前协程的上下文,并恢复目标协程的上下文。该函数可以在任何协程中被调用,**但不建议用户直接调用该接口函数**。 - 参数 | name | description | input/output | | ---- | ------------------------------------ | ------------ | | args | 本次调度时调度器传递给目标协程的参数 | input | - 返回值 | value | description | | ----- | -------------------------- | | 所有 | 目标协程返回时传递的返回值 | ### 申请退出当前协程 ```c #define EXIT(ret) cr_exit(__cr_task__, ret) long cr_exit(__cr_task_func__, long ret); ``` 将当前协程设置为死亡状态,并将ret作为返回值返回主协程。 该函数由普通协程调用,被调用后仅仅会将当前函数设置为死亡状态后返回主协程,不会马上对当前协程进行析构工作。调度器在下次调度到该协程时,会强制让这个协程跳转到析构函数的位置,并恢复其上下文。待该协程再次返回调度器后,调度器再释放该协程的协程控制块等资源。**该函数是为协程信号量和协程事件等在实现对应功能时所调用的,用户不应直接调用该函数。** - 参数 | name | description | input/output | | ---- | -------------------------------- | ------------ | | ret | 本协程退出时传递给调度器的返回值 | input | - 返回值 | value | description | | ----- | ---------------------- | | None | 成功退出协程后不再返回 | | < 0 | 退出协程失败 | ### 唤醒给定协程 ```c long cr_wakeup(cr_task_struct_t *task); ``` 检查给定协程所处的状态,如果给定协程处于等待状态,将该协程设置为就绪状态,并将其放入等待队列。如果给定的协程已经处于就绪状态,则协程唤醒失败。 **该函数是为协程信号量和协程事件等在实现对应功能时所调用的,用户不应直接调用该函数。** - 参数 | name | description | input/output | | ---- | ------------------------------ | ------------ | | task | 需要唤醒的协程的协程控制块指针 | input | - 返回值 | value | description | | ----- | ---------------- | | 0 | 成功唤醒指定协程 | | < 0 | 唤醒指定协程失败 | ### 让某个协程进入等待状态 ```c long cr_asleep(cr_task_struct_t *task); ``` 检查给定协程的状态,如果该协程处于就绪状态,将所该协程从就绪队列中移出,并将其设置为等待状态。该函数不会将协程加入某个等待队列,具体工作需要其调用者来完成。 **该函数是为协程信号量和协程事件等在实现对应功能时所调用的,用户不应直接调用该函数。** - 参数 | name | description | input/output | | ---- | ------------------------------ | ------------ | | task | 需要等待的协程的协程控制块指针 | input | - 返回值 | value | description | | ----- | ------------------------ | | 0 | 指定协程成功进入睡眠状态 | | < 0 | 指定协程进入睡眠状态失败 | ### 创建一个协程 ```c #define CREATE_TASK(entry_func, destructor, stack_size, args) \ cr_create(__cr_task__, entry_func, destructor, stack_size, args) tid_t cr_create( CR_TASK_ENTRYHEADER; __cr_task_func__, cr_task_func_t entry_func, cr_task_func_t destructor, size_t stack_size, void *args ); ``` 根据给定的信息创建一个处于就绪状态的协程,并将其放入全局协程表的就绪队列中。该函数可以在任何协程中被调用,但主协程必须在启动调度器前创建好最初的协程,不然在启动调度器后主协程和全局协程表会立刻被析构。 该函数在创建协程时会判断指定的栈大小是否过小,如果指定的栈大小小于```CR_MINIMUN_STACK_SIZE```的值,则协程创建失败。 **最小的协程栈大小** ```c #define CR_MINIMUN_STACK_SIZE 2048 ``` - 参数 | name | description | input/output | | ---------- | ----------------------------------------------------- | ------------ | | entry_func | 协程的入口函数函数指针,不能为NULL | input | | destructor | 协程的析构函数函数指针,可以为NULL | input | | stack_size | 协程的栈大小,不能小于CR_MINIMUN_STACK_SIZE | input | | args | 创建时传递给协程的参数,可在协程中通过CR_ARGS()宏获得 | input | - 返回值 | value | description | | ----- | --------------------- | | >= 0 | 成功创建的协程TID编号 | | < 0 | 创建协程失败 | ### 杀死一个协程 ```c #define KILL_TASK(victim) cr_kill(__cr_task__, victim) long cr_kill(__cr_task_func__, cr_task_struct_t *victim); ``` 将一个协程设置为死亡状态,当调度器再次调度该协程时,会完成相应的析构工作。该函数可以在任何协程中被调用,但该函数不会主动返回调度器,所以如果需要结束当前协程,请使用```EXIT()```宏。 - 参数 | name | description | input/output | | ------ | -------------------------------- | ------------ | | victim | 需要被杀死的协程的协程控制块指针 | input | - 返回值 | value | description | | ----- | ------------ | | 0 | 成功杀死协程 | | < 0 | 杀死协程失败 | ### 根据协程TID从全局协程表中获得协程控制块 ```c #define GET_TID_TASK(tid) cr_get_tid_task(CR_MAINTABLE(), tid) cr_task_struct_t *cr_get_tid_task( global_cr_table_t *main_table, tid_t tid ); ``` 在主协程表的协程队列中查找给定TID的协程控制块,并将其返回。 - 参数 | name | description | input/output | | ---- | ------------------------------- | ------------ | | tid | 需要获得协程控制块的协程TID编号 | input | - 返回值 | value | description | | ----- | ------------------------- | | NULL | 查找协程控制块失败 | | 其他 | 对应TID的协程控制块的指针 | ## 自旋锁 --- 考虑到今后可能往多线程方向改进,为了避免因多线程竞争导致协程元数据的完整性被破坏,本框架提供了基于CAS操作的自旋锁。 任何任务在申请自旋锁时,都会不断重复尝试,一直原地等待直到最终获得自旋锁为止,所以自旋锁的持有时间不宜过长,且释放所有自旋锁前绝不能以任何方式离开协程,否则极易导致整个框架的效率低下,甚至使得整个框架陷入永久的死锁。 基于上述原因,通常不建议用户使用自旋锁,而是以协程信号量取而代之。 ### 协程自旋锁的初始化 ```c cr_spinlock_t *cr_spinlock_init(cr_spinlock_t * lock); ``` 在为一个协程自旋锁分配完空间后,调用该函数对自旋锁进行初始化,使其处于解锁状态,最后返回自旋锁的指针。 - 参数 | name | description | input/output | | ---- | ------------------------------ | ------------ | | lock | 需要进行初始化的协程自旋锁指针 | input | - 返回值 | value | description | | ----- | ---------------------------- | | NULL | 初始化失败。 | | 其他 | 初始化后的协程自旋锁的指针。 | ### 自旋等待获得自旋锁 ```c void cr_spinlock_acquire(cr_spinlock_t *lock); ``` 不断重复尝试获得自旋锁,直到获得了自旋锁为止。 - 参数 | name | description | input/output | | ---- | ------------------------------ | ------------ | | lock | 自选等待并加锁的协程自旋锁指针 | input | - 返回值 None ### 释放协程自旋锁 ```c void cr_spinlock_release(cr_spinlock_t *lock); ``` 强行释放自旋锁,无论给定的自旋锁处于加锁还是解锁状态,都使其处于解锁状态。 - 参数 | name | description | input/output | | ---- | ------------------------ | ------------ | | lock | 需要释放的协程自旋锁指针 | input | - 返回值 None ### 尝试获得协程自旋锁 ```c bool cr_spinlock_try_acquire(cr_spinlock_t *lock); ``` 尝试获得自旋锁,无论有没有成功获得自旋锁,都直接返回。 - 参数 | name | description | input/output | | ---- | -------------------------- | ------------ | | lock | 尝试获得的协程自旋锁的指针 | input | - 返回值 | value | description | | ----- | ------------------ | | true | 成功获得协程自旋锁 | | false | 获得协程自旋锁失败 | ### 查看自旋锁是否已被占用 ```c bool cr_spinlock_is_locked(cr_spinlock_t *lock); ``` - 参数 | name | description | input/output | | ---- | ---------------------------------- | ------------ | | lock | 需要查看占用状态的协程自旋锁的指针 | input | - 返回值 | value | description | | ----- | ---------------------- | | true | 协程自旋锁已经被占用 | | false | 协程自旋锁处于空闲状态 | ## 信号量 --- 协程信号量是实现协程同步互斥的重要工具,其功能与Linux的信号量类似,但是会被休眠或者唤醒的是协程,而不是线程。 在本协程框架中,只有当协程进行down操作时无法立刻分配到信号量才会陷入休眠状态然后立刻返回调度器,其余任何情况下对信号量的操作均不会导致协程返回调度器,所以请注意在需要的地方手动调用YIELD从而返回调度器。 ### 协程信号量的初始化 ```c cr_semaphore_t *cr_sem_init(cr_semaphore_t *sem, long count); ``` 为协程信号量分配了内存空间后,通过该函数将协程信号量初始化至给定计数的状态。 - 参数 | name | description | input/output | | ----- | ------------------------------ | ------------ | | sem | 需要进行初始化的协程信号量指针 | input | | count | 协程信号量初始的计数值 | input | - 返回值 | value | description | | ----- | ---------------------------- | | NULL | 初始化失败。 | | 其他 | 初始化后的协程自旋锁的指针。 | ### 释放协程信号量 ```c #define SEM_UP(sem) cr_sem_up(__cr_task__, sem) void cr_sem_up(__cr_task_func__, cr_semaphore_t *sem); ``` 对协程信号量进行UP操作。若指定协程信号量的就绪队列不为空,将其中一个协程唤醒;如果指定协程信号量的就绪队列为空,则将信号量的计数加一。 - 参数 | name | description | input/output | | ---- | ---------------------------- | ------------ | | sem | 释放资源计数的协程信号量指针 | input | - 返回值 None ### 申请协程信号量 ```c #define SEM_DOWN(sem) cr_sem_down(__cr_task__, sem) void cr_sem_down(__cr_task_func__, cr_semaphore_t *sem); ``` 对协程信号量进行DOWN操作。若指定协程信号量的计数大于0,直接将信号量的计数减一;如果指定信号量的计数小于等于0,则让当前协程设置为等待状态,放入信号量的就绪队列,并最终调用YIELD返回调度器。 - 参数 | name | description | input/output | | ---- | ------------------------ | ------------ | | sem | 申请资源的协程信号量指针 | input | - 返回值 None ### 查看信号量当前值 ```c long cr_sem_val(cr_semaphore_t *sem); ``` - 参数 | name | description | input/output | | ---- | -------------- | ------------ | | sem | 协程信号量指针 | input | - 返回值 | value | description | | ----- | ------------------ | | 所有 | 给定信号量当前的值 | ## 事件 --- 事件是本框架提供用于实现任务间异步操作的另一个重要工具,是协程框架响应操作系统信号的一个基础。 协程可以通过事件的EID编号申请等待一个已经被创建的事件的触发,并陷入等待状态后返回调度器。一旦事件被操作系统的信号或者其他协程触发,首先将执行对应事件的回调函数。待事件回调函数执行完毕后,若有协程正在等待事件的触发,将所有正在等待该事件被触发的协程唤醒。只有申请对事件的等待会导致整个框架返回调度器,所以在操作事件时同样需要注意在合适的地方手动调用YIELD返回调度器。 ### 事件的创建与添加 ```c #define CREATE_EVENT(eid, callback) cr_create_event(__cr_task__, eid, callback) eid_t cr_create_event( __cr_task_func__, eid_t eid, cr_event_func_t callback ); ``` 初始化指定EID编号的协程事件,并设置其被触发时调用的回调函数。**已经被占用的EID编号在对应事件被释放前不允许被第二次被用于创建事件,且EID必须介于0~eid_limit之间**。 - 参数 | name | description | input/output | | -------- | ------------------------------------------------------------ | ------------ | | eid | 需要添加的事件EID编号。如果指定EID为-1,会从全局协程表中挑选一个空闲的EID。 | input | | callback | 事件被触发时调用的事件回调函数 | input | - 返回值 | value | description | | ----- | ----------------------- | | < 0 | 事件添加失败 | | >= 0 | 刚刚创建的事件EID编号。 | ### 事件的删除 ```c #define EXIT_EVENT(eid) cr_exit_event(__cr_task__, eid) long cr_exit_event(__cr_task_func__, eid_t eid); ``` 将指定EID编号对应的协程事件删除。 - 参数 | name | description | input/output | | ---- | ----------------------- | ------------ | | eid | 需要删除的事件的EID编号 | input | - 返回值 | value | description | | ----- | ------------ | | 0 | 事件删除成功 | | < 0 | 事件删除失败 | ### 触发事件 ```c #define TRIGGER_EVENT(eid, args) cr_trigger_event(__cr_task__, eid, args) long cr_trigger_event(__cr_task_func__, eid_t eid, void *args); ``` 触发指定EID编号的协程事件,并将args作为参数调用被触发事件的回调函数,最后唤醒所有位于事件等待队列中的协程。 - 参数 | name | description | input/output | | ---- | ------------------------ | ------------ | | eid | 需要触发的事件EID编号 | input | | args | 传递给事件回调函数的参数 | input | - 返回值 | value | description | | ----- | ------------ | | 0 | 事件触发成功 | | < 0 | 事件触发失败 | ### 等待事件的触发 ```c #define WAIT_EVENT(eid) cr_wait_event(__cr_task__, eid) long cr_wait_event(__cr_task_func__, eid_t eid); ``` 申请等待给定EID编号的协程事件,并进入等待状态。在协程所等待的事件被触发后,协程会重新回到就绪状态。 - 参数 | name | description | input/output | | ---- | --------------------- | ------------ | | eid | 需要等待的事件EID编号 | input | - 返回值 | value | description | | ----- | ---------------- | | 0 | 申请等待事件成功 | | < 0 | 申请等待事件失败 | ## 系统信号 --- 本框架通过操作系统的信号机制实现中断,一旦接受到了来自操作系统的信号,与之绑定的协程事件会被触发。 本框架在响应操作系统发出的信号前,需要先创建一个事件,并将系统信号的编号与协程事件的EID编号相绑定。本框架有一个默认的操作系统信号处理函数,唯一的动作就是查找并触发与收到的信号相绑定的事件。除此之外,本框架不提供用户自定义操作系统信号处理函数的接口,因为所有的事情完全可以放在协程事件的回调函数中完成。 ### 添加信号 ```c #define ADD_SIGNAL(signum, eid) cr_signal_add(__cr_task__, signum, eid) long cr_signal_add(__cr_task_func__, int signum, eid_t eid); ``` 向操作系统注册一个信号,并将其与给定EID编号的事件相绑定。signum和EID都必须合法,不然信号添加失败。 - 参数 | name | description | input/output | | ------ | ----------------------------------- | ------------ | | signum | 需要添加的系统信号编号 | input | | eid | 该信号到来时触发的协程事件的EID编号 | input | - 返回值 | value | description | | ----- | ------------ | | 0 | 添加信号成功 | | < 0 | 添加信号失败 | ### 删除信号 ```c #define DEL_SIGNAL(signum) cr_signal_del(__cr_task__, signum) long cr_signal_del(__cr_task_func__, int signum); ``` 删除一个信号。被删除的信号必须已经被添加,否则删除失败。 - 参数 | name | description | input/output | | ------ | ---------------------- | ------------ | | signum | 需要删除的系统信号编号 | input | - 返回值 | value | description | | ----- | ------------ | | 0 | 删除信号成功 | | < 0 | 删除信号失败 | ### 获得相应某个信号的事件的EID编号 ```c #define SIGNAL_EID(signum) cr_signal_eid(__cr_task__, signum) eid_t cr_signal_eid(__cr_task_func__, int signum); ``` - 参数 | name | description | input/output | | ------ | ------------------------- | ------------ | | signum | 需要获得EID的系统信号编号 | input | - 返回值 | value | description | | ----- | ----------------------------------- | | -1 | 获得EID失败 | | >= 0 | 编号为signum的信号对应的事件EID编号 | ## 未来的改进 --- - 本框架在创建之初主要参考了操作系统以进程为主的任务管理与调度模型,为此我们为每个协程都指定了TID编号,且将所有的协程都加入到了一个红黑树中。在进一步构建整个框架的过程中,我们发现协程的TID几乎没有起到什么作用,但上面的步骤对协程的创建等操作带来了较大的代价,所以我们会在未来逐渐抛弃协程TID。 - 本框架采用的调度算法过于初级,目前仅仅依靠一个简单的双向链表实现就绪队列,运用先进先出算法进行任务调度。所以在任务的并发量特别大的时候,对实时性要求较高的任务极容易等待过长时间。为缓解这些问题,我们需要对调度算法进行进一步的完善,例如添加优先级的概念,使得更急迫的任务能够更早被完成。 - 作为一个面向百万级并发的任务管理框架,目前为止只能运用单独一个CPU核心,难以在实际应用中发挥其所有潜力。我们为此会在未来实现多线程的任务调度。 - 本框架目前缺乏实际的应用,所以整个框架的架构和API都还有很大的改进空间。