NIO详解(三):IO多路复用模型之select、poll、epoll 客官°小女子只卖身不卖艺 2022-02-25 15:08 410阅读 0赞 # 1. 前言 # 最近在研究基于Java的高性能异步非阻塞I/O框架Netty,因为最近做的RDMAChannel要用到其中的思想。Netty底层是通过大量的NIO实现的,通过分析底层NIO源码,发现NIO底层调用的是poll系统调用。所以本博客就来细谈select、poll、epoll系统调用。 # 2. NIO Selector底层源码分析 # protected int doSelect(long timeout) throws IOException { if (channelArray == null) throw new ClosedSelectorException(); this.timeout = timeout; // set selector timeout processDeregisterQueue(); if (interruptTriggered) { resetWakeupSocket(); return 0; } // Calculate number of helper threads needed for poll. If necessary // threads are created here and start waiting on startLock adjustThreadsCount(); finishLock.reset(); // reset finishLock // Wakeup helper threads, waiting on startLock, so they start polling. // Redundant threads will exit here after wakeup. startLock.startThreads(); // do polling in the main thread. Main thread is responsible for // first MAX_SELECTABLE_FDS entries in pollArray. try { begin(); try { subSelector.poll(); } catch (IOException e) { finishLock.setException(e); // Save this exception } // Main thread is out of poll(). Wakeup others and wait for them if (threads.size() > 0) finishLock.waitForHelperThreads(); } finally { end(); } // Done with poll(). Set wakeupSocket to nonsignaled for the next run. finishLock.checkForException(); processDeregisterQueue(); int updated = updateSelectedKeys(); // Done with poll(). Set wakeupSocket to nonsignaled for the next run. resetWakeupSocket(); return updated; } 其中 subSelector.poll() 是select的核心,由native函数poll0实现,readFds、writeFds 和exceptFds数组用来保存底层select的结果,数组的第一个位置都是存放发生事件的socket的总数,其余位置存放发生事件的socket句柄fd。 在早期的JDK1.4和1.5 update10版本之前,Selector基于select/poll模型实现,是基于IO复用技术的非阻塞IO,不是异步IO。在JDK1.5 update10和linux core2.6以上版本,sun优化了Selctor的实现,底层使用epoll替换了select/poll。 # 2. I/O多路复用模型(I/O Multiplexing) # > 解决问题:如果一个I/O流进来,我们就开启一个进程处理这个I/O流。那么假设现在有一百万个I/O流进来,那我们就需要开启一百万个进程一一对应处理这些I/O流(——这就是传统意义下的多进程并发处理)。思考一下,一百万个进程,你的CPU占有率会多高,这个实现方式及其的不合理。所以人们提出了I/O多路复用这个模型,一个线程,通过记录I/O流的状态来同时管理多个I/O,可以提高服务器的吞吐能力。 # 3. select、poll、epoll简介 # select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。**但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的**,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIxMTI1MTgz_size_16_color_FFFFFF_t_70] ## 3.1 select实现 ## 在这种模式中,首先不是进行read系统调动,而是进行select系统调用。当然,这里有一个前提,需要将目标网络连接,提前注册到select的可查询socket列表中。然后,才可以开启整个的IO多路复用模型的读流程。(1)进行select系统调用,查询可以读的连接。kernel会查询所有select的可查询socket列表,当任何一个socket中的数据准备好了,select就会返回。当用户进程调用了select,那么整个线程会被block(阻塞掉)。(2)用户线程获得了目标连接后,发起read系统调用,用户线程阻塞。内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。(3)用户线程才解除block的状态,用户线程终于真正读取到数据,继续执行。 select的调用过程如下所示: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIxMTI1MTgz_size_16_color_FFFFFF_t_70 1] > 1. 使用copy\_from\_user从用户空间拷贝fd\_set到内核空间。fd\_set是需要查询的文件描述符(可以理解为Socket)。 > 2. 程序注册注册回调函数\_\_pollwait,进入阻塞状态。 > 3. 遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock\_poll,sock\_poll根据情况会调用到tcp\_poll,udp\_poll或者datagram\_poll)。 > 4. 把fd\_set从内核空间拷贝到用户空间。 \_\_pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp\_poll来说,其等待队列是sk->sk\_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。一旦设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。用户线程可以通过相应的fd读取到相应的可读socket中的数据了。 poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd\_set赋值。如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule\_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程,然后进程就会通过相应的fd读取到相应的可读的socket中的数据了。如果超过一定的超时时间(schedule\_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。 ### 总结: ### select的几大缺点: > * 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 > * 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 > * select支持的文件描述符数量太小了,默认是1024 ## 3.2 poll实现 ## poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd\_set结构,其他的都差不多。但是它没有最大连接数的限制,原因是它是基于链表来存储的. ## 3.3 epoll实现 ## epoll提供了三个函数,epoll\_create,epoll\_ctl和epoll\_wait。 > * epoll\_create是创建一个epoll句柄; > * epoll\_ctl是注册要监听的事件类型; > * epoll\_wait则是等待事件的产生。 对于第一个缺点,epoll的解决方案在epoll\_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll\_ctl中指定EPOLL\_CTL\_ADD),会把所有的fd拷贝进内核,而不是在epoll\_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。 对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll\_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒进程等待队列上的等待者时,然后调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。**epoll\_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule\_timeout()实现睡一会,判断一会的效果,和select实现中的是类似的)** 对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。 ### 总结: ### (1)epoll其实也需要调用epoll\_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,**但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll\_wait中进入睡眠的进程**。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制(Callback)带来的性能提升。 (2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll\_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIxMTI1MTgz_size_16_color_FFFFFF_t_70 2] [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIxMTI1MTgz_size_16_color_FFFFFF_t_70]: /images/20220225/b0353a438766464c91872dbf1a79fd95.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIxMTI1MTgz_size_16_color_FFFFFF_t_70 1]: /images/20220225/32ee2eb99504478b9b49fe96d4ca45b1.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIxMTI1MTgz_size_16_color_FFFFFF_t_70 2]: /images/20220225/830787b5965b433498ce806dd52d04f4.png
还没有评论,来说两句吧...