2.3.1 协程设计原理与汇编实现

一、为什么会要协程

对于服务器处理百万计的 IO。Handle(sockfd)实现方式有两种。

1.IO同步操作

int handle(int sockfd) {
 recv(sockfd, rbuffer, length, 0);
 
 parser_proto(rbuffer, length);
 send(sockfd, sbuffer, length, 0);
 
}

handle 的 io 操作(send,recv)与 epoll_wait 是在同一个处理流程里面的。这就是 IO 同步操作。
优点:
1.sockfd 管理方便。
2.操作逻辑清晰。
缺点:
1.服务器程序依赖 epoll_wait 的循环响应速度慢。
2.程序性能差

2.IO异步操作

handle(sockfd)函数内部将 sockfd 的操作,push 到线程池中,代码如下:

int thread_cb(int sockfd) {
 // 此函数是在线程池创建的线程中运行。
 // 与 handle 不在一个线程上下文中运行
 recv(sockfd, rbuffer, length, 0);
 parser_proto(rbuffer, length);
 send(sockfd, sbuffer, length, 0);
}
int handle(int sockfd) {
 //此函数在主线程 main_thread 中运行
 //在此处之前,确保线程池已经启动。
 push_thread(sockfd, thread_cb); //将 sockfd 放到其他线程中运行。
}

Handle 函数是将 sockfd 处理方式放到另一个已经其他的线程中运行,如此做法,将 io 操作(recv,send)与 epoll_wait 不在一个处理流程里面,使得 io操作(recv,send)与 epoll_wait 实现解耦。这就叫做 IO 异步操作。
优点:
1.子模块好规划。
2.程序性能高。
缺点:
模块之间的 sockfd 的管理异常麻烦

3.IO 异步操作与 IO 同步操作的对比

1.sockfd 管理: 同步方便 ;异步由多个线程共同管理
2.代码逻辑: 同步整体清晰;异步的子模块逻辑清晰
3.程序性能:同步相应慢,性能差;异步响应快,性能好

4.协程的来源

有没有一种方式,有异步性能,同步的代码逻辑,方便编程人员对 IO 操作的组件?
采用一种轻量级的协程来实现。在每次 send 或者 recv 之前进行切换,再由调度器来处理 epoll_wait 的流程。
NtyCo采用了基于这样的思考,实现了一个 IO 异步操作与协程结合的组件。

二、异步的运行流程

1.异步操作

通过检测io是否就绪,如果没有则加入epoll,切换到epoll_wait,如果epoll_wait返回大于0向下继续操作,返回小于0继续io操作。
异步的运行流程

2.在 send 与 recv 调用的时候,如何实现异步操作的?

while (1) {
 int nready = epoll_wait(epfd, events, EVENT_SIZE, -1);
 for (i = 0;i < nready;i ++) {
 int sockfd = events[i].data.fd;
 if (sockfd == listenfd) {
 int connfd = accept(listenfd, xxx, xxxx);
 
 setnonblock(connfd);
 ev.events = EPOLLIN | EPOLLET;
 ev.data.fd = connfd;
 epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
 } else {
 
 epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
 recv(sockfd, buffer, length, 0);
 //parser_proto(buffer, length);
 send(sockfd, buffer, length, 0);
 epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, NULL);
 }
 }
}

在进行 IO 操作(recv,send)之前,先执行了 epoll_ctl 的 del 操作,将相应的 sockfd 从 epfd中删除掉,在执行完 IO 操作(recv,send)再进行 epoll_ctl 的 add 的动作。这段代码看起来似乎好像没有什么作用。如果是在多个上下文中,这样的做法就很有意义了。能够保证 sockfd 只在一个上下文中能够操作 IO 的。不会出现在多个上下文同时对一个 IO 进行操作的。协程的 IO 异步操作正式是采用此模式进行的。

三、协程的原语操作

1.什么是原语操作

原语操作就是最基本的操作
在这里插入图片描述
1.commit完后,switch --> epoll_wait 是 yield (让出操作)
2.epoll_wait --> io处理流程 是 resume (恢复操作)
yield 与 resume 是一个switch操作(三种实现方式):
1.longjump/setjump
2.ucontext
3.汇编实现

2.协程的上下文切换

x86_64 的寄存器有 16 个 64 位寄存器,分别是:
%rax, %rbx, %rcx, %esi, %edi, %rbp, %rsp, %r8, %r9, %r10, %r11, %r12,
%r13, %r14, %r15。
%rax 作为函数返回值使用的。
%rsp 栈指针寄存器,指向栈顶
%rdi, %rsi, %rdx, %rcx, %r8, %r9 用作函数参数,依次对应第 1 参数,第 2 参数。。。
%rbx, %rbp, %r12, %r13, %r14, %r15 用作数据存储,遵循调用者使用规则,换句话
说,就是随便用。调用子函数之前要备份它,以防它被修改
%r10, %r11 用作数据存储,就是使用前要先保存原值。

上下文切换,就是将 CPU 的寄存器暂时保存,再将即将运行的协程的上下文寄存器,分别
mov 到相对应的寄存器上。此时上下文完成切换。
在这里插入图片描述

3.切换函数_switch定义

int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);
//参数 1:即将运行协程的上下文,寄存器列表
//参数 2:正在运行协程的上下文,寄存器列表

nty_cpu_ctx 结构体的定义,为了兼容 x86,结构体项命令采用的是 x86 的寄存器名
字命名。

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 返回后,执行即将运行协程的上下文。是实现上下文的切换

4.yield,resume原语操作

yield resume都是基于switch实现的
yield switch(ep,next);
resume switch(next,ep);
yield resume是一个可逆过程

void nty_coroutine_yield(nty_coroutine *co)
// 参数:当前运行的协程实例
// 调用后该函数不会立即返回,而是切换到最近执行 resume 的上下文。该函
// 数返回是在执行 resume 的时候,会有调度器统一选择 resume 的,然后再
// 次调用 yield 的。resume 与 yield 是两个可逆过程的原子操作。

5.其他原语操作

create:创建一个协程。

  1. 调度器是否存在,不存在也创建。调度器作为全局的单例。将调度
    器的实例存储在线程的私有空间 pthread_setspecific。
  2. 分配一个 coroutine 的内存空间,分别设置 coroutine 的数据
    项,栈空间,栈大小,初始状态,创建时间,子过程回调函数,子
    过程的调用参数。
  3. 将新分配协程添加到就绪队列 ready_queue 中
int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, 
void *arg) {
assert(pthread_once(&sched_key_once, 
nty_coroutine_sched_key_creator) == 0);
nty_schedule *sched = nty_coroutine_get_sched();
if (sched == NULL) {
nty_schedule_create(0);
sched = nty_coroutine_get_sched();
if (sched == NULL) {
printf("Failed to create scheduler\n");
return -1;
}
}
nty_coroutine *co = calloc(1, sizeof(nty_coroutine));
if (co == NULL) {
printf("Failed to allocate memory for new coroutine\n");
return -2;
}
//
int ret = posix_memalign(&co->stack, getpagesize(), 
sched->stack_size);
if (ret) {
printf("Failed to allocate stack for new coroutine\n");
free(co);
return -3;
}
co->sched = sched;
co->stack_size = sched->stack_size;
co->status = BIT(NTY_COROUTINE_STATUS_NEW); //
co->id = sched->spawned_coroutines ++;
co->func = func;
co->fd = -1;
co->events = 0;
co->arg = arg;
co->birth = nty_coroutine_usec_now();
*new_co = co;
TAILQ_INSERT_TAIL(&co->sched->ready, co, ready_next);
return 0;
}

四、协程的定义

  1. 运行体 R:包含运行状态{就绪,睡眠,等待},运行体回调函数,回调参数,栈指针,栈大小,当前运行体
  2. 调度器 S:包含执行集合{就绪,睡眠,等待}
  3. 运行体如何高效地在多种状态集合更换
    在这里插入图片描述
typedef struct _nty_coroutine {
	nty_cpu_ctx ctx;
	proc_coroutine func;
	void *arg;
	size_t stack_size;
	nty_coroutine_status status;
	nty_schedule *sched;
	uint64_t birth;
	uint64_t id;
	void *stack;
	RB_ENTRY(_nty_coroutine) sleep_node;
	RB_ENTRY(_nty_coroutine) wait_node;
	TAILQ_ENTRY(_nty_coroutine) ready_next;
	TAILQ_ENTRY(_nty_coroutine) defer_next;
} nty_coroutine;

五、调度器的定义

调度器是管理所有协程运行的组件,协程与调度器的运行关系。
在这里插入图片描述
调度器的属性,需要有保存 CPU 的寄存器上下文 ctx,可以从协程运行状态yield 到调度器运行的。从协程到调度器用 yield,从调度器到协程用resume

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;
	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;
	nty_coroutine_queue ready;
	nty_coroutine_rbtree_sleep sleeping;
	nty_coroutine_rbtree_wait waiting;
} nty_schedule;

六、调度的策略

调度器的实现,有两种方案,一种是生产者消费者模式,另一种多状态运
行。
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值