Netty源码分析—Linux网络IO模型简介

Linux网络IO模型简介

Linux的内核将所有外部设备都看做成一个文件来操作,对一个文件的读写操作也会调用到内核提供的系统命令,返回一个fd(file discriptor 文件描述符)。而对一个socket的读写也会有相应的描述符,称为socket fd(socket描述符),描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性),根据UNIX网络编程对I/O模型的分类,UNIX提供了5中I/O模型,分别如下。

阻塞IO模型

在内核将数据准备好之前,系统调用会一直等待所有的套接字Socket传来数据,默认的是阻塞的方式。
Netty源码分析—Linux网络IO模型简介
Java中socket.read()方式最终会调用底层操作系统的recfrom方法,os会判断来自网络的数据是否准备好,当数据报准备好了之后,os就会将数据从内核空间拷贝到用户空间(因为我们的用户程序只能获取用户空间的内存,无法直接获取内核空间的内存)。拷贝完之后socket.read()就会解除阻塞,并得到网络数据的结果。

BIO中的阻塞,就得阻塞在这2个地方

  1. OS等待数据报通过网络发送过来,如果建立连接之后数据一一直没有发送过来,就会白白的浪费线程的资源
  2. 将数据从内核空间拷贝到用户空间

在这两个时候,我们的线程会一直阻塞,啥事情都干不了

非阻塞IO模型

Netty源码分析—Linux网络IO模型简介
每次应用程序询问内核是否有数据报准备好,当有数据报准备好的时候,就进行拷贝数据报的操作,从内核拷贝到用户空间,和拷贝完成范围的这段时间,应用进程是阻塞的,但在没有数据报准备好的时候,并不会阻塞程序,内核直接返回为准备好的信号,等待应用进程的下一次询问。但是轮询的方式对于CPU来说是较大的浪费,一般只有在特定的场景下才会使用。

从图中可以看到,非阻塞IO的recvfrom()调用会立即得到一个返回结果,这个结果是关于数据报是否准备皓的,我们可以根据返回的结果继续执行不同的逻辑。而阻塞IO的recvfrom调用,如果无数据报准备好,那么就一定会被阻塞住。虽然非阻塞IO比阻塞IO少了一段阻塞的过程,但是事实上非阻塞IO也是低效的,因为我们不得不使用轮的方式去询问OS: 我的数据准备好没有?

BIO不会在拷贝数据之前阻塞,但是会在将数据从内核空间拷贝到用户空间的时候阻塞,一定要注意这个地方,Non - Blocking还是会阻塞的。

IO复用模型

Linux提供select/poll。进程通过将一个或者多个fd传递给select系统调用,阻塞发生在select/poll操作之后。select/poll可以帮我们监视多个fd是否处于就绪状态,他们顺序扫描fd是否就绪,但是支持的fd的数量有限,因为它的使用也受到了一些制约。

Linux还提供了一个epoll的系统调用,epoll使用基于时间驱动的方式代替顺序扫描,因此性能更高,当有fd就绪的时候,立即回调函数rollback。
Netty源码分析—Linux网络IO模型简介

信号驱动IO模型

首先开启 套接口 信号驱动IO功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,他是非阻塞的)。当数据准备就绪的时候,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序recvfrom来读取数据,并通知主循环函数来处理数据
Netty源码分析—Linux网络IO模型简介

异步IO模型

告知内核启动某个操作,并让内核在整个操作完成之后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的区别就是,信号驱动IO由内核通知我们何时可以开始一个IO操作。异步IO模型由内核通知我们IO操作何时完成。
Netty源码分析—Linux网络IO模型简介
从这无种IO模型的结构可以看出 阻塞程度:阻塞IO > 非阻塞IO > io多路复用 > 信号驱动IO > 异步IO ,效率也是由低到高的。

IO多路复用技术

Java NIO的核心类库,多路复用器Selector就是基于epoll的多路复用技术的实现

在IO编程过程中,当需要同时处理多个客户端接入请求的时候,可以利用多线程或者IO多路复用技术处理,IO多路复用技术通过吧多个IO阻塞复用到一个select的阻塞上,从而使得系统在单线程的情况下也可以同时处理多个客户端请求,IO多路复用的最大优势是系统开销小,系统不需要创建额外的进程或者线程,也不需要维护这些线程和线程的运行,降低了系统的维护工作量,节省了系统资源,IO多路复用的主要应用场景如下:

服务器需要同时处理多个监听状态或者多个连接状态的套接字
服务器需要同时处理多个网络协议中的套接字

目前支持IO多路复用的系统调用有select,pselect,poll,epoll,在Linux网络编程的过程中,很长一段时间都是用select做轮询和网络事件通知,然而select的一些固有缺陷导致了它的应用受到了很大的限制,最终linux选择了epoll.epoll与select的原理比较类似,为了克服select的缺点,epoll做了很多改进。

1.支持一个进程打开的socket描述符fd不受限制(仅仅受限于操作系统的最大文件句柄数)
2.IO效率不会随着FD数目的增加而线性下降
3.epoll的API更加简单

值得说明的是,用来克服select/epoll缺点的方法不只有epoll,epoll只是Linux的一种实现方案。

select / epoll的区别

IO多路复用的机制
多路复用是同步非阻塞IO,他是利用单独的线程(内核级别)统一检测所有的socket,一旦有某个Socket有了IO数据,则启动相应的Application处理,在Select和poll中轮询socket句柄的方式来实现检测socket是否有IO数据到达,这种方式是由开销的,epoll则改进了这种方式,底层调用notify机制,即Reactor方式来检测,Java NIO也是采用这种机制,这里需要注意,其实多路复用还是有阻塞的(这个阻塞并非定义上的阻塞,这里是指Socket无IO数据的时候还是被wait,此外当使用select函数copy IO数据进入Application buffer ,application还是会被阻塞的)

select / epoll的区别

select
1.每次调用select,都要把fd集合从用户态拷贝到内核态,这个开销在fd很多的时候会很大
2.同时每次调用select都需要在内核遍历传递进来所有fd,这个开销在fd很多的时候也很大
3.select支持的文件描述符太小了,默认是1024

poll
4.它没有最大连接数的限制,原因是他是居于链表来存储的,但是同样有一个缺点:大量的fd的数组被整体复制与用户态和内核态地址空间之间,而不管这样的复制是不是有意义

epoll
5. epoll 会为每个fd指定一个回调函数,当设备就绪的时候,唤醒等待队列上的等待者,就会调用这个回调函数
epoll所支持的FD上限是最大可以打开文件的数目