IO多路复用
1.前言
如你所知,高性能web服务器Nginx选择使用IO多路复用技术处理客户端请求,高性能内存数据库Redis同样也选择使用IO多路复用技术处理客户端请求,这足以说明IO多路复用是非常优秀的IO模型,毕竟人以类聚、物以群分。
接下来会从传统阻塞IO模型说起,聊到传统非阻塞IO模型,再到IO多路复用
2. IO模型
2.1 传统阻塞IO模型

传统阻塞IO模型中,当客户端与服务器端建立连接创建socket后,服务端需要从socket中读取数据,如果socket数据不可读,就会阻塞当前线程,又由于是单线程,就会导致服务端无法对外提供任何服务
2.2 传统非阻塞IO模型
通过对传统阻塞IO模型进行分析后,可得知问题的关键在单线程上,知道问题所在,对应解决方案也就有了,那就是针对每个client创建一个线程进行socket数据读取

通过多线程解决了单线程阻塞导致服务不可用的问题,随之而来也带了一个新的问题,那就是线程过多会造成资源浪费(每个线程拥有1M栈空间)、上下文切换问题
2.3 IO多路复用
单线程和多线程都有对应的问题存在,那么还有其它更好的解决方案嘛?方案当然是有的,那就是IO多路复用。IO多路复用的实现有select()、poll()、epoll(),分别来看一下
2.3.1 术语介绍
文件描述符:客户端与服务端建立的socket连接称作一个文件描述符(File Descriptor 以下简称 FD)
2.3.2 select()
2.3.2.1 描述

通过select()描述可以得知该函数允许应用程序同时监听多个fd,在fd可读可写之前会一直阻塞,同时还有一个很关键的点,那就是select()同时监听的文件描述符数量不能大于1024
2.3.2.2 返回值

select()调用成功后会返回就绪fd个数
2.3.2.3 流程
应用程序调用select()函数将fd传给内核空间

内核空间会对fd进行循环遍历,当有fd变得就绪后,应用程序的select()调用会返回就绪fd个数,此时应用程序再通过循环遍历方式读取就绪fd数据
2.3.2.4 存在的问题
内核不知道fd何时就绪,只能通过循环遍历的方式得知,会造成CPU资源浪费内核只会返回就绪fd的个数,应用程序并不知道具体哪个fd是就绪状态,只能再次循环系统调用才可得知,会造成无效系统调用- 同时监听
文件描述符数不能超过1024
2.3.2 epoll
2.3.3.1 描述

epollAPI核心概念就是epoll instance,epoll instance是一个内核数据结构。从用户空间角度来看,epoll instance是一个包含进程注册的fd列表和就绪fd列表的容器
epoll提供了3个系统调用用于创建和管理epoll实例
epoll_create:创建一个epoll实例,并返回一个fdepoll_ctl:对fd进行增、删、改epoll_wait:阻塞等待IO事件
2.3.3.2 流程

先通过epoll_create创建一个epoll instance,再通过epoll_ctl往注册列表中添加fd并监听对应事件(比如读事件、写事件),最后通过epoll_wait阻塞等待,直到就绪列表中有fd为止,期间如果某个fd就绪,会从注册列表中移动到就绪列表中,epoll_wait返回就绪fd个数
通过流程可以看到:
- 应用程序每次都是增量往注册列表中添加
fd,而不像select那样每次都传所有fd
- 内核空间通过事件驱动方式得知
fd就绪,而不像select那样需要循环遍历 epoll_wait返回的后,应用程序知道具体哪一个fd就绪,而不像select那样循环遍历所有fd才知道哪些处于就绪状态
2.3.3.3 示例分析

3. 查看redis的IO多路复用实现
3.1 追踪redis
strace -ff -o ./redis.out redis-6.2.6/src/redis-server /opt/redis/redis-6.2.6/redis.conf
3.2 查看追踪文件

vi redis.out.5444

可以看到redis通过epoll实现了IO多路复用
转载自:https://juejin.cn/post/7091951675384004621