多路复用IO
Java NIO使用的是IO多路复用模型,IO多路复用模型与传统IO模型的区别在于:一个线程可以检查多个fd,而传统IO模型中一个线程只能检查一个文件描述符fd。
IO多路复用模型用三种常见的实现方式:
- select
- poll
- epoll
select/poll
select API:
int select (int __nfds, fd_set *__restrict __readfds,
fd_set *__restrict __writefds,
fd_set *__restrict __exceptfds,
struct timeval *__restrict __timeout);
poll API:
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
select、poll实现原理类似:都是用单个函数实现的。
函数将需要检查的fd通过单个函数传递给内核,内核负责检查这些fd有没有产生新事件,如果没有,则函数陷入休眠,知道检查的fd有新的事件产生,函数才会返回,返回值是所有检查的fd,调用方需要自己遍历fd拿到产生新事件的具体fd。
通常在应用层代码实现上,需要循环调用这个函数,这会导致两个问题:
- 每次循环需要在用户空间和内核空间中间拷贝监听的fd
- 每次循环需要重新注册fd监听事件
另外还有一点:调用方拿到函数返回值后,需要遍历所有fd集合才能拿到目标fd。
上述三点是select和poll在监听fd集合过大,并且新事件较少时,效率低下的原因。
注意这里的效率低下的前置条件,如果fd集合不大,并且产生新事件的fd在fd集合中占比很高时,select和poll函数的效率并不比epoll低。//todo
epoll
使用epoll函数需要调用三个api:
//声明epoll fd
int epoll_create (int __size) __THROW;
//将要监听的fd注册给epoll fd
int epoll_ctl (int __epfd, int __op, int __fd,
struct epoll_event *__event) __THROW;
//命令epoll fd开始监听感兴趣的fd
int epoll_wait (int __epfd, struct epoll_event *__events,
int __maxevents, int __timeout);
使用epoll需要依次调用三个函数,最后循环调用epoll_wait函数实现服务器一直接受网络请求。因为api分成了三个,所以避免了select和poll函数的缺点,即不需要再频繁的在用户空间和内核监控之间拷贝监听的fd和重新注册fd监听。因为epoll_ctl只需要调用一次就够了。
至于epoll_wait的返回值,则只会返回产生了新事件的fd,不会将所有注册的fd都返回,所以调用方不会浪费时间遍历没有用的fd,这在产生新事件占比较低的情况下,效率提升很大。
那epoll是怎么实现只返回产生新事件fd这一功能的呢?
epoll使用了两个数据结构,红黑树+链表,红黑树用来存储所有感兴趣的fd,称为interest list,注册和减少感兴趣的fd只需要在红黑树上增加或减少响应节点;
链表用来存储产生新事件的fd,称为ready list,每当fd有新事件产生时,都会调用回调方法,回调方法会在链表上追加一个元素。
当epoll函数返回时,只需要将ready list直接返回即可。
参考
说明了select 和 epoll的回调机制实现原理
https://cloud.tencent.com/developer/article/1005481
https://blog.csdn.net/wangfeng2500/article/details/9127421
https://www.infoq.cn/article/26lpjzsp9echwgnic7lq#fromHistory

浙公网安备 33010602011771号