学习笔记_协程的实现和原理
ucontext
user context 用户上下文
23 general register 23个通用寄存器
enum
{
REG_R8 = 0,
# define REG_R8 REG_R8
REG_R9,
# define REG_R9 REG_R9
REG_R10,
# define REG_R10 REG_R10
REG_R11,
# define REG_R11 REG_R11
REG_R12,
# define REG_R12 REG_R12
REG_R13,
# define REG_R13 REG_R13
REG_R14,
# define REG_R14 REG_R14
REG_R15,
# define REG_R15 REG_R15
REG_RDI,
# define REG_RDI REG_RDI
REG_RSI,
# define REG_RSI REG_RSI
REG_RBP,
# define REG_RBP REG_RBP
REG_RBX,
# define REG_RBX REG_RBX
REG_RDX,
# define REG_RDX REG_RDX
REG_RAX,
# define REG_RAX REG_RAX
REG_RCX,
# define REG_RCX REG_RCX
REG_RSP,
# define REG_RSP REG_RSP
REG_RIP,
# define REG_RIP REG_RIP
REG_EFL,
# define REG_EFL REG_EFL
REG_CSGSFS, /* Actually short cs, gs, fs, __pad0. */
# define REG_CSGSFS REG_CSGSFS
REG_ERR,
# define REG_ERR REG_ERR
REG_TRAPNO,
# define REG_TRAPNO REG_TRAPNO
REG_OLDMASK,
# define REG_OLDMASK REG_OLDMASK
REG_CR2
# define REG_CR2 REG_CR2
};
主要的结构体
mcontext_t 描述整个处理器状态的上下文
/* Context to describe whole processor state. */
typedef struct
{
gregset_t gregs; // 保存23个通用寄存器状态
fpregset_t fpregs; // 表示浮点寄存器FPU register的状态指针
__extension__ unsigned long long __reserved1 [8]; // 保留
} mcontext_t;
ucontext_t 用户上下文
/* Userlevel context. */
typedef struct ucontext {
unsigned long int uc_flags;
struct ucontext* uc_link; // 当前context执行结束后要执行的下一个context
// 如果uc_link为空,执行完当前context后退出程序
stack_t uc_stack; // 当前使用的栈信息, 当前context所需stack
mcontext_t uc_mcontext; // 当前处理器状态, 保存具体的程序执行上下文
__sigset_t uc_sigmask; // 执行context过程中, 要屏蔽的信号集合, 即信号掩码
struct _libc_fpstate __fpregs_mem; //表示浮点寄存器FPUregister的状态
} ucontext_t;
/*Alternate, preferred interface.*
typedef struct sigaltstack {
void* ss_sp;
int ss_flags;
size_t ss_size;
} stack_t
主要函数, 成功返回0, 错误返回-1, 并设置errno
int getcontext(ucontext_t *ucp)
将当前context保存到ucp中
int setcontext(const ucontext_t* ucp)
恢复当前context为ucp所指的context, 成功的调用函数不会返回,
ucp所指的context应该通过getcontext或makecontext获取
void makecontext(ucontext_t* ucp, void(*func)(), int argc, ...)
修改context ucp, 再getcontext获取到context后才可以调用,
可以通过修改ucp中的uc_stack来自定义context使用的堆和堆内存空间大小,
修改uc_link设置完成后运行的上下文,
传入func函数来设置context ucp要执行的函数, argc为传入func的参数数量,
...为传入的参数
int swapcontext(ucontext_t*_restrict_oucp, const ucontext_t* _restrict_ ucp)
保存当前context到oucp中, 然后激活ucp context, 执行成功函数不返回
协程框架
为什么会有协程,解决什么问题?
同步的编程方式,异步的性能,写代码的时候同步,运行的逻辑异步。
异步的性能,将检测IO和读写IO放在不同的线程中
同步的编程方式,麻烦点,性能低,好处是,逻辑简单。
异步的编程方式,麻烦点,免多个线程公用一个fd的现象,这种现象会造成recv的数据乱序和突然被另一边关闭的问题。好处是,性能比较高。
客户端流程
send之后进入 让出 ,进入epoll_wait,判断是否可以recv,否就 切换 到其他的send
// 同步的编程方式,检测IO和读写IO在同一流程里面
fun() {
while(1) {
epoll_wait()
for(;;) {
recv();
send();
}
}
}
// 异步的编程方式,epoll_wait和recv&send不在同一流程里面
thread_cb() {
poll();//在异步编程中利用poll对fd进行处理
recv();
send();
}
fun() {
while(1) {
epoll_wait()
for(;;) {
push_other_thread(thread_cb);
}
}
}
// 异步的编程方式,利用对event的增删一样不推荐
thread_cb() {
recv();
epol_ctl(EPOLL_CTL_ADD,EPOLLIN);
send();
}
fun() {
while(1) {
epoll_wait()
for(;;) {
epol_ctl(EPOLL_CTL_DEL,EPOLLIN);
push_other_thread(thread_cb);
}
}
}
// 如何把下面的流程变为异步
epoll_wait()
for(;;) {
recv()
parser()
send()
}
// 变成:io操作-》epoll检测-》io操作 这样子的循环
// 这就是协程的雏形,是在同一个线程里面
while(idx++ < 50) {
send(fd);
event->fd = fd;
event->event = EPOLLIN;
epoll_ctl(epfd, add, event);
int nready = epoll_wait(epfd);
while(i++ < nready) {
recv();
}
}
// 协程,yield resume
while(idx++ < 50) {
send(fd);
event->fd = fd;
event->event = EPOLLIN;
epoll_ctl(epfd, add, event);
jup label;//切换到epoll_wait yeild
}
label:
int nready = epoll_wait(epfd);
while(i++ < nready) {
recv();
}
ret;//回到循环 resume
原语 yield, resume,让出,恢复
协程切换相关寄存器
-
X86-64的16个64位寄存器:
// 为了兼容X86,结构体命令采用的是x86的寄存器名字命名
typedef struct _nty_cpu_ctx {
void *esp; //% rsp
void *ebp; //% rbp
void *eip;
void *edi;
void *esi;
void *ebx;
void *r1;
void *r2;
void *r3;
void *r4;
void *r5;
} nty_cpu_ctx;int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);
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"
" movq %rbp, 8(%rsi) # save frame_pointer \n"
" movq (%rsp), %rax # save insn_pointer \n"
" movq %rax, 16(%rsi) \n"
" 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"
" movq 48(%rdi), %r14 \n"
" 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"
" movq %rax, (%rsp) \n"
" ret \n"
);
切换
-
goto为什么不能实现?
goto是在栈内进行切换 -
实现的3种方法
- setjmp/longjmp
- ucontext
- 汇编的代码
-
切换流程
- save,保存寄存器
- load,加载寄存器
-
switch(new->ctx, cur->ctx);
#if defined(__x86_64__) __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" " movq %rbp, 8(%rsi) # save frame_pointer \n" " movq (%rsp), %rax # save insn_pointer \n" " movq %rax, 16(%rsi) \n" " 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" " movq 48(%rdi), %r14 \n" " 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" " movq %rax, (%rsp) \n" " ret \n" ); #endif
协程结构体定义
pthread_create(threadid, NULL, func, arg);
// 注意NULL,是线程的属性,用于设置栈的大小
pthread_join(threadi, func_retval);
struct cpu_register_set {
void *eax;
void *ebx;
...
};
// 其中**last表示
#define queue(name,type)\
struct name { \
struct type *first; \
struct type **last; \
}
#define rbtree_node(name, type)\
struct name {\
char color;\
struct type *right;\
struct type *left;\
struct type *parent;\
}
struct coroutine {
struct cpu_register_set *set; // 保存CPU寄存器组
void *func; // 协程的入口函数
void *arg; // 入口函数的参数
void *retval; // 入口函数的返回值
// 栈存储大括号中,当协程一开始执行的时候,就将stack执行栈指针
// 全局栈,共享栈,所有的协程共用一个栈,不推荐
// 独立栈,每个协程分配独立的固定的空间
// 栈:用于维护函数调用的上下文,包括函数的参数,局部变量等等
// 栈帧是从栈上分配的一段内存,每次函数调用时,用于存储自动变量。
// 从物理介质角度看,栈帧是位于esp(栈指针)及ebp(基指针)之间的一块区域。
// 局部变量等分配均在栈帧上分配,函数结束自动释放。
void *stack;
size_t stack_size;
// 新建,就绪:队列
// 等待:红黑树
// 睡眠:红黑树-->key
queue(struct coroutine) *ready; // 新建、就绪
rbtree(struct coroutine) *wait; // 等待
rbtree(struct coroutine) *sleep; // 睡眠optional
//
}
coroutine_create(entry_cb, arg);
coroutine_exec(co) {
co->value = co->func(co->arg);
}
// 等待协程入口函数返回并获得返回值,这个函数就和pthread_join差不读
int coroutine_join(coid, &ret) {
// 阻塞
co = search(coid)
if(co->ret == NULL) {
co_wait();
}
ret = co->retval;
return 0;
}
struct scheduler {
struct scheduler_pos *pos;
struct coroutine* cur;
int epfd;
queue_node *read_set;
rbtree() *wait_set;
rbtree() *sleep_set;
}
// 协程的调度策略
struct scheduler_pos {
struct scheduler_ops *next;
enset();
deset();
}
typedef struct _nty_coroutine {
//private
nty_cpu_ctx ctx;
proc_coroutine func;
void *arg;
void *data;
size_t stack_size;
size_t last_stack_size;
nty_coroutine_status status;
nty_schedule *sched;
uint64_t birth;
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;
调度的策略
调度器如何定义
协程api的实现,hook,将同步io该为异步
socket
bind
listen
accept
send
recv
close
connect
king_accept() {
int ret = poll(fd);//判断fd是否就绪
if(ret >0) {
accept();
} else {
epoll_ctl(epfd);
yield();
}
}
多核测试
- 多线程,CPU亲缘性,需要锁
- 多进程,
- CPU指令()
如何测试
同步的编程方式,异步的性能,但是不超过异步的性能
100K连接的测试
coroutine:2300-2400ms
reactor:2200ms
其他问题
堆栈是属于进程的,而不是当个函数或者线程
协程会不会造成实时性不足?协程的切换同epoll_wait的切换差不读概念,不会出现实时性的问题
如果eventlist[1024*1024]改为eventlist[1024],但一次触发1500就绪事件的时候,有没有什么方法能够一次性处理1500个?