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 描述
epoll
API核心概念就是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多路复用
转载自:https://juejin.cn/post/7091951675384004621