思路
先理解
- Select / poll /epoll ?
基于事件驱动来做的 io 复用
- Reactor ?
用epoll来封装的反应堆
- Http
网络http协议 协议头 如何实现内部 get cgi
- Webserver ?
做一个网络的服务端 基于reactor
- 单线程 多线程 多进程reactor?
- Posix api 和协议栈 tcp协议栈
- Udp 协议 kcp
在理解
- 为什么会有协程?解决什么问题?
同步的线程 异步的操作
- 原语操作有哪些?同步和异步的切换 操作中最基本的单位,
eg create
、yelid
、resume
。 - 切换 (Switch)
类似于线程 进程的切换
- 协程的运行流程
- 协程结构体定义
- 调度策略
类似于 进程调度 lua算法等一系列的策略 cfs
- 调度器如何定义?
- 协程 api 的实现,hook?
- 多核模式
- 如何测试?
注意: 没有特别声明fd
、sockfd
、连接
、io
指的都是 fd
!!!
为什么会有协程?解决什么问题?
- 理解什么是同步什么是异步?
- 理解什么是同步的编程方式异步的性能?同步的编程方式和异步的编程方式的优缺点是什么?
- 为什么异步的编程方式避免不了多个线程共用一个 fd?导致数据乱序?
- 适用于服务器与客户端异步处理。
IO 同步、异步操作对比:
站在服务器端
比如 reactor 监听时间。
同步 7.5s 连接1000个 。
异步 1.4s 连接1000个。
//同步 while(1) { epoll_wait For(;;) { Recv(); Send(); } } //异步 Push_other_thread() { Poll(); //检测 可读可写 治标不治本 不能根除 多个线程同用一个fd Recv(); Send(); } While(1) { Epoll_wait(); For(;;) { Push_other_thread(); } }
分析:
- Io 异步的问题 push_other_thread 避免不了多个线程公用一个fd的现象 尽量的去避免
- 问题?乱序读的数据不对 A再读 B 关闭。莫名其妙的就出现问题 ,如何解决?
线程前加poll再次检测但是治标不治本 。
站在客户端的角度
客户端发送之后等待服务端的结果,发送50个域名给客户端进行请求同步的没异步的处理快。
总结:不管是再客户端还是服务端 io异步的性能总是快。
通过站在客户端与服务器端推出:
协程就是解决了同步的性能,异步的共用现象使用同步的编程,却实现了异步的操作处理
如何把同步的编程变成异步的呢?
正常的流程如下:
- 建立连接
- 当客户端发送一个请求到服务端时候,
send()
; - Epoll_wait判断是否可读;
- 等待服务端一个回发数据
recv()
(不可读会阻塞)同步,再去做一系列的操作
改成如下:
- 建立连接
- 当客户端发送一个请求到服务端时候
send()
,send之后切换成epoll_wait
,if(不可读){ 开另外一个流程再次send }
,相当于异步操作 开了另外一个io口去做send ,epoll_wait
判断是否可读. - 等待服务端一个回发数据
recv()
(不可读会阻塞) 但是不影响上面,因为上面会切换另外一个请求去再次处理其他的send,再去做一系列的操作。 - 在send完毕后做一个切换到
epoll_wait
去判断fd
是否可读不可读,让出回到主线程,去另外一个流程中再次send
。
总结:
先理解io操作
,io检测
。核心:遇到 io 操作就 yeid
send()
一次 fd ----》 利用epoll_ctl()
加入epoll_wait()
中检测 ----》 yeid(让出)跳入到epoll_wait()检测 if(可读){ recv() , 继续epoll_wait()
检测} else { resume()}
切换(Switch)
yield
:IO 操作 ----》 IO 检测resume
:IO 检测 ----》 IO 操作
协程入口函数
server_reader -> nty_recv -> nty_poll_inner -> nty_schedule_sched_wait -> _nty_coroutine_yield
协程结构体定义
举例:以客户端与服务器连接时,客户端流程
void client_func() { send(fd, buffer, length, 0); int nready = epoll_wait(epfd, ); //加一个判断把同步的流程改成异步; recv(fd, rbuffer, length, 0); //阻塞住 不要的 //.... //parser()解析服务端返回的数据 }
协程结构体定义
//用宏写 C++ 模板 #define queue_node(name type) struct name{\ struct type *next; struct type *prev; } #define rbtree_node(name type) struct name{\ char color; struct type *right; struct type *left struct parents; } struct cpi_register_set { void* eax; //寄存器上下文 void* ebx; //寄存器上下文 .... }; struct coroutine { // 类比线程 来定义协程需要的结构 struct cpi_register_set *set; //保存cpu void *coroutine e_create;// entry 协程入口函数 void *arg; // 参数 void *reval; //返回值 不做计算的话没什么意义 // 函数调用如何调用? 利用栈 void *stack_addr; //指向栈空间的首地址,在堆上开辟一个空间当做栈 size_t stack_size; // 协程的数量很多 要用什么结构来储存 // 从调度器的角度来看 栈的格式不符合(会导致最底部的协程调用不到)stack_coroutine *next; // 队列的格式来用 queue_node(ready_name,coroutine) *ready; rbtree_node(wait_name,coroutine) *wait; rbtree_node(sleep_name,coroutine) *sleep; }co;
协程的创建类比线程的创建
pthread_create(thid, NULL, entry_cb, arg) { } //线程id, 线程属性(堆栈大小的参数),线程入口函数,参数 只做了两件事情: 1. 创建一个线程实体:task_struct *task; 2. 加入就绪队列 :enqueue_ready;
//协程创建 oroutine_create(entry_cb,arg); coroutine_join(coid,&reval) //获取子线程返回的值 { co = search(coid) if(co->reval ==NULL){ wait(); //单线程 用条件等待 } return co->reval; } exec(co){ //启动了协程函数 取得返回值 co->reval = co->func(co->arg) //存到了协程的结构体里 signal();// }
调度策略
调度器
//调度器 struct scheduler{ struct scheduler_ops *ops; //当前运行的协程 当调度器让出的时候、 struct coroutine *cur; //当前哪个协程 int epfd; // queue_node *ready_set; rbtree() *wait_set; rbtree() *sleep_set; }
协程 api 的实现,hook?
socket
、bind
、listen
、connect
、accept
、send
、recv
同步改为异步- 利用
hook
函数
//异步api void my_accept(){ int ret=Poll(fd); if(ret>0){ //就绪后 调用真正的accept accept(); } else{ //吧fd加入到epoll 后 “让出” epoll管理 epoll_ctl(epfd); //time out 值设成0 立刻返回 做成异步的操作 yield(); //调度器用 } }
hook 是把代码段中的改变 在进程开启阶段会调用inin_hook(),把函数内部 把系统调用对应的 指定函数 (send/recv)改成(send_f/recv_f)。
在应用下 用到了send(普通函数了)或者recv(普通函数了) 会自动的调用send_f/recv_f替换。
Yield之后如何执行 yield和resume如何实现的呢?让出到对应的resume下 resume到对应的yield下。
充电站
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习