NIO 的优劣

优势:相比 BIO 来说,NIO 可以通过1个或几个线程,来解决 N 个 IO 连接的处理
弊端:当有大量文件描述符存在时,不管你用多少个线程,都是O(n)复杂度的recv调用,需要用户态内核态切换才能实现,而这些调用有很多是无意义的(有数据返回数据,无数据返回-1),浪费资源。
read是无罪的,大量无效的read被调用才是性能损耗的关键。

下图:左侧是 NIO,右侧是多路复用器。

多路复用器的实现

常问的几个概念

(我们先只关注IO,不关注IO之后的处理)
同步:application 自己读写内容
异步:由 kernel 完成 IO 内容的读写,写到进程的一个 buffer 区域里,看起来好像程序没有访问 IO,只是访问了 buffer 就能拿到数据(实际上是在IO注册了一些回调),只有 windows 上的 iocp 是纯异步的
阻塞:Blocking,如果没有则等待
非阻塞:Non-Blocking,一定能拿到返回值,就算没有数据,也会返回-1

目前来说,在Linux以及主流成熟框架中,我们常用的是同步阻塞、同步非阻塞的组合。

通过多路复用器只能获取状态,最终还是需要由程序对有状态的IO进行读/写。
只要程序自己读写,那么你的IO模型就是同步的。(而不是你读完了IO数据之后的处理的同步或异步)

多路复用器:select, poll, epoll 都是多路复用器,都属于同步状态下非阻塞的模型。
select 是在不同操作系统中很容易实现的,不依赖特定的软硬件的一个系统调用。
epoll 要求内核当中要求一定的实现,在linux上是epoll,在unix上是kqueue。技术是随着问题的产生一步步发展起来的。
这两者都是基于IO事件的一种通知行为。

异步阻塞是没有意义的。异步都是用非阻塞。

关于为什么linux目前没有通用的内核异步处理方案,因为这样不安全,会让linux的内核做的事情太多,容易出bug。windows敢于这么做,是因为windows的市场比较广,一方面是用户市场,一方面是服务器市场,它的市场比较广,况且windows比较注重用户市场,所以敢于把内核做的胖一些,也是因此虽然现在已经win10了,但是蓝屏啊,死机啊,挂机啊这些问题也还是会出现。Linux现在6.x版本当中,对异步也开始上心了。

select

man select 帮助文档中的描述:

翻译:select()和pselect()允许程序监视多个文件描述符,直到其中一个或多个文件描述符为某种I/O操作(如输入可能)“准备好”。如果文件描述符可以不阻塞地执行相应的I/O操作(如read(2)),则认为它已经准备好了。

select在linux中有一个FD_SETSIZE(大小为1024)的限制,所以现在一般不用select了


其实,无论是NIO,还是SELECT,还是POLL,这些多路复用器都是要遍历所有的IO询问状态。

只不过,在NIO中,这个遍历的成本在用户态到内核态的切换。
但是在SELECT、POLL的模型下,遍历的过程触发了一次系统调用(用户态到内核态的切换),过程中把很多的fd文件描述符传递给内核,内核重新根据用户这次调用传过来的所有fd,遍历并修改状态。每次都要重新重复传递fd。

所以多路复用器在这个时期,就已经比NIO快了。

SELECT、POLL的弊端在于,每次都要重新传递fd,造成每次内核被调用之后,针对这次调用都要触发一个fd的全量遍历的复杂度。

这里插入一个概念

在内存中,有 kernel,有 app 等等的这些程序
软中断: trap int 80
硬中断:时钟中断(晶振)
IO中断:网卡、硬盘、鼠标

关于网络IO中断
最开始的时候,网卡来了IO数据包的时候,是可以产生中断的,这时候就会打断CPU,将输入的数据存到内存中。
后来经过改进,网卡是有buffer的,在内存中开辟一个DMA区域,专门给网卡用,网卡可以收集很多数据之后积攒起来,积攒到一定量之后一起发给DMA。

中断会产生callback回调函数
event有事件,就要去处理
在epoll之前的callback,只是完成了将网卡发来的数据,走一下内核的网络协议栈(2链路,3网络,4传输层),最终关联到fd的buffer里面
所以你在某一时间,如果从application询问内核某一个或者某些fd是否可读/可写,会有状态返回。

如果内核在回调的处理中,再加入(?红黑树、list),就有了多selector

epoll

epoll 规避了遍历的问题。

帮助手册:

翻译:

epoll API执行与poll(2)类似的任务:监视多个文件描述符,看看其中任何一个文件描述符上是否有I/O。epoll API既可以用作边缘触发接口,也可以用作级别触发接口,可以很好地扩展到大量监视的文件描述符。提供以下系统调用来创建和管理一个epoll实例:

  • epoll_create(2)创建一个epoll实例,并返回引用该实例的文件描述符。(最近的epoll_create1(2)扩展了epoll_create(2)的功能。)
  • 然后通过epoll_ctl(2)注册对特定文件描述符的兴趣。当前在epoll实例上注册的文件描述符集有时称为epoll集。
  • epoll_wait(2)等待I/O事件,如果当前没有可用的事件,则阻塞调用线程。

epoll_create
在内核中开辟一块空间,用来放红黑树

epoll_ctl
添加、修改、删除某一个文件描述符,并记录关注它的哪些事件(如read事件)

epoll_wait
epoll_wait 在等待从红黑树复制过来的一个链表

下图:epoll(左) 与 select/poll 的本质区别(右):
epoll 已经悄悄地将结果集给你准备好了,你需要有状态的结果集fds的时候,直接取就可以了。它不传递 fds, 也不触发内核遍历。

讲了这么多操作系统内核提供的多路复用器,最终我们都要回归到受制于Java对于这些系统调用的包装:Selector

回归到 Java 代码

SocketMultiplexingSingleThreadv1.java

package com.bjmashibing.system.io;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;public class SocketMultiplexingSingleThreadv1 {//这个代码看不懂的话,可以去看马老师的坦克 一、二期(netty)private ServerSocketChannel server = null;private Selector selector = null;   //linux 多路复用器(select poll epoll kqueue) nginx  event{}int port = 9090;public void initServer() {try {server = ServerSocketChannel.open();server.configureBlocking(false); // 设置成非阻塞server.bind(new InetSocketAddress(port));  // 绑定监听的端口号//如果在epoll模型下,Selector.open()其实完成了epoll_create,可能给你返回了一个 fd3selector = Selector.open();  // 可以选择 select  poll  *epoll,在linux中会优先选择epoll  但是可以在JVM使用-D参数修正//server 约等于 listen 状态的 fd4/*register 初始化过程如果在select,poll的模型下,是在jvm里开辟一个数组,把fd4放进去如果在epoll的模型下,调用了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))/*java中的select()是啥意思:1,如果用select,poll 模型,其实调的是内核的select方法,并传入参数(fd4),或者poll(fd4)2,如果用epoll模型,其实调用的是内核的epoll_wait()注意:参数可以带时间。如果没有时间,或者时间是0,代表阻塞。如果有时间,则设置一个超时时间。方法selector.wakeup()可以外部控制让它不阻塞。这时select的结果返回是0。*/while (selector.select(500) > 0) {Set<SelectionKey> selectionKeys = selector.selectedKeys();  //拿到返回的有状态的fd集合Iterator<SelectionKey> iter = selectionKeys.iterator();  // 转成迭代器//所以,不管你是啥多路复用器,你只能告诉我fd的状态,我还得一个一个的去处理他们的R/W。同步好辛苦!!!//我们之前用NIO的时候,需要自己对着每一个fd调用系统调用,浪费资源,那么你看,这里是不是调用了一次select方法,知道具体的那些可以R/W了?是不是很省力?while (iter.hasNext()) {SelectionKey key = iter.next();iter.remove(); //这时一个set,不移除的话会重复循环处理if (key.isAcceptable()) { //我前边强调过,socket分为两种,一种是listen的,一种是用于通信 R/W 的//这里是重点,如果要去接受一个新的连接//语义上,accept接受连接且返回新连接的FD,对吧?//那新的FD怎么办?//如果使用select,poll的时候,因为他们内核没有空间,那么在jvm中保存,和前边的fd4那个listen的放在一起//如果使用epoll的话,我们希望通过epoll_ctl把新的客户端fd注册到内核空间acceptHandler(key);} else if (key.isReadable()) {readHandler(key);//在当前线程,这个方法可能会阻塞,如果阻塞了十年,其他的IO早就没电了。。。//所以,为什么提出了 IO THREADS,我把读到的东西扔出去,而不是现场处理//你想,redis是不是用了epoll?redis是不是有个io threads的概念?redis是不是单线程的?//你想,tomcat 8,9版本之后,是不是也提出了一种异步的处理方式?是不是也在 IO 和处理上解耦?//这些都是等效的。}}}}} catch (IOException e) {e.printStackTrace();}}public void acceptHandler(SelectionKey key) {try {ServerSocketChannel ssc = (ServerSocketChannel) key.channel();SocketChannel client = ssc.accept(); //来啦,目的是调用accept接受客户端  fd7client.configureBlocking(false);ByteBuffer buffer = ByteBuffer.allocate(8192);  //前边讲过了// 0.0  我类个去//你看,调用了register/*select,poll:    jvm里开辟一个数组 fd7 放进去epoll:          epoll_ctl(fd3,ADD,fd7,EPOLLIN*/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 = client.read(buffer);if (read > 0) {buffer.flip();while (buffer.hasRemaining()) {client.write(buffer);}buffer.clear();} else if (read == 0) {break;} else {client.close();break;}}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();service.start();}
}

网络与IO知识扫盲(五):从 NIO 到多路复用器相关推荐

  1. 网络与IO知识扫盲(六):多路复用器

    NIO存在的问题 NIO的优势:可以通过一个或几个线程来解决N个IO连接的处理 NIO存在C10K问题:当客户端连接的数量达到10K时,单线程每循环一次所有的fd,成本是O(n)复杂度,每一次循环都会 ...

  2. 网络与IO知识扫盲(四):C10K问题、BIO的弊端与NIO的引入

    C10K 问题 C10K 问题: http://www.kegel.com/c10k.html 我们使用BIO的时候,来一个连接就抛出一个线程.被抛出的独立的线程进行阻塞,等待接收已连接的client ...

  3. 网络与IO知识扫盲(三):从系统调用的角度,剖析 Socket 的连接过程、BIO 的连接过程

    Socket的连接过程.TCP的一些参数 前置知识 用到的命令 netstat -natp 查看网络连接和占用的端口 tcpdump -nn -i eth0 port 9090 开监听抓取数据包 ls ...

  4. 网络与IO知识扫盲(一):Linux虚拟文件系统,文件描述符,IO重定向

    系统IO原理 在 Linux 中: VFS(Virtual Filesystem Switch):虚拟文件系统,是一个目录树.树上不同的节点可以映射到物理的文件地址,可以挂载. 相当于一个解耦层,在具 ...

  5. 网络与IO知识扫盲(七):仿照Netty工作架构图,手写多路复用模型

    Netty工作架构图 从图上看来: 一个线程在 Boss Group 中负责接收 另外两个线程在 Worker Group 中由接收之后的连接分配过去,负责读写 根据上图模型,仿照Netty手写一个多 ...

  6. 网络IO发展历程:BIO、NIO、多路复用器、epoll

    网络侧IO,通过网络来通信(偏向内核方面) C10K问题:http://www.kegel.com/c10k.html#frameworks BIO NIO 多路复用器 什么是NIO 操作系统角度:N ...

  7. Lwip从入门到放弃之(一)---基础网络知识扫盲

    Lwip从入门到放弃之-基础网络知识扫盲(一) 由于工作中用到了有关Lwip的有关知识,本人作为一个网络通信协议的门外汉,打算系统的学习一下以太网通讯的有关知识.而Lwip作为一款开源的轻量级TCP/ ...

  8. 网络知识扫盲,一文搞懂 DNS

    在找工作面试的过程中,面试官非常喜欢考察基础知识,除了数据结构与算法之外,网络知识也是一个非常重要的考察对象. 而网络知识,通常是很抽象,不容易理解的,有很多同学就在这里裁了跟头.为了更好地通过面试, ...

  9. android进阶3step2:Android App通信 ——端口号IP等网络基础知识扫盲

    网络操作基础知识 一.IP 地址和端口号 1) IP 地址用于在网络中唯一标识一台机器(通信实体),是一个 32 位整数,通常 用 4 个 0-255 的十进制数标识; 2) 端口号用于唯一标识通信实 ...

最新文章

  1. Mysql-cobar集群安装部署手册
  2. 让程序常驻后台运行的原理和方法
  3. Atmosic推出ATM33新品,全新的ATM33系列性能大升级
  4. QEMU CVE-2020-14364 漏洞分析(含 PoC 演示)
  5. 老项目换unicorn的大致步骤
  6. HDU2025 查找最大元素【入门】
  7. 18.1 集群介绍 18.2 keepalived介绍 18.3/18.4/18.5 用keepalived配置高可用集群
  8. PHP基础教程-54课-问题
  9. 还在搭建传统IT架构的你,正在慢慢被行业淘汰
  10. phpmyadmin创建账号授权指定表
  11. 2021系统分析师论文题目记忆
  12. 微机课设 | 基于STC15单片机的简易数字密码锁设计
  13. 记录linux deploy如何进行分区安装centos7
  14. 三极管作为电流源时的公式计算
  15. PHP 中的 use function是什么意思
  16. 飞行CSS3导航菜单
  17. spi通讯不需要地线吗_SPI通信时是不是主机和从机的MISO、MOSI两根线应该交叉接呀?...
  18. 机械键盘恢复出厂fn,机械键盘构成-求助,机械键盘fn键的解决方法
  19. 视频 | 苏炳添的“冠军卧室”曝光,来看看百米飞人的另一面
  20. Python学习记录day6-反射、常用模块

热门文章

  1. POJ - 1459 Power Network(网络流-最大流)
  2. UVA - 11214Guarding the Chessboard守卫棋盘(迭代加深搜索)
  3. codesys 简单案例_第一章:初识Codesys-1.4从一个示例程序讲起
  4. hdu4746(莫比乌斯反演)
  5. Shell case esac语句
  6. 关于寻路算法的一些思考(3):A*算法的实现
  7. C++ 基础 : 函数重载、引用、内联函数、auto、范围for循环
  8. 第四期直播分享预告-高薪offer指南
  9. 如何构建一套高性能、高可用性、低成本的视频处理系统?
  10. 干货 | 这次我们看看阿里的人是如何蹂躏CPU的