本文共 14391 字,大约阅读时间需要 47 分钟。
【1】协程简介
IO 同步操作的逻辑代码
IO 异步操作的逻辑代码
IO 异步操作与 IO 同步操作对比
协程主要解决的问题,提供同时具有异步性能与同步代码逻辑的解决方案;C/C++ 典型的协程库 NtyCo,;
【2】NtyCo 简介
【2.1】NtyCo 相关 API 简介
NtyCo 封装了若干接口,一类是协程本身的,一类是 posix 异步封装协程 API;
协程相关 API 函数接口
协程创建int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg);协程调度器运行void nty_schedule_run(void);
POSIX 异步封装 API 函数接口
创建 socke 实例int nty_socket(int domain, int type, int protocol);接收请求int nty_accept(int fd, struct sockaddr *addr, socklen_t *len);接收数据ssize_t nty_recv(int fd, void *buf, size_t len, int flags);发送数据ssize_t nty_send(int fd, const void *buf, size_t len, int flags);关闭 socket 实例int nty_close(int fd);
【2.2】协程的实现
1. 协程的实现之创建协程
int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg);
协程不存在亲属关系,都是一致的调度关系,接受调度器的调度;调用 create API 就会创建一个新协程,新协程就会加入到调度器的就绪队列中;
2. 协程的实现之实现 IO 异步操作
调度器与协程的上下文切换图示
在协程的上下文 IO 异步操作函数,步骤如下
IO 异步操作的上下文切换的时序图
3. 协程的实现之回调协程的子过程
关键示例代码
static void nty_coroutine_init(nty_coroutine *co) { void **stack = (void **)(co->stack + co->stack_size); stack[-3] = NULL; stack[-2] = (void *)co; // ctx 协程的上下文 // 可以将回调函数地址存入 EIP 中,将相应参数存储到相应的参数寄存器中 co->ctx.esp = (void*)stack - (4 * sizeof(void*)); co->ctx.ebp = (void*)stack - (3 * sizeof(void*)); // CPU 寄存器 EIP 用于存储 CPU 运行下一条指令的地址 co->ctx.eip = (void*)_exec; // 设置回调函数入口 co->status = BIT(NTY_COROUTINE_STATUS_READY); }
static void _exec(void *lt) {#if defined(__lvm__) && defined(__x86_64__) __asm__("movq 16(%%rbp), %[lt]" : [lt] "=r" (lt));#endif nty_coroutine *co = (nty_coroutine*)lt; co->func(co->arg); // co->status |= (BIT(NTY_COROUTINE_STATUS_EXITED) | BIT(NTY_COROUTINE_STATUS_FDEOF) | BIT(NTY_COROUTINE_STATUS_DETACH)); #if 1 nty_coroutine_yield(co);#else co->ops = 0; _switch(&co->sched->ctx, &co->ctx);#endif}
4. 协程实现之原语操作
协程的核心原语操作 : create, resume, yield;
协程的原语操作有 create 但却没有 exit 的分析
原语 create : 创建一个协程
// 1. 调度器是否存在,不存在也创建;调度器作为全局的单例;将调度器的实例存储在线程的私有空间 pthread_setspecific// 2. 分配一个 coroutine 的内存空间,分别设置 // coroutine 的数据项,栈空间,栈大小,初始状态,创建时间,子过程回调函数,子过程的调用参数// 3. 将新分配协程添加到就绪队列 ready_queue 中int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg)
原语 yield : 让出 CPU
// 参数:需要恢复运行的协程实例// 调用后该函数也不会立即返回,而是切换到运行协程实例的 yield 的位置;// 返回是在等协程相应事务处理完成后,主动 yield 会返回到 resume 的地方;void nty_coroutine_yield(nty_coroutine *co)
原语 resume : 恢复协程的运行权
// 参数:需要恢复运行的协程实例// 调用后该函数也不会立即返回,而是切换到运行协程实例的 yield 的位置;// 返回是在等协程相应事务处理完成后,主动 yield 会返回到 resume 的地方;int nty_coroutine_resume(nty_coroutine *co)
5. 协程实现之切换
上下文切换,就是将 CPU 的寄存器暂时保存,再将即将运行的协程的上下文寄存器,分别 mov 到相对应的寄存器上,从而完成上下文的切换;
nty_cpu_ctx 结构体
// 关联相关寄存器的结构体typedef struct _nty_cpu_ctx { void *esp; void *ebp; void *eip; void *edi; void *esi; void *ebx; void *r1; void *r2; void *r3; void *r4; void *r5;} nty_cpu_ctx;
_switch 函数 API
// 参数 1 : 即将运行协程的上下文,寄存器列表// 参数 2 : 正在运行协程的上下文,寄存器列表int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);// _switch 返回后,执行即将运行协程的上下文,从而实现上下文的切换
_switch 函数实现代码分析
// %rdi 函数第一个参数, %rsi 函数第二个参数__asm__ (" .text \n"" .p2align 4,,15 \n"".globl _switch \n"".globl __switch \n""_switch: \n""__switch: \n"" movq %rsp, 0(%rsi) # save stack_pointer \n" // 保存栈指针到 cur_ctx 实例的 rsp 项" movq %rbp, 8(%rsi) # save frame_pointer \n" // 保存帧指针到 cur_ctx 实例的 rbp 项" movq (%rsp), %rax # save insn_pointer \n" // 将栈顶地址里面的值存储到 rax 寄存器中" movq %rax, 16(%rsi) \n" // 保存 rax,rbx,r12 - r15" movq %rbx, 24(%rsi) # save rbx,r12-r15 \n"" movq %r12, 32(%rsi) \n"" movq %r13, 40(%rsi) \n"" movq %r14, 48(%rsi) \n"" movq %r15, 56(%rsi) \n"" movq 56(%rdi), %r15 \n" // 将 rdi 对应偏移量中的值保存入相应的寄存器中" movq 48(%rdi), %r14 \n" // 即将 rdi 对应偏移量中的值保存入 new_ctx 的每一个项中" movq 40(%rdi), %r13 # restore rbx,r12-r15 \n"" movq 32(%rdi), %r12 \n"" movq 24(%rdi), %rbx \n"" movq 8(%rdi), %rbp # restore frame_pointer \n"" movq 0(%rdi), %rsp # restore stack_pointer \n"" movq 16(%rdi), %rax # restore insn_pointer \n" // 将指令指针 rip 的值存储到 rax 中" movq %rax, (%rsp) \n" // 将存储 rip 值的 rax 寄存器赋值给栈指针指向的地址处" ret \n" // 出栈,回到栈指针,执行 rip 指向的指令);
6. 协程实现之定义
协程的运行体 R 与运行体调度器 S
6.1 运行体在多种状态集合的切换
协程的状态
就绪 (ready),睡眠 (sleep),等待 (wait) 集合对应的数据结构
图示中,Coroutine 是协程的相应属性,status 表示协程的运行状态,并包含 sleep 与 wait 两棵红黑树以及 ready 队列;前提条件是不管何种运行状态的协程,都在就绪队列中,只是同时包含有其他的运行状态;
6.2 调度器与协程的功能界限
协程属性 : 每一协程都需要使用的而且可能会不同的属性;调度器属性 : 每一协程都需要的而且一致的数据;
协程的核心结构体
typedef struct _nty_coroutine { //private // 协程自身的上下文,需要保存 CPU 的寄存器 ctx nty_cpu_ctx ctx; // 子过程的回调函数 func proc_coroutine func; // 子过程回调函数的参数 arg 以及数据 data void *arg; void *data; // 协程自身的栈空间的大小 stack_size size_t stack_size; size_t last_stack_size; // 协程当前的运行状态 nty_coroutine_status status; // 调度器的全局对象 nty_schedule *sched; // 协程的创建时间 uint64_t birth; // 协程 id uint64_t id;#if CANCEL_FD_WAIT_UINT64 int fd; unsigned short events; //POLL_EVENT#else int64_t fd_wait;#endif char funcname[64]; struct _nty_coroutine *co_join; void **co_exit_ptr; // 协程自身的栈空间 void *stack; void *ebp; uint32_t ops; uint64_t sleep_usecs; // 当前运行状态的结点 RB_ENTRY(_nty_coroutine) sleep_node; RB_ENTRY(_nty_coroutine) wait_node; LIST_ENTRY(_nty_coroutine) busy_next; // TAILQ_ENTRY(_nty_coroutine) ready_next; TAILQ_ENTRY(_nty_coroutine) defer_next; TAILQ_ENTRY(_nty_coroutine) cond_next; TAILQ_ENTRY(_nty_coroutine) io_next; TAILQ_ENTRY(_nty_coroutine) compute_next; struct { void *buf; size_t nbytes; int fd; int ret; int err; } io; struct _nty_coroutine_compute_sched *compute_sched; int ready_fds; struct pollfd *pfds; nfds_t nfds;} nty_coroutine;
调度器的属性,需要有保存 CPU 的寄存器上下文 ctx,可以从协程运行状态 yield 到调度器运行;从协程到调度器用 yield,从调度器到协程用 resume;
调度器结构体
typedef struct _nty_coroutine_link nty_coroutine_link;typedef struct _nty_coroutine_queue nty_coroutine_queue;typedef struct _nty_coroutine_rbtree_sleep nty_coroutine_rbtree_sleep;typedef struct _nty_coroutine_rbtree_wait nty_coroutine_rbtree_wait;typedef struct _nty_schedule { uint64_t birth; nty_cpu_ctx ctx; void *stack; size_t stack_size; int spawned_coroutines; uint64_t default_timeout; struct _nty_coroutine *curr_thread; int page_size; int poller_fd; int eventfd; struct epoll_event eventlist[NTY_CO_MAX_EVENTS]; int nevents; int num_new_events; pthread_mutex_t defer_mutex; nty_coroutine_queue ready; nty_coroutine_queue defer; nty_coroutine_link busy; nty_coroutine_rbtree_sleep sleeping; nty_coroutine_rbtree_wait waiting; //private } nty_schedule;
7. 协程实现之调度器
调度器的实现,有两种方案,一种是生产者消费者模式,另一种是多状态运行;
7.1 生产者消费者模式
逻辑代码示例
while (1) { // 遍历睡眠集合,将满足条件的协程加入到 ready 队列 nty_coroutine *expired = NULL; while ((expired = sleep_tree_expired(sched)) != NULL) { TAILQ_ADD(&sched->ready, expired); } // 遍历等待集合,将满足条件的协程加入到 ready 队列 nty_coroutine *wait = NULL; int nready = epoll_wait(sched->epfd, events, EVENT_MAX, 1); for (i = 0; i < nready; i ++) { wait = wait_tree_search(events[i].data.fd); TAILQ_ADD(&sched->ready, wait); } // 使用 resume 恢复 ready 的协程运行权 while (!TAILQ_EMPTY(&sched->ready)) { nty_coroutine *ready = TAILQ_POP(sched->ready); resume(ready); }}
7.2 多状态运行
逻辑代码示例
while (1) { // 遍历睡眠集合,使用 resume 恢复 expired 的协程运行权 nty_coroutine *expired = NULL; while ((expired = sleep_tree_expired(sched)) != NULL) { resume(expired); } // 遍历等待集合,使用 resume 恢复 wait 的协程运行权 nty_coroutine *wait = NULL; int nready = epoll_wait(sched->epfd, events, EVENT_MAX, 1); for (i = 0; i < nready; i ++) { wait = wait_tree_search(events[i].data.fd); resume(wait); } // 使用 resume 恢复 ready 的协程运行权 while (!TAILQ_EMPTY(sched->ready)) { nty_coroutine *ready = TAILQ_POP(sched->ready); resume(ready); }}
补充知识点
1. X86-64寄存器和栈帧
1.1 寄存器
通用寄存器
X86_64 有 16 个 64 位寄存器,分别是:%rax,%rbx,%rcx,%rdx,%rsi,%rdi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15;
段寄存器
状态和控制寄存器 eflags
该寄存器表示的意义非常丰富,程序中并不直接操作此寄存器,并由此衍生出很多操作指令,除去一些保留位,其他每位都代表一个具体的含义,
其中 bits 0, 2, 4, 6, 7, 11 是状态位,标识了此操作后的状态
8, 9, 10 位为控制标识
指令寄存器 EIP
EIP —— 标志当前进程将要执行指令位置,在64位模式下扩展为 RIP 64位指令寄存器
1.2 栈帧
1.2.1 栈帧结构
C 语言属于面向过程语言,其最大特点就是把一个程序分解成若干过程 (函数),比如:入口函数是 main,然后调用各个子函数;在对应机器语言中,GCC 把过程转化成栈帧 (frame),每个栈帧对应一个过程;X86-32 典型栈帧结构中,由 %ebp 指向栈帧开始,%esp 指向栈顶;
1.2.2 C 函数与汇编分析
int foo ( int x ){ int array[] = {1,3,5}; return array[x];} /* ----- end of function foo ----- */int main ( int argc, char *argv[] ){ int i = 1; int j = foo(i); fprintf(stdout, "i=%d,j=%d\n", i, j); return EXIT_SUCCESS;} /* ----- end of function main ----- */
未经过优化的结果
优化后的结果
从优化可见,此处直接引用了栈顶之外的空间;
访问栈顶之外
通过 readelf 查看可执行程序的 header 信息
根据红色区域可见 x86-64 遵循 ABI 规则的版本,其定义了一些规范,遵循 ABI 的具体实现应该满足这些规范,其中,便规定了程序可以使用栈顶之外 128 字节的地址;
汇编相关分析
一、movl value, %eax
二、call foo
三、ret
四、cltq
五、.LC0
.LC0 指一个字符串,此处代表 fprintf 的格式化字符串;
六、leave
leave 指令等价于
Movq %rbp %rsp // 撤销栈空间,回滚 %rspPopq %rbp // 恢复上一个栈帧的 %rbp
1.3 寄存器保存惯例
C 语言示例代码
#include#include void sfact_helper ( long int x, long int * resultp){ if (x <= 1) *resultp = 1; else { long int nresult; sfact_helper(x-1, &nresult); *resultp = x * nresult; }} /* ----- end of function foo ----- */long int sfact ( long int x ){ long int result; sfact_helper(x, &result); return result;} /* ----- end of function sfact ----- */intmain ( int argc, char *argv[] ){ int sum = sfact(10); fprintf(stdout, "sum=%d\n", sum); return EXIT_SUCCESS;} /* ---------- end of function main ---------- */
sfact_helper 函数生成的汇编代码
在函数 sfact_helper 中,用到了寄存器 %rbx 和 %rbp,在覆盖之前,GCC 选择了先保存他们的值,如代码 6~9 所示;在函数返回之前,GCC 依次恢复了他们,如代码 27-28 所示;
%rbx 在函数进入的时候指向的是 -16(%rsp),而在退出的时候,指向的是32(%rsp);此处使用了访问栈帧之外的空间的特性,即 GCC 不用预先分配空间再使用,而是先使用栈空间,然后在适当的时机分配,如第 11 行代码分配了空间,之后栈指针发生变化,所以同一个地址的引用偏移也相应做出调整;
注意事项
汇编相关分析
一、lea 指令
二、imul 指令
三、mul 指令
1.4 参数传递范例
C 示例代码
#include#include int foo ( int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7 ){ int array[] = {100, 200, 300, 400, 500, 600, 700}; int sum = array[ arg1 ] + array[ arg7 ]; return sum;} /* ----- end of function foo ----- */intmain ( int argc, char *argv[] ){ int i = 1; int j = foo(0, 1, 2, 3, 4, 5, 6); fprintf(stdout, "i = %d, j = %d\n", i, j); return EXIT_SUCCESS;} /* ---------- end of function main ---------- */
生成的汇编代码
Main 函数中,代码 31~37 准备函数 foo 的参数
Foo 函数中,代码 14-15,分别取出参数 7 和参数 1,参与运算;
此处数组引用,使用了最经典的寻址方式,-40(%rsp,%rdi,4) = %rsp + %rdi * 4 + (-40); 其中 %rsp 用作数组基地址,%rdi 用作了数组的下标,数字 4 表示 sizeof(int) = 4
1.5 结构体传参
C 示例代码
#include#include struct demo_s { char var8; int var32; long var64;};struct demo_s foo (struct demo_s d){ d.var8 = 8; d.var32 = 32; d.var64 = 64; return d;} /* ----- end of function foo ----- */intmain ( int argc, char *argv[] ){ struct demo_s d, result; result = foo (d); fprintf(stdout, "demo: %d, %d, %ld\n", result.var8, result.var32, result.var64); return EXIT_SUCCESS;} /* ---------- end of function main ---------- */
生成汇编代码
参考与致谢
本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。【1】协程的实现与原理 [ M ]
【2】
【3】
【4】
【5】
【6】
【7】
【8】
转载地址:http://olmr.baihongyu.com/