I/O多路复用的意思就是一个进程同时处理多个TCP连接。
- I/O:一般指网络I/O。
- 多路:多个客户端连接(连接就是套接字描述符,即socket或channel),指多个TCP连接。
- 复用:用一个进程来处理多条连接,使用单进程就能够实现同时处理多个客户端的连接。
在多路复用之前使用的是同步阻塞I/O模型,因为一个请求会阻塞进程/线程,所以要为每个请求分配一个进程/线程,一个进程/线程处理一个网络连接,进程/线程的上下文切换消耗很大,并且创建进程/线程也会有一定消耗,所以同步阻塞I/O模型性能很低。
Linux有三种实现I/O多路复用的机制:select -> poll -> epoll。
开始了解多路复用之前,先要了解操作系统的以下几个概念:
-
文件句柄
Linux中一切皆文件,所有内容都是以文件的形式来保存和管理的。文件句柄(File Handle)是操作系统中用于访问文件的一种数据结构,通常是一个整数或指针。文件句柄用于标识打开的文件,每个打开的文件都有一个唯一的文件句柄。
-
用户态和内核态
- 用户态(User Mode),运行用户程序。
- 内核态(Kernel Mode),运行操作系统程序,操作硬件。
在内核态下代码可以执行任何CPU指令以及引用任何内存地址,用户程序不能直接进入内核态,需要通过系统调用。
-
同步和异步
同步和异步是指访问数据的机制,同步一般指主动请求并等待I/O操作完成的方式。异步指主动请求数据后便可以继续处理其他任务,随后等待I/O操作完毕的通知。
-
阻塞和非阻塞
阻塞指的是进程调用接口后如果接口没有准备好数据,那么这个进程会被挂起什么也不能做,直到有数据返回时唤醒。非阻塞就是进程调用接口后如果接口没有准备好数据,进程也能处理后续的操作,但是需要不断地去轮询检查数据是否已经处理完成。
-
并行和并发
并行指的是多个事情在同一个时间点上同时发生了。并发指的是多个事情,在同一时间段内同时发生了。
select
接收到多个请求后,将对应的文件描述符数据存储到一个数组中,并用一个bitmap类型的rset数据存储需要被监听的文件描述符。
将rset整个复制到内核态中,在内核态中判断是否有数据到来,如果没有数据来,内核态会一直判断,此时用户程序是阻塞状态。
当有数据的时候,内核会将有数据的fd置位,表示有数据来了,然后select函数会返回,程序会继续执行,将有数据的文件标志符中的数据读出来,并进行相应的处理。
缺点:
- bitmap的大小是1024,即使这个值是可以设置的,也存在上限。
- fdset不可重用,每次都需要重新设置一下,因为rset的值在内核中被修改了。
- 用户态和内核态的切换仍然有一定的开销。
- rset被置位之后,仍然不知道是哪些被置位了,需要再去遍历一遍,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)。
poll
poll和select的方式一样,但是使用了pollfd,有数据的时候是pollfd.revents置位,(而不是像select一样修改整个rset),然后poll返回。
因为没有使用bitmap,所以没有文件描述符数量的限制,因为没有使用rset,直接修改了文件描述符数组中的某一项元素的revents,所以文件描述符数组是可以复用的。
poll解决了select存在的1、2两个问题,但是3、4两个问题依然没有解决。
epoll
epoll中用户态和内存态共享epfd的内存。不需要用户态和内核态的拷贝了。
有数据的时候,通过重排来置位,将有数据的fd放到最前面的位置,然后返回。epoll_wait是有返回值的,如果有3个fd触发了事件,就会返回3,之后就只需要遍历数组的前3个元素,对数组的前3个元素进行数据的读取和处理就行了,操作的时间复杂度是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。
epoll解决了select存在的问题1、2、3、4。
Redis和Nginx,以及Java的NIO底层就是用epoll来实现的。
Redis与IO多路复用
从Redis6开始,将网络数据读写,请求协议解析通过多个IO线程来处理。对于真正的命令执行,依然使用单线程操作。Redis中的命令执行都是单线程的,性能比较好的原因除了是基于内存操作外,还有一个更主要的原因就是使用了IO多路复用。
Redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,一次放到文件事件派发器,事件派发器将事件分发给事件处理器。
学习地址
RedisIO多路复用:www.bilibili.com/video/BV13R...