目录
一、NIO Epoll事件轮询模型
对于 NIO 的 Selector,其非常强大,我们追踪一个open()方法的源码:
// 创建 selector,管理多个 channel
Selector selector = Selector.open();
// 进入.open()
public static ServerSocketChannel open() throws IOException {
return SelectorProvider.provider().openServerSocketChannel();
}
// 进入 .provider()
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
if (loadProviderFromProperty())
return provider;
if (loadProviderAsService())
return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
// 进入 .create()
public static SelectorProvider create() {
// Provider基于操作系统版本,我是mac这里是KQueueSelectorProvider
// 如果是 Linux 则可能是 EpollSelectorProvider
// 如果是 windows 则可能是 WindowsSelectorProvider
return new KQueueSelectorProvider();
}
Epoll就是Linux版本下的多路复用实现,即 EpollSelectorProvider 就是关键所在。
EpollSelectorProvider 的实现本地调用的是本地方法,即Linux操作系统层面函数。
- Selector.open() --> SelectorProvider.provider() -->
- 内部实现类,通过 new EPollSelectorImpl(this) 方法 --> new EpollArrayWrapper() --> epfs=epollCreate() 调用本地方法 -->
- 调用 linux 系统函数 epfd=epoll_create(256) 创建了一个 epoll,selector就是对它的封装,内部有一个存储 channel 的数组
- socketChannel.register(Selector sel, int ops, Object att) --> SelctorImpl.register -->
- 底层就是调用 EpollSelectorImpl.implRegister --> 将fd(file descriptot文件描述符,就是 scoketChannel)添加到内部的集合里 pollWrapper.add(fd)
- selector.selector() --> SelectorImpl.select --> EpollSelectorImpl.doSelect -->
- 最终也是调用 pollWrapper.poll(timeout) --> 真正去注册绑定事件 updateRegistrations() -->
- 调用native方法epollWait()等待epfd(就是epoll实例)上的事件 --> 底层是linux内核函数 epoll_wait
- 当socket收到数据后,操作系统的中断程序调用回调函数会给epoll实例的事件就绪列表list里添加该socket引用(这块是操作系统实现的),当程序执行到epoll_wait时,如果rdlist已经引用了socket,那么epoll_wait直接返回,如果rdlist为空,阻塞进程
- 中断是系统用来响应硬件设备请求的一种机制,操作系统收到硬件的中断请求,会打断正在执行的进程,然后调用内核中的中断处理程序来响应请求
- 调用native方法epollCtl()进行事件绑定 --> 底层是linux内核函数 epoll_ctl
Epoll的事件轮询就是基于这三个底层函数来实现的(epoll_create、 epoll_wait、 epoll_ctl)
- int epoll_create():创建一个 epoll 实例,并返回一个非负数作为文件描述符,用于对 epoll 接口的所有后续调用
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):使用文件描述符epfd引用的epoll实例,对目标文件描述符fd执行op操作。参数epfd表示epoll对应的文件描述符,参数fd表示socket对应的文件描述符。
- 监听selecotor的注册事件列表,他会注册channel到该列表,也会将有事件发生的channel转移到就绪事件列表rdlist
- 参数op由以下几个值
- EPOLL_CTL_ADD:注册新的fd到epfd中,并关联事件event
- EPOLL_CTL_DEL:从epfd中移除fd,并且忽略掉绑定的event,这时event可以为null
- EPOLL_CTL_MOD:修改已经注册的fd的监听事件
- 参数event是一个结构体
struct epoll_event{ _unit32_t events; epoll_data_t data; }
- events有很多可选值,这里列举几个常见的:
- EPOLLIN:表示对应的文件描述符是可读的;
- EPOLLERR:表示对应的文件描述符发生了错误;
- EPOLLOUT:表示对应的文件描述符是可写的;
- 成功返回0,失败返回-1
- int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout):等待文件描述符epfd上的事件。epfd是Epoll对应的文件描述符,events表示调用者所有可能事件的集合,maxevents表示最多等到多少个事件就返回,timeout是超时时间。
- 监听selector的就绪事件列表(rdlist)它有事件的话就会来处理,没有就阻塞
IO多路复用底层主要用的Linux内核函数(select,poll,epoll)来实现,windows不支持epoll实现,windows底层基于winsock2的select函数实现的(不开源)。
select | poll | epoll(jdk1.5及以上) |
遍历 | 遍历 | 回调 |
数组 | 链表 | 哈希表 |
每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当有IO事件就绪,系统注册的回调函数就会被调用,时间复杂度O(1) |
有上限 | 无上限 | 无上限 |
二、Redis线程模型
简单说一下redis的线程模型,在redis源码的src文件夹下有一个文件:ae_epoll.c 说一下里面的一些核心接口
- aeApiCreate(aeEventLoop *eventLoop):redis启动时回调该方法
- 调用函数 epoll_create(1024) 创建一个epoll
- aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask):
- 调用 epoll_ctl()
- aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp):
- 调用 epoll_wait()
Linux版本的Redis底层的通信就是基于epoll的事件轮询机制实现的。
三、Netty线程模型
核心就是在基础NIO多路复用的基础上做了大量的封装和优化。