系列文章目录
NIO之bio、nio、多路复用器发展历程(一)
NIO之select、poll、epoll 内核触发模型对比(二)
NIO之epoll(三)
上一篇文章最后提及了多路复用器,今天稍微涉及select、poll,会重点讲解epoll
三种多路复用器及nio优缺点:
- NIO: 这个遍历的成本在用户态、内核态切换过程
- (多路复用器-1)select、poll: 这个遍历的过程触发了一次系统调用(用户态内核态的切换),同时把fds集合传递给了kernel(需要重复传递),kernel重新根据程序这次传过来的fds,进行遍历,再修改fd的io状态。
- (多路复用器-2)epoll: 通过epoll_create()、epoll_ctl()、epoll_wait() 实现,在内核内部用红黑树维护fds,避免了用户态和内核态来回切换,在内核内部遍历所有的fd。
多路复用器:select、poll 的弊端
- 1、每次都要重新,重复传递fds(内核开辟空间)
- 2、每次内核被调用了之后,针对这次调用,会触发一个fds的全量的遍历
当网卡来了数据后,操作系统会发生什么呢?
当网卡来了数据后,会发生中断,会从中断向量表找中断号对应的callback,cpu调用 callback 对应的程序指令。
中断–》回调callback
内核的多路复用器的升级
- 1、epoll 之前的callback:只是完成了将网卡发来的数据走内核协议栈(2-链路层、3-网络层、4-传输控制层)最终关联到FD的buffer。所以,application某一时间询问内核某一个或者某些FD有可R/W时,则返回有状态的fd。
ps: 如果内核在cb处理中再加入内核自己管理fds并单独存储有状态的fd,就不用再程序不断发起select,内核再遍历获取有状态fd了,epoll就是干了这件事。- 2、epoll:
- event事件–>回调
- 内核自己维护fds
- yum install man man-pages:安装
- man 2 select :查看select 使用
一、select、poll
- 共有: 代码中while { select(fds) 只发生一次系统调用<耗时O(1)>,recv(fd ) 耗时O(m) },站在程序角度,调用select()的时候,内核才遍历fds修正状态。
- select: FD_SIZE 限制了1024,但是可以修改配置。
- poll: 没有1024的限制
二、epoll
特点:
- 用户态到内核态不需要再不断传递fds,内核也不需要发生遍历
- 即便程序不调用内核,内核也会随着中断完成有状态fd的变更
- 没有1024的限制
- 内核开辟独立空间存储fds,红黑树的结构
epoll下的fds存储空间:
epoll是通过内核与用户空间mmap同一块内存实现的。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。内核可以直接看到epoll监听的句柄,效率高。
红黑树将存储epoll所监听的套接字。上面mmap出来的内存如何保存epoll所监听的套接字,epoll在实现上采用红黑树去存储所有套接字,当添加或者删除一个套接字时(epoll_ctl),都在红黑树上去处理,红黑树本身插入和删除性能比较好,时间复杂度O(logN)。
三个关键方法:
- 新建epoll描述符==epoll_create(…)
- 在红黑树上添加、删除fd 并绑定事件== epoll_ctrl(ep_fd,OP,fds,EVENT),<epoll描述符,添加或者删除所有待监控的连接>
- 返回的活跃连接 ==epoll_wait(ep_fd),<epoll描述符>
三种方式的多路复用器体现到java就是selector<多路开关选择器>,一个选择器能够管理多个信道上的I/O操作,
结合下面的java代码实例,来看整个epoll过程:
epoll过程:
- listen socket 绑定的是fd4,当java 程序调用【selector = Selector.open(); 对应内核调用epoll_create(…)】,epoll_create(…) 只会执行一次,假如返回ep_fd6,然后在kernel开辟一块ep_fd6对应的空间,红黑树结构储存fd,
- 紧接着调用【server.register(selector, SelectionKey.OP_ACCEPT);底层对应 把注册的fd暂存到了jvm内部。
- 然后程序循环调用【selector.select() ,底层触发 epoll_ctr(ep_fd6,ADD,fd4,OP_ACCEPT)】,将fd4放到ep_fd6对应区域的红黑树上,注册为OP_ACCEPT事件,epoll_wait(…)】,当方法返回值大于0时,代表有 有状态的fd 需要处理。
- 当初处理OP_ACCEPT事件时,会给创建的连接分配一个fd7,并给fd7注册OP_READ事件,调用epoll_crl() 放入到内核的红黑树中。
数据到达:
目前红黑树只有一个fd4,当有连接请求时,经过网卡,发生中断,会把网卡数据放到fd4的buffer上,同时把红黑树里的fd4拷贝到链表中,当epoll_wait()被调用时会把fd4返回。程序再发起read()<所有同步io模型,都需要程序自己发起read()>,
nio+epoll流程:
**例子:**通过Java代码跟上面的内核触发流程对应上。(select、poll、epoll都可以结合下面的实例一一对应)
客户端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
/**
* @author: di
* @create: 2020-06-06 15:12
*/
public class C10Kclient {
public static void main(String[] args) {
LinkedList<SocketChannel> clients = new LinkedList<>();
InetSocketAddress serverAddr = new InetSocketAddress("192.168.150.11", 9090);
for (int i = 10000; i < 65000; i++) {
try {
SocketChannel client1 = SocketChannel.open();
client1.bind(new InetSocketAddress("192.168.150.1", i));
// 192.168.150.1:10000 192.168.150.11:9090
client1.connect(serverAddr);
clients.add(client1);
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("clients "+ clients.size());
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class SocketMultiplexingSingleThreadv1 {
private ServerSocketChannel server = null;
private Selector selector = null; //linux 多路复用器(select>poll>epoll) nginx event{}
int port = 9090;
public void initServer() {
try {
/**
*
* 1、所有模式都必须走的
* server 约等于 listen状态的 fd4
*/
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
/**
* 2、
* select、poll、epoll 优先选择:epoll 但是可以 -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider 修正
* epoll模型下
* open--》 epoll_create -> fd3
* select、poll下
* 无系统调用
*/
selector = Selector.open(); //
/**
* 3、register
* select,poll:
* jvm里开辟一个数组 fd4 放进去
* epoll:-- 此时register注册事件只是放到一个暂存区,未调用epoll-ctl [20210316 update]
* epoll_ctl(fd3,ADD,fd4,EPOLLIN
*/
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
initServer();
System.out.println("服务器启动了。。。。。");
try {
while (true) { //死循环
Set<SelectionKey> keys = selector.keys();
System.out.println(keys.size()+" size");
/**
* 1、
* 调用多路复用器(select,poll or epoll (epoll_wait))
* select()是啥意思:
* select,poll下:
* 对应内核的select(fd4) poll(fd4)
* epoll下:
* 对应内核的 epoll_wait()
*
* select()参数可以带时间:没有时间,0 : 阻塞,有时间设置一个超时
* selector.wakeup() 结果返回0
* 懒加载:[20210316 update]
* 其实此时触发到selector.select()调用的时候,才正常触发了epoll_ctl的调用,调用了epoll_wait
* 问题:如果只调用 server.register(..),因为未触发epoll_ctl,发生中断时,fd是否会添加到rdlist 就绪列表中???
*/
while (selector.select(500) > 0) {
//返回的有状态的fd集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
/**
* 管你啥多路复用器,你呀只能给我状态,我还得一个一个的去处理他们的R/W。
* NIO 自己对着每一个fd调用系统调用,浪费资源,而这里只调用了一次select方法,就知道具体的那些可以R/W了?
* socket: listen 通信 R/W
*/
while (iter.hasNext()) {
SelectionKey key = iter.next();
//set 不移除会重复循环处理
iter.remove();
if (key.isAcceptable()) {
/**
* 2-1、
* 看代码的时候,这里是重点,如果要去接受一个新的连接
* 语义上,accept接受连接且返回新连接的FD对吧?
* 那新的FD怎么办?
* select,poll,因为他们内核没有空间,那么在jvm中保存和前边的fd4那个listen的一起
* epoll: 我们希望通过epoll_ctl把新的客户端fd注册到内核空间
*/
acceptHandler(key);
} else if (key.isReadable()) {
/**
* 2-2、
* 连read 还有 write都处理了
* 在当前线程,这个方法可能会阻塞 ,如果阻塞了十年,其他的IO早就没电了。。。
* 所以,为什么提出了 IO THREADS
* redis 是不是用了epoll,redis是不是有个io threads的概念 ,redis是不是单线程的
* tomcat 8,9 异步的处理方式 IO 和 处理上 解耦
*/
readHandler(key);
}
//此处没有处理写事件,放到后面讲
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void acceptHandler(SelectionKey key) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//目的是调用accept接受客户端 fd7
SocketChannel client = ssc.accept();
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(8192); //前边讲过了
/**
* 你看,调用了register
*
* select,poll:
* jvm里开辟一个数组 fd7 放进去
* epoll:-- 此时register注册事件只是放到一个暂存区,未调用epoll-ctl [20210316 update]
* epoll_ctl(fd3,ADD,fd7,EPOLLIN fd7放到ep_fd3对应的红黑树上
*/
client.register(selector, SelectionKey.OP_READ, buffer);
System.out.println("-------------------------------------------");
System.out.println("新客户端:" + client.getRemoteAddress());
System.out.println("-------------------------------------------");
} catch (IOException e) {
e.printStackTrace();
}
}
public void readHandler(SelectionKey key) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read = 0;
try {
while (true) {
//read()
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
client.write(buffer);
}
buffer.clear();
} else if (read == 0) {
break;
} else {
//-1 ,对应直接把连接断了
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();
service.start();
}
}
三、进程跟踪调用过程
strace 跟踪整个进程执行过程
strace: 可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间。
shell> java -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider SocketMultiplexingSingleThreadv1 //指定poll方式
shell> strace -ff -o epoll java SocketMultiplexingSingleThreadv1 //默认epoll方式,发生调用后生成epoll.pid 进程文件
shell> netstat -natp
shell> nc localhost
poll进程文件内容:
epoll进程文件内容:
通过 lsof -p pid ,看到对应进程打开的fd,可以看到有个ep_fd 7。
四、问题:
1、如果只调用 server.register(…),因为未触发epoll_ctl,发生中断时,fd是否会添加到rdlist 就绪列表中???
答:待确定
总结
其实无论nio、select、poll都是需要遍历所有的fd,询问状态。