IO多路复用

1,063 阅读4分钟

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实例,并返回一个fd
  • epoll_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多路复用