文章目录
一、异步大概步骤(不是协程)
流程:commit->yield->callback->resume->继续往下走
yield等于switch(a,b)//a切换到b
resume等于switch(b,a)//从b切换到a
1)commit
①创建socket
②connect服务器
③准备好协议
④send
⑤fd加入到epoll
⑥yield让出CPU资源
int dns_async_client_commit(struct async_context* ctx, const char *domain, async_result_cb cb) {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("create socket failed\n");
exit(-1);
}
printf("url:%s\n", domain);
set_block(sockfd, 0); //nonblock
struct sockaddr_in dest;
bzero(&dest, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_port = htons(53);
dest.sin_addr.s_addr = inet_addr(DNS_SVR);
int ret = connect(sockfd, (struct sockaddr*)&dest, sizeof(dest));
//printf("connect :%d\n", ret);
struct dns_header header = {0};
dns_create_header(&header);
struct dns_question question = {0};
dns_create_question(&question, domain);
char request[1024] = {0};
int req_len = dns_build_request(&header, &question, request);
int slen = sendto(sockfd, request, req_len, 0, (struct sockaddr*)&dest, sizeof(struct sockaddr));
struct ep_arg *eparg = (struct ep_arg*)calloc(1, sizeof(struct ep_arg));
if (eparg == NULL) return -1;
eparg->sockfd = sockfd;
eparg->cb = cb;
struct epoll_event ev;
ev.data.ptr = eparg;
ev.events = EPOLLIN;
ret = epoll_ctl(ctx->epfd, EPOLL_CTL_ADD, sockfd, &ev);
//printf(" epoll_ctl ADD: sockfd->%d, ret:%d\n", sockfd, ret);
return ret;
}
2)callback(起的子线程去做)
①epoll_wait()
②read fd
③解析对应的数据
④resume()堆栈恢复
//dns_async_client_proc()
//epoll_wait
//result callback
static void* dns_async_client_proc(void *arg) {
struct async_context *ctx = (struct async_context*)arg;
int epfd = ctx->epfd;
while (1) {
struct epoll_event events[ASYNC_CLIENT_NUM] = {0};
int nready = epoll_wait(epfd, events, ASYNC_CLIENT_NUM, -1);
if (nready < 0) {
if (errno == EINTR || errno == EAGAIN) {
continue;
} else {
break;
}
} else if (nready == 0) {
continue;
}
printf("nready:%d\n", nready);
int i = 0;
for (i = 0;i < nready;i ++) {
struct ep_arg *data = (struct ep_arg*)events[i].data.ptr;
int sockfd = data->sockfd;
char buffer[1024] = {0};
struct sockaddr_in addr;
size_t addr_len = sizeof(struct sockaddr_in);
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&addr, (socklen_t*)&addr_len);
struct dns_item *domain_list = NULL;
int count = dns_parse_response(buffer, &domain_list);
data->cb(domain_list, count); //call cb
int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
//printf("epoll_ctl DEL --> sockfd:%d\n", sockfd);
close(sockfd); /
dns_async_client_free_domains(domain_list, count);
free(data);
}
}
}
二、做法
1)longjmp/setjmp讲解和使用提示
- 思路:
在yield时候setjmp一个点,read fd触发后使用longjmp跳回到setjmp的地方继续往下走
- 函数说明:
#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
- 函数功能:
1)setjmp 函数的功能是将函数在此处的上下文保存在 jmp_buf 结构体中,以供 longjmp 从此结构体中恢复。
2)longjmp 函数的功能是从 jmp_buf 结构体中恢复由 setjmp 函数保存的上下文,该函数不返回,而是从 setjmp 函数中返回
- 参数说明
(1)setjmp
1)参数 env 即为保存上下文的 jmp_buf 结构体变量;
2)如果直接调用setjmp函数,返回值为 0; 若该函数从 longjmp 调用返回,返回值为非零,由 longjmp 函数提供。根据函数的返回值,我们就可以知道 setjmp 函数调用是第一次直接调用,还是由其它地方跳转过来的
(2)longjmp
1)参数 env 是由 setjmp 函数保存过的上下文。
2)参数 val 表示从 longjmp 函数传递给 setjmp 函数的返回值,如果 val 值为0, setjmp 将会返回1,否则返回 val。
3)longjmp 不直接返回,而是从 setjmp 函数中返回,longjmp 执行完之后,程序就像刚从 setjmp 函数返回一样
- 代码说明
#include <setjmp.h>
int main()
{
jmp_buf env;
int i;
i = setjmp(env);
printf("i = %d\n",i);
if(i!=0)
exit(0);
longjmp(env,2);
printf("This line does not get printed\n");
}
运行该程序得到的结果为:
i = 0;
i = 2
2)ucontext实现(待补充)
3)汇编实现(后面详细讲)
三、有栈协程与无栈协程的区别
1)有栈
- 特点
1)每一个协程有独立的栈
2)管理比无栈的简单,内存利用率低
3)比无栈性能更高
4)容易实现
2)无栈(可以用于文件传输等存文件大的io)
- 特点:
1)共享栈
2)管理比有栈的更加复杂,内存利用率更高
3)比有栈效率低
四、交换栈帧switch
int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);
#ifdef __i386__ ///* 32 bit x86 detected */
__asm__ (
" .text \n"
" .p2align 2,,3 \n"
".globl _switch \n"
"_switch: \n"
"__switch: \n"
"movl 8(%esp), %edx # fs->%edx \n"
"movl %esp, 0(%edx) # save esp \n"
"movl %ebp, 4(%edx) # save ebp \n"
"movl (%esp), %eax # save eip \n"
"movl %eax, 8(%edx) \n"
"movl %ebx, 12(%edx) # save ebx,esi,edi \n"
"movl %esi, 16(%edx) \n"
"movl %edi, 20(%edx) \n"
"movl 4(%esp), %edx # ts->%edx \n"
"movl 20(%edx), %edi # restore ebx,esi,edi \n"
"movl 16(%edx), %esi \n"
"movl 12(%edx), %ebx \n"
"movl 0(%edx), %esp # restore esp \n"
"movl 4(%edx), %ebp # restore ebp \n"
"movl 8(%edx), %eax # restore eip \n"
"movl %eax, (%esp) \n"
"ret \n"
);
#elif defined(__x86_64__) ///* 64 bit detected */
__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"
表示将rsp寄存器的数据放到esp这个位置
" movq %rbp, 8(%rsi) # save frame_pointer \n"
偏移8个字节,也就是void*的大小,将rbp放到ebp哪里
" movq (%rsp), %rax # save insn_pointer \n"
" movq %rax, 16(%rsi) \n"
将rax放到第三个位置,偏移16个字节
" movq %rbx, 24(%rsi) # save rbx,r12-r15 \n"
将rbx放到第四个位置,偏移24个字节
" movq %r12, 32(%rsi) \n"
" movq %r13, 40(%rsi) \n"
" movq %r14, 48(%rsi) \n"
" movq %r15, 56(%rsi) \n"
//以上就是偏移结束了
//下面就是加载过来,resume的过程
" 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"
//ret返回
);
#endif
补充(后续再加):
1)rsp是寄存器本身的名字
五、协程怎么启动?
- 大体流程
1)创建协程
2)协程加入就绪队列
3)协程运行的时候调用入口函数 - ntyco怎么做的
1)将eip这个指针指向入口函数,
2)把参数放到对应位置,
3)esp这个栈指针的首地址也要保存起来
- 代码
static void nty_coroutine_init(nty_coroutine *co) {
void **stack = (void **)(co->stack + co->stack_size);//stack指向栈底
stack[-3] = NULL;
stack[-2] = (void *)co;
co->ctx.esp = (void*)stack - (4 * sizeof(void*));
//esp:寄存器存放当前线程的栈顶指针
co->ctx.ebp = (void*)stack - (3 * sizeof(void*));
//ebp:寄存器存放当前线程的栈底指针
co->ctx.eip = (void*)_exec;
//eip:寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取吓一条指令的内存地址,然后继续执行
co->status = BIT(NTY_COROUTINE_STATUS_READY);
}
//exec是我们自己定义的,这里协程的入口函数是func,不是exec
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)
#if 1
nty_coroutine_yield(co);
#else
co->ops = 0;
_switch(&co->sched->ctx, &co->ctx);
#endif
}
六、协程定义(每个协程都有可能是6、7、8状态)
1)上下文context,用来切换用,存cpu寄存器的值
2)stack栈,在协程调用压栈用
3)size栈的大小
4)func协程的入口函数
5)arg调用协程的参数
6)wait等待的状态(集合中的元素),红黑树
7)sleep睡眠的状态(集合中的元素),红黑树
8)ready就绪的状态(集合中的元素),队列
9)exit的状态
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; //sleep,红黑树
nty_coroutine_rbtree_wait waiting; //wait 红黑树
//private
} nty_schedule;
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;
//sleep的红黑树节点
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;
八、ntyco协程API介绍
1、协程创建
- 函数
int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func,void *arg)
- 参数介绍
1)参数 1:nty_coroutine **new_co,需要传入空的协程的对象,这个对象是由内部
创建的,并且在函数返回的时候,会返回一个内部创建的协程对象
2)参数2:proc_coroutine func,协程的子过程。当协程被调度的时候,就会执行该函数。
3)参数 3:void *arg,需要传入到新协程中的参数
- 备注
协程不存在亲属关系,都是一致的调度关系,接受调度器的调度。调用 create API
就会创建一个新协程,新协程就会加入到调度器的就绪队列中
2、协程调度器的运行
void nty_schedule_run(void)
3、POSIX 异步封装 API
int nty_socket(int domain, int type, int protocol)
int nty_accept(int fd, struct sockaddr *addr, socklen_t *len)
int nty_recv(int fd, void *buf, int length)
int nty_send(int fd, const void *buf, int length)
int nty_close(int fd)
九、协程如何对多核支持
1)多进程
比较好做,不用改变协程本身,ntyco是基于多进程去做的
2)多线程
要在调度器里面加锁,每取出一个节点加锁,队列就使用自旋锁,红黑树就用互斥锁
3)x86指令
不懂怎么做,不了解
十、协程调度器运行作用举例(先略,太困了)
十一、自定义的协程接口
1、协程的创建
coroutine_create
2.提供调度器接口
scheduler_loop()
3.sleep()
4.所有的io操作需要重新封装一次(下面没有成功就不返回)
accept
connect
send
write
recv
read
sendto
recvfrom
//不需要封装
socket
close
fcntl
setsockopt
getsockopt
listen