对比几种不同的 IO
-
阻塞IO
应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。
特点:阻塞进程,CPU 利用率高
-
非阻塞IO
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的轮训(增加了系统调用)来获知 I/O 是否完成。
特点:不阻塞进程,轮训增加了 CPU 消耗 -
IO复用
单个进程可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时,把数据从内核复制到进程中。
特点:阻塞进程,无轮训 CPU -
异步 I/O
应用进程执行阻塞的调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
特点:不阻塞进程,无轮训 CPU
同步IO 与 异步IO
同步与异步体现在 内核态到用户态的数据拷贝 是否阻塞等待
IO复用
非异步IO,只是允许等待的 fd事件 变多了
- 同步IO:包含
阻塞IO
、非阻塞IO
、IO复用
- 异步IO:即
异步IO
需要注意IO复用本质是 同步IO
Epoll 与 Select
fd :文件描述符
Select
- 描述符有限,最大值 1024
- 查询 fd 均遍历数组,O(n) 复杂度
- 轮训就绪状态的 fd 均需要传入所有 fd
- 每次轮训传入fd 都会触发用户态到内核态的拷贝
Epoll
- 描述符无限制
- fd 存放结构为红黑树,就绪 fd 事件触发回调插入就绪链表,读写 fd 事件的性能 O(logN),查询就绪 fd 事件性能 O(1) (直接读取就绪链表)
- 通过就绪链表存放就绪状态的 fd 事件,查询时直接返回就绪链表
- 红黑树与就绪链表直接存放内核态中,避免了用户态与内核态间的大量拷贝 (epoll_wait 拷贝少量就绪 fd 句柄)
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
// epoll_create 创建 epoll,设定 fd 数量(内核开辟存储空间)
// epoll_ctl 管理 fd 事件的监控(红黑树的插入/删除)
// epoll_wait 返回相应就绪的事件的 fd(内核态向用户态拷贝就绪链表内的就绪事件)
epoll
LT 与 ET
- LT(水平触发):epoll_wait 不会删除就绪链表,每次 epoll_wait 均返回
- ET(边缘触发):epoll_wait 后会删除就绪链表
ET 下读写方式:
只要可读,就一直读,直到读完或失败
只要可写,就一直写,直到数据发送完或失败
【经典问题】
1. LT 下 ‘可写就绪’ 一直触发,怎么解决
要写数据时,添加fd可写事件通过 epoll_crt 添加到epoll里,写完后移除。
2. TCP accept
接收到连接到达时, while(accept) 循环处理所有连接,避免连接丢失
Reactor
反应堆模式
也叫 Dispatcher 模式,即 I/O 多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个线程。
单 Reactor 单进程 / 线程
只适用于业务处理非常快速的场景,不适用计算机密集型的场景。需要 handler 尽量快速地执行处理,否则会影响吞吐。
- Reactor 对象的作用是监听和分发事件;
- Acceptor 对象的作用是获取连接;(连接建立的事件,则交由 Acceptor 对象进行处理)
- Handler 对象的作用是处理业务;(不是连接建立事件, 则交由当前对应的 Handler 来处理)
单 Reactor 多进程 / 线程
单 Reactor 多进程 / 线程事件处理由 worker 线程池负责,解决了单线程下 handler处理耗时导致吞吐降低的问题。
但还存在大并发量情况下 dispatch 速率影响吞吐的问题。
多(主从) Reactor 多进程 / 线程
多(主从) Reactor 多进程 / 线程Reactor 只负责 dispatch 事件,因此为了充分利用cpu,可以通过多 Reactor 并行 dispatch。
主从 Reactor,主负责建立连接(处理accept),子 Reactor 负责 dispatch 事件到 worker 线程池
网友评论