一、概念说明
1、内核态(内核空间)和用户态(用户空间)的区别和联系?
用户空间是用户进程所在的内存区域,系统空间是操作系统所在的内存区域。为了保证内核的安全,处于用户态的程序只能访问用户空间,而处于内核态的程序可以访问用户空间和内核空间。
2、文件描述符fd
Linux将所有设备都当做文件来处理,文件描述符来标识每个文件对象。
当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
3、缓存IO
Linux的缓存IO机制中,操作系统会将IO的数据缓存在文件系统的页缓存中,也就是说,数据会先被拷贝到操作系统内核的缓冲区,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
1.1. Java IO读写原理
read系统调用,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区。这个两个系统调用,都不负责数据在内核缓冲区和磁盘之间的交换。底层的读写交换,是由操作系统kernel内核完成的
1.1.1. 内核缓冲与进程缓冲区
缓冲区的目的,是为了减少频繁的系统IO调用。大家都知道,系统调用需要保存之前的进程数据和状态等信息,而结束调用之后回来还需要恢复之前的信息,为了减少这种损耗时间、也损耗性能的系统调用,于是出现了缓冲区。
有了缓冲区,操作系统使用read函数把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区中。等待缓冲区达到一定数量的时候,再进行IO的调用,提升性能。至于什么时候读取和存储则由内核来决定,用户程序不需要关心。
在linux系统中,系统内核也有个缓冲区叫做内核缓冲区。每个进程有自己独立的缓冲区,叫做进程缓冲区。
所以,用户程序的IO读写程序,大多数情况下,并没有进行实际的IO操作,而是在读写自己的进程缓冲区。
1.1.2. java IO读写的底层流程
用户程序进行io的读写,用到系统调用read&write,read将数据从内核缓冲区复制到用户的进程缓冲区,write将用户的进程缓冲区的数据复制到内核缓冲区
client<->网卡<->read&write<->内核缓冲区<—>read&write-<>用户进程缓冲区<->用户程序
1.2. 四种主要的IO模型
常见的io模型有四种
(1)同步阻塞io(Blocking IO)BIO
- 两个过程
1)内核kernel准备数据
2)复制数据到进程缓冲区,并返回用户进程
是传统的io模型,是在读写数据的过程中发生阻塞,用户线程发出io读写请求后(用户线程调用了read系统调用),内核kernel回去检查数据是否就绪,如果数据没有就绪,用户线程就会阻塞,用 户线程交出 CPU
当数据就绪时,内核会将数据复制到进程缓冲区,并返回结果给用户线程.
用户才解除block状态. 典型的例子
data= socket.read();
优点:在阻塞期间,用户线程挂起,不会占用cpu资源
缺点:
(2)同步非阻塞(None Blocking IO)
NIO 模型中应用程序在一旦开始IO系统调用,出现两种情况
1)内核缓冲区没有准备好的情况下,用户线程发起系统io调用立即返回,这样就得到了一个结果。如果结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作,用户线程需要循环发起系统io调用
2)内核缓冲区就绪后,用户发起io调用,用户线程阻塞.kernel开始从内核缓冲区复制数据到用户进程缓冲区,然后kernel返回结果
3)用户线程解除block
典型的非阻塞 IO 模型一般如下:
while(true){
data = socket.read(); if(data!= error){ 处理数据
break;
}
}
优点:每次发起系统io调用,内核等待数据的过程中可以立即返回,用户线程不会阻塞
缺点:需要重复的发起io系统调用,不断的轮询内核,占用大量的cpu时间,cpu资源利用率低
总之,NIO模型在高并发场景下,也是不可用的。一般 Web 服务器不使用这种 IO 模型。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。java的实际开发中,也不会涉及这种IO模型。
(3)io多路复用
再次说明,Java NIO(New IO)( 不是IO模型中的NIO模型,而是另外的一种模型,叫做IO多路复用模型( IO multiplexing )。
IO多路复用是一种新的系统调用,一个进程可以监视多个文件描述符,一旦描述符就绪(一般是内核缓冲区可读/可写),内核kernel能够通知程序进行相应的IO系统调用
目前支持IO多路复用的系统调用,有 select,epoll等等。select系统调用,是目前几乎在所有的操作系统上都有支持,具有良好跨平台特性。epoll是在linux 2.6内核中提出的,是select系统调用的linux增强版本。
IO多路复用模型的基本原理就是select/epoll系统调用,单个线程不断的轮询select/epoll系统调用所负责的成百上千的socket连接,当某个或者某些socket网络连接有数据到达了,就返回这些可以读写的连接。因此,好处也就显而易见了——通过一次select/epoll系统调用,就查询到到可以读写的一个甚至是成百上千的网络连接。
流程
进行select/poll系统调用,前提是将目标网络注册到select/poll的可查询socket 列表中
1.用户线程轮询进行select/epoll系统调用(Java NIO 中的selector.select()),查询可以读的连接.kernel会查询所有select的socker列表,当有socket数据准备好,select的就会返回,如果没有事件,负责用户调用select系统调用的线程就会阻塞,因此,多路复用 IO 比较适合连 接数比较多的情况。
2.当用户线程获取到连接后,发起系统调用,用户线程阻塞,内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。
3.用户线程解除block,用户线程终于真正读取到数据,继续执行
多路复用IO的特点:
I多路复用IO需要用到两个系统调用(system call), 一个select/epoll查询调用,一个是IO的读取调用。
和NIO模型相似,多路复用IO需要轮询。负责select/epoll查询调用的线程,需要不断的进行select/epoll轮询,查找出可以进行IO操作的连接。
另外,多路复用IO模型与前面的NIO模型,是有关系的。对于每一个可以查询的socket,一般都设置成为non-blocking模型。只是这一点,对于用户程序是透明的(不感知)。
多路复用IO的优点:
用select/epoll的优势在于,它可以同时处理成千上万个连接(connection)。与一条线程维护一个连接相比,I/O多路复用技术的最大优势是:系统不必创建线程,也不必维护这些线程,从而大大减小了系统的开销。
Java的NIO(new IO)技术,使用的就是IO多路复用模型。在linux系统上,使用的是epoll系统调用。
多路复用IO的缺点:
本质上,select/epoll系统调用,属于同步IO,也是阻塞IO。都需要在读写事件就绪后,自己负责进行读写,也就是说这个读写过程是阻塞的。
另外多路复用 IO 为何比非阻塞 IO 模型的效率高是因为在非阻塞 IO 中,不断地询问 socket 状态
时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效 率要比用户线程要高的多。
不过要注意的是,多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件 逐一进行响应。因此对于多路复用 IO 模型来说,一旦事件响应体很大,那么就会导致后续的事件 迟迟得不到处理,并且会影响新的事件轮询。
(4)异步io(asynchronous IO)
如何进一步提升效率,解除最后一点阻塞呢?这就是异步IO模型,全称asynchronous I/O,简称为AIO。
AIO的基本流程是:用户线程通过系统调用,告知kernel内核启动某个IO操作,用户线程返回。kernel内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。
(1)当用户线程调用了read系统调用,立刻就可以开始去做其它的事,用户线程不阻塞。
(2)内核(kernel)就开始了IO的第一个阶段:准备数据。当kernel一直等到数据准备好了,它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存)。
(3)kernel会给用户线程发送一个信号(signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了。
(4)用户线程读取用户缓冲区的数据,完成后续的业务操作。
异步IO模型的特点:
在内核kernel的等待数据和复制数据的两个阶段,用户线程都不是block(阻塞)的。用户线程需要接受kernel的IO操作完成的事件,或者说注册IO操作完成的回调函数,到操作系统的内核。所以说,异步IO有的时候,也叫做信号驱动 IO 。
异步IO模型缺点:
需要完成事件的注册与传递,这里边需要底层操作系统提供大量的支持,去做大量的工作。
目前来说, Windows 系统下通过 IOCP 实现了真正的异步 I/O。但是,就目前的业界形式来说,Windows 系统,很少作为百万级以上或者说高并发应用的服务器操作系统来使用。
而在 Linux 系统下,异步IO模型在2.6版本才引入,目前并不完善。所以,这也是在 Linux 下,实现高并发网络编程时都是以 IO 复用模型模式为主。
kernel的数据准备是将数据从网络物理设备(网卡)读取到内核缓冲区;kernel的数据复制是将数据从内核缓冲区拷贝到用户程序空间的缓冲区。
4、信号驱动IO(signal driven IO)bu不常用
在信号驱动 IO 模型中,当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函 数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到 信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作。
I/O 多路复用之select、poll、epoll详解
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
1、select
select函数监视文件描述符,调用后select函数会阻塞,直到有描述符就绪,或者超时,函数返回,当select函数返回后,就可以遍历描述符,找到就绪的描述符。
select的一个缺点在于单个进程能够监视的文件描述符的数量也存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制。但是这样也会造成效率的降低。
2、poll
没有最大限制(但是数量过大后性能也是会下降)。和select函数一样,poll返回后,需要遍历来获取就绪的描述符。
select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在同一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
3、epoll
相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符。
参考
https://www.cnblogs.com/crazymakercircle/p/10225159.html
https://www.cnblogs.com/natian-ws/p/10785649.html
https://segmentfault.com/a/1190000003063859?utm_source=tag-newest#articleHeader0