文章目录
IO多路复用
所谓的IO多路复用就是实现一个线程能够监听多个网络连接的机制。一旦监听到其中一个或者多个网络连接发起了读写请求,那么该线程就会通知事件处理器进行IO操作。
同步阻塞IO
当客户端请求服务端的时候,服务端采用单线程,当accpet到一个请求后,在send或者receive的时候阻塞,这个时候无法accpet其他请求,必须等到上一个send或者receive完成后才能继续accpet其他请求,与此同时客户端一直处于等待服务端响应的状态,无法同时处理其他事情,直到服务端响应后才能继续往下执行。
同步非阻塞
同步非阻塞IO是在同步阻塞IO的基础上,将socket设置为NONBLOCK
。服务端的用户线程在发起IO请求后可以立即返回。由于Socket是非阻塞方式的,因此服务端用户线程发起IO操作的时候会立即返回,此时如果未读取到任何数据,服务端的用户线程会不断的发情IO请求,直到读取到数据后才继续往下执行,与此同时客户端在收到服务端的响应时,处于等待状态,无法往下执行,只有收到服务端的响应时才会继续往下执行。
多路复用
多路复用技术有以下三种实现方式:
select
poll
epoll
select
在发生网络IO请求的时候,select只能仅仅知道发生了IO请求,但是它不知道是哪个网络连接发生的,所以它只能通过遍历所有的流,找到发生IO事件的流,然后进行后续的操作。所以处理的流越多,遍历的时间越长。
- 使用
copy_from_user
从用户空间拷贝fd_set到内核空间
注册回调函 __pollwait
遍历所有fd
,调用其对应的poll
方法(对于socket
,这个poll
方法是sock_poll
,sock_poll
根据情况会调用到tcp_poll
,udp_poll
或者datagram_poll
)- 以
tcp_poll
为例,其核心实现就是pollwait,也就是上面注册的回调函数。 __pollwait
的主要工作就是把current
(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep
(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current
便被唤醒了。- poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给
fd_set
赋值。 - 如果遍历完所有的
fd
,还没有返回一个可读写的mask
掩码,则会调用schedule_timeout
是调用select的进程(也就是current
)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select
的进程会重新被唤醒获得CPU
,进而重新遍历fd,判断有没有就绪的fd
。 - 把
fd_set
从内核空间拷贝到用户空间。
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
- 单个进程所打开的FD是有限制的,通过
FD_SETSIZE
设置,默认1024 ; - 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大;
需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
- 对 socket 扫描时是线性扫描,采用轮询的方法,效率较低(高并发)
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
poll
poll本质上和select
没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd
对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有缺点:
- 每次调用
poll
,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大; - 对
socket
扫描是线性扫描,采用轮询的方法,效率较低(高并发时)
epoll
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是**事件驱动(每个事件关联上fd)**的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1)
)
当某一进程调用epoll_create
方法时,Linux
内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll
的使用方式密切相关。
每一个epoll对象都有一个独立的eventpoll
结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn
,其中n为红黑树元素个数)。
而所有添加到epoll
中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback
,它会将发生的事件添加到rdlist双链表中。
在epoll
中,对于每一个事件,都会建立一个epitem结构体
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll
对象中的rdlist
双链表中是否有epitem元素即可。如果rdlist
不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。 讲解完了Epoll
的机理,我们便能很容易掌握epoll
的用法了。一句话描述就是:三步曲。
- 第一步:
epoll_create
()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。 - 第二步:
epoll_ctl
()系统调用。通过此调用向epoll
对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。 - 第三部:epol
l
_wait()系统调用。通过此调用收集收集在epoll
监控中已经发生的事件。
epoll的优点
- 没有最大并发连接的限制,能打开的
FD
的上限远大于1024(1G的内存上能监听约10
万个端口); - 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll;
- 内存拷贝,利用
mmap
()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
epoll
缺点
- epoll只能工作在
linux
下
select/poll/epoll之间的区别
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现
select | poll | epoll | |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
数据结构 | bitmap | 数组 | 红黑树 |
最大连接数 | 1024(x86)或 2048(x64) | 无上限 | 无上限 |
最大支持文件描述符数 | 一般有最大值限制 | 65535 | 65535 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝 |
工作模式 | LT | LT | 支持ET高效模式 |
工作效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1) |
epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。
支持一个进程所能打开的最大连接数
- select:单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32_32,同理64位机器上FD_SETSIZE为32_64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
- poll:poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。
- epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。
FD剧增后带来的IO效率问题
- select:因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
poll
:同上epoll
:因为epoll
内核中实现是根据每个fd
上的callback函数来实现的,只有活跃的socket
才会主动调用callback
,所以在活跃socket
较少的情况下,使用epoll
没有前面两者的线性下降的性能问题,但是所有socket
都很活跃的情况下,可能会有性能问题。
消息传递方式
select
:内核需要将消息传递到用户空间,都需要内核拷贝动作poll
:同上epoll
:epoll通过内核和用户空间共享一块内存来实现的。
总结
select,poll
实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll
其实也需要调用epoll_wait
不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select
和poll
在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU
时间。这就是回调机制带来的性能提升。
select
,poll
每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current
往设备等待队列中挂一次,而epoll
只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait
的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll
内部定义的等待队列)。这也能节省不少的开销。