先来看看API的使用。
1. IO 流原理及流的分类
1.1 Java IO
1. I/O是Input/Output的缩写,I/O技术是非常实用的技术,用于处理数据传输。如读/写文件,网络通讯等。
2. Java程序中,对于数据的输入/输出操作以“流(Stream)”的方式进行。
3. java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过方法输入或输出数据
4. 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中
5. 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中
1.2 流的分类
image.png image.png节点流和处理流
节点流和处理流的区别和联系
- 节点流是底层流/低级流,直接跟数据源相接。
- 处理流(包装流)包装节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入输出
- 处理流(也叫包装流)对节点流进行包装,使用了修饰器设计模式,不会直接与数据源相连
处理流的功能主要体现在以下两个方面:
- 性能的提高:主要以增加缓冲的方式来提高输入输出的效率
- 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入输出大批量的数据,使用更加灵活方便。
来看看4种处理流:
1.处理流BufferedReader 和BufferedWriter
BufferedReader 和 BufferedWriter 属于字符流,是按字符来读取数据的
关闭时,处理流只需要关闭外层流即可
下面来看看BufferedReader使用。
public class BufferedReader_ {
public static void main(String[] args) throws Exception {
String filePath = "e:\\a.java";
//创建bufferedReader
BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
//读取
String line; //按行读取, 效率高
//说明
//1. bufferedReader.readLine() 是按行读取文件
//2. 当返回null 时,表示文件读取完毕
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
//关闭流, 这里注意,只需要关闭BufferedReader ,因为底层会自动的去关闭节点流
bufferedReader.close();
}
}
2.处理流BufferedInputStream 和BufferedOutputStream
BufferedInputStream 是字节流,在创建BufferedInputStream 时,会创建一个内部缓冲区数组。
BufferedOutputStream是字节流,实现缓冲的输出流,可以将多个字节写入底层输出流中,而不必对每次字节写入调用底层系统。
image.png
3.对象流ObjectInputStream 和ObjectOutputStream
功能:提供了对基本类型或对象类型的序列化和反序列化的方法
ObjectOutputStream 提供序列化功能
ObjectInputStream 提供反序列化功能
1.序列化就是在保存数据时,保存数据的值和数据类型
2.反序列化就是在恢复数据时,恢复数据的值和数据类型
3.需要让某个对象支持序列化机制,则必须让其类是可序列化的,为了让某个类是可序列化的,该类必须实现接口。
image.png
4.转换流InputStreamReader 和OutputStreamWriter
先看一个文件乱码问题,引出学习转换流必要性。
image.png
//看一个中文乱码问题
public class CodeQuestion {
public static void main(String[] args) throws IOException {
//读取e:\\a.txt 文件到程序
//思路
//1. 创建字符输入流BufferedReader [处理流]
//2. 使用BufferedReader 对象读取a.txt
//3. 默认情况下,读取文件是按照utf-8 编码
String filePath = "e:\\a.txt";
BufferedReader br = new BufferedReader(new FileReader(filePath));
String s = br.readLine();
System.out.println("读取到的内容: " + s);
br.close();
//InputStreamReader
//OutputStreamWriter
}
}
演示使用InputStreamReader 转换流解决中文乱码问题
public class InputStreamReader_ {
public static void main(String[] args) throws IOException {
String filePath = "e:\\a.txt";
//解读
//1. 把FileInputStream 转成InputStreamReader
//2. 指定编码 gbk
//InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath), "gbk"); //3. 把 InputStreamReader 传入 BufferedReader
//3. 把InputStreamReader 传入 BufferedReader
//BufferedReader br = new BufferedReader(isr);
//将2 和 3 合在一起
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "gbk"));
//4. 读取
String s = br.readLine();
System.out.println("读取内容=" + s);
//5. 关闭外层流
br.close();
}
}
1.3 Java IO读写原理
无论是Socket的读写还是文件的读写,在Java层面的应用开发或者是linux系统底层开发,都属于输入input和输出output的处理,简称为IO读写。在原理上和处理流程上,都是一致的。区别在于参数的不同。
用户程序进行IO的读写,基本上会用到read&write两大系统调用。可能不同操作系统,名称不完全一样,但是功能是一样的。
先强调一个基础知识:read系统调用,并不是把数据直接从物理设备,读数据到内存。write系统调用,也不是直接把数据,写入到物理设备。
read系统调用,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区。这个两个系统调用,都不负责数据在内核缓冲区和磁盘之间的交换。底层的读写交换,是由操作系统kernel内核完成的。
操作系统中谁负责IO拷贝?
DMA 负责内核间的 IO 传输,CPU 负责内核和应用间的 IO 传输。
两种拷贝类型:
(1)CPU COPY
通过计算机的组成原理我们知道, 内存的读写操作是需要 CPU 的协调数据总线,地址总线和控制总线来完成的因此在"拷贝"发生的时候,往往需要 CPU 暂停现有的处理逻辑,来协助内存的读写,这种我们称为 CPU COPY。CPU COPY 不但占用了 CPU 资源,还占用了总线的带宽。
(2)DMA COPY
DMA(DIRECT MEMORY ACCESS) 是现代计算机的重要功能,它有一个重要特点:当需要与外设进行数据交换时, CPU 只需要初始化这个动作便可以继续执行其他指令,剩下的数据传输的动作完全由DMA来完成可以看到 DMA COPY 是可以避免大量的 CPU 中断的。
拷贝过程中会发生什么?
从内核态到用户态时会发生上下文切换,上下文切换时指由用户态切换到内核态, 以及由内核态切换到用户态。
非零拷贝IO流程
https://zhuanlan.zhihu.com/p/430848775
由上图可以很清晰地看到, 一次 read-send 涉及到了四次拷贝:
硬盘拷贝到内核缓冲区(DMA COPY);
内核缓冲区拷贝到应用程序缓冲区(CPU COPY);
应用程序缓冲区拷贝到socket缓冲区(CPU COPY);
socket buf拷贝到网卡的buf(DMA COPY)。
其中涉及到2次 CPU 中断, 还有4次的上下文切换。很明显,第2次和第3次的的 copy 只是把数据复制到 app buffer 又原封不动的复制回来, 为此带来了两次的 CPU COPY 和两次上下文切换, 是完全没有必要的。
Linux 的零拷贝技术就是为了优化掉这两次不必要的拷贝。
内核缓冲与进程缓冲区
缓冲区的目的,是为了减少频繁的系统IO调用。大家都知道,系统调用需要保存之前的进程数据和状态等信息,而结束调用之后回来还需要恢复之前的信息,为了减少这种损耗时间、也损耗性能的系统调用,于是出现了缓冲区。
有了缓冲区,操作系统使用read函数把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区中。等待缓冲区达到一定数量的时候,再进行IO的调用,提升性能。至于什么时候读取和存储则由内核来决定,用户程序不需要关心。
在linux系统中,系统内核也有个缓冲区叫做内核缓冲区。每个进程有自己独立的缓冲区,叫做进程缓冲区。
所以,用户程序的IO读写程序,大多数情况下,并没有进行实际的IO操作,而是在读写自己的进程缓冲区。
2. NIO
NIO:Non-blocking IO ,非阻塞IO。
2.1 NIO API 简单理解
先看看NIO概述,可以参考这篇
java nio 详_java NIO 详解
下面来简单看看MappedByteBuffer,这是NIO包下的一个类。
https://www.runoob.com/manual/jdk1.6/java.base/java/nio/MappedByteBuffer.html
FileChannel提供了map方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射。
具体可参考这2篇
深入浅出MappedByteBuffer
MappedByteBuffer的使用
2.2 NIO的零拷贝
NIO的零拷贝由transferTo方法实现。transferTo方法将数据从FileChannel对象传送到可写的字节通道(如Socket Channel等)。在transferTo方法内部实现中,由native方法transferTo0来实现,它依赖底层操作系统的支持。在UNIX和Linux系统中,调用这个方法会引起sendfile()系统(Linux 内核2.1开始引入一个叫 sendFile 系统调用,这个系统调用可以在内核态内把数据从内核缓冲区直接复制到套接字(SOCKET)缓冲区内, 从而可以减少上下文的切换和不必要数据的复制。 具体可看这篇 https://zhuanlan.zhihu.com/p/430848775)调用,实现了数据直接从内核的读缓冲区传输到套接字缓冲区,避免了用户态(User-space) 与内核态(Kernel-space) 之间的数据拷贝。
最后数据拷贝变成只有两次 DMA COPY:
1.硬盘拷贝到内核缓冲区(DMA COPY);
2.内核缓冲区拷贝到网卡的 buf(DMA COPY)。
"零拷贝"中的"拷贝"是指操作系统在I/O操作中,将数据从一个内存区域复制到另外一个内存区域,而"零"并不是指0次复制, 更多的是指在用户态和内核态之间的复制是0次。
在不需要进行数据文件操作时,可以使用NIO的零拷贝。但如果既需要IO速度,又需要进行数据操作,则需要使用NIO的直接内存映射。
2.3 内存映射
2.3.1 Linux直接内存映射
Linux提供的mmap系统调用, 它可以将一段用户空间内存映射到内核空间, 当映射成功后, 用户对这段内存区域的修改可以直接反映到内核空间;同样地, 内核空间对这段区域的修改也直接反映用户空间。正因为有这样的映射关系, 就不需要在用户态(User-space)与内核态(Kernel-space) 之间拷贝数据, 提高了数据传输的效率,这就是内存直接映射技术。
2.3.2 NIO的直接内存映射
JDK1.4加入了NIO机制和直接内存,目的是防止Java堆和Native堆之间数据复制带来的性能损耗,此后NIO可以使用Native的方式直接在 Native堆分配内存。
image.png
直接内存的创建
在ByteBuffer有两个子类,HeapByteBuffer和DirectByteBuffer。前者是存在于JVM堆中的,后者是存在于Native堆中的。
image.png
申请堆内存
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
申请直接内存
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
使用直接内存的原因
- 对垃圾回收停顿的改善。因为full gc时,垃圾收集器会对所有分配的堆内内存进行扫描,垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。如果使用堆外内存的话,堆外内存是直接受操作系统管理。这样做的结果就是能保持一个较小的JVM堆内存,以减少垃圾收集对应用的影响。(full gc时会触发堆外空闲内存的回收。)
- 减少了数据从JVM拷贝到native堆的次数,在某些场景下可以提升程序I/O的性能。
- 可以突破JVM内存限制,操作更多的物理内存。
当直接内存不足时会触发full gc,排查full gc的时候,一定要考虑。
使用直接内存的问题
- 堆外内存难以控制,如果内存泄漏,那么很难排查(VisualVM可以通过安装插件来监控堆外内存)。
- 堆外内存只能通过序列化和反序列化来存储,保存对象速度比堆内存慢,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合。
- 直接内存的访问速度(读写方面)会快于堆内存。在申请内存空间时,堆内存速度高于直接内存。
使用场景
- 直接内存适合申请次数少,访问频繁的场合。如果内存空间需要频繁申请,则不适合直接内存。
3.IO与NIO的区别
IO流是阻塞的,NIO流是不阻塞的。
- Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
-
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
image.png
选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
NIO和IO适用场景
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,这时候用NIO处理数据可能是个很好的选择。
而如果只有少量的连接,而这些连接每次要发送大量的数据,这时候传统的IO更合适。使用哪种处理数据,需要在数据的响应等待时间和检查缓冲区数据的时间上作比较来权衡选择。
4. Linux中五种IO模型,阻塞,非阻塞,同步,异步
- Blocking IO - 阻塞IO
- NoneBlocking IO - 非阻塞IO
- IO multiplexing - IO多路复用
- signal driven IO - 信号驱动IO
- asynchronous IO - 异步IO
https://www.jianshu.com/p/b8203d46895c
由于signal driven IO在实际使用中并不常用,所以这里只讨论剩下的四种IO模型。
在讨论之前先说明一下IO发生时涉及到的对象和步骤,对于一个network IO,它会涉及到两个系统对象:
- application 调用这个IO的进程
- kernel 系统内核
那他们经历的两个交互过程是:
- 阶段1 wait for data 等待数据准备
- 阶段2 copy data from kernel to user 将数据从内核拷贝到用户进程中
之所以会有同步、异步、阻塞和非阻塞这几种说法就是根据程序在这两个阶段的处理方式不同而产生的。了解了这些背景之后,我们就分别针对四种IO模型进行讲解。
1.Blocking IO - 阻塞IO
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概如下图:
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network IO来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。
2.NoneBlockingIO - 非阻塞IO
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
从图中可以看出,当用户进程发出recvfrom这个系统调用后,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个结果(no datagram ready)。从用户进程角度讲 ,它发起一个操作后,并没有等待,而是马上就得到了一个结果。用户进程得知数据还没有准备好后,它可以每隔一段时间再次发送recvfrom操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,用户进程其实是需要不断的主动询问kernel数据好了没有。
3.IO multiplexing - IO多路复用
I/O多路复用(multiplexing)是网络编程中最常用的模型,像我们最常用的select、epoll都属于这种模型。以select为例:
看起来它与blocking I/O很相似,两个阶段都阻塞。但它与blocking I/O的一个重要区别就是它可以等待多个数据报就绪(datagram ready),即可以处理多个连接。这里的select相当于一个“代理”,调用select以后进程会被select阻塞,这时候在内核空间内select会监听指定的多个datagram (如socket连接),如果其中任意一个数据就绪了就返回。此时程序再进行数据读取操作,将数据拷贝至当前进程内。由于select可以监听多个socket,我们可以用它来处理多个连接。
在select模型中每个socket一般都设置成non-blocking,虽然等待数据阶段仍然是阻塞状态,但是它是被select调用阻塞的,而不是直接被I/O阻塞的。select底层通过轮询机制来判断每个socket读写是否就绪。
当然select也有一些缺点,比如底层轮询机制会增加开销、支持的文件描述符数量过少等。为此,Linux引入了epoll作为select的改进版本。
4.asynchronous IO - 异步IO
异步I/O在网络编程中几乎用不到,在File I/O中可能会用到:
这里面的读取操作的语义与上面的几种模型都不同。这里的读取操作(aio_read)会通知内核进行读取操作并将数据拷贝至进程中,完事后通知进程整个操作全部完成(绑定一个回调函数处理数据)。读取操作会立刻返回,程序可以进行其它的操作,所有的读取、拷贝工作都由内核去做,做完以后通知进程,进程调用绑定的回调函数来处理数据。
阻塞、非阻塞,同步和异步
阻塞和非阻塞:
- 阻塞调用会一直等待远程数据就绪再返回,即上面的 阶段1 会阻塞调用者,直到读取结束。
- 而非阻塞无论在什么情况下都会立即返回,虽然非阻塞大部分时间不会被block,但是它仍要求进程不断地去主动询问kernel是否准备好数据,也需要进程主动地再次调用recvfrom来将数据拷贝到用户内存。
同步和异步:
- 同步方法会一直阻塞进程,直到I/O操作结束,注意这里相当于上面的阶段1,阶段2都会阻塞调用者。其中 Blocking IO - 阻塞IO,Nonblocking IO - 非阻塞IO,IO multiplexing - IO多路复用,signal driven IO - 信号驱动IO 这四种IO都可以归类为同步IO。
- 而异步方法不会阻塞调用者进程,即使是从内核空间的缓冲区将数据拷贝到进程中这一操作也不会阻塞进程,拷贝完毕后内核会通知进程数据拷贝结束。
一般的IO相关API都是同步。
怎样理解阻塞非阻塞与同步异步的区别?怎样理解阻塞非阻塞与同步异步的区别? - 陈硕的回答 - 知乎
https://www.zhihu.com/question/19732473/answer/26091478
在处理 IO 的时候,阻塞和非阻塞都是同步 IO。
只有使用了特殊的 API 才是异步 IO。
https://blog.csdn.net/chen8238065/article/details/48315085
同步与异步
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
举个通俗的例子:
你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下”,然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。
而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。
阻塞与非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
还是上面的例子:
你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。
在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。
同步和异步的例子可参考AIDL的oneway 异步调用。
oneway非onewayhttp://gityuan.com/2016/09/04/binder-start-service/
你(app进程)要给远方的家人(system_server进程)邮寄一封信(transaction), 你需要通过邮寄员(Binder Driver)来完成.整个过程如下:
1.你把信交给邮寄员(BC_TRANSACTION);
2.邮寄员收到信后, 填一张单子给你作为一份回执(BR_TRANSACTION_COMPLETE). 这样你才放心知道邮递员已确定接收信, 否则就这样走了,信到底有没有交到邮递员手里都不知道,这样的通信实在太让人不省心, 长时间收不到远方家人的回信, 无法得知是在路的中途信件丢失呢,还是压根就没有交到邮递员的手里. 所以说oneway也得知道信是投递状态是否成功.
3.邮递员利用交通工具(Binder Driver),将信交给了你的家人(BR_TRANSACTION);
当你收到回执(BR_TRANSACTION_COMPLETE)时心里也不期待家人回信, 那么这便是一次oneway的通信过程.
http://gityuan.com/2016/09/04/binder-start-service/
如果你希望家人回信, 那便是非oneway的过程,在上述步骤2后并不是直接返回,而是继续等待着收到家人的回信, 经历前3个步骤之后继续执行:
1.家人收到信后, 立马写了个回信交给邮递员BC_REPLY;
2.同样,邮递员要写一个回执(BR_TRANSACTION_COMPLETE)给你家人;
3.邮递员再次利用交通工具(Binder Driver), 将回信成功交到你的手上(BR_REPLY)
这便是一次完成的非oneway通信过程.
Binder 的执行过程多数是是同步操作。换句话说,通过Binder去调用服务进程提供的接口函数,那么此函数执行结束时结果就已经产生,不涉及回调机制。比如用户使用getService 向ServiceManager 发起查询请求一函数的返回值就是查询的结果,意味若用户不需要提供额外的回调函数来接收结果。
Binder 是如何做到这一点的呢?可以想到的方法有很多, 其中常见的一种就是让调用者进程暂时挂起,直到目标进程返回结果后, Binder 再唤醒等待的进程。
因为一个transcation 通常涉及两个进程A 和B , 当A 向B 发送了请求后, B 需要一段时间来执行:
此时对A 来说就是一个"未完成的操作"一直到B 返回结果后, Binder 驱动才会再次启动A 来继续执行。
像一些系统服务调用应用进程的时候就会使用 oneway,比如 AMS 调用应用进程启动 Activity,这样就算应用进程中做了耗时的任务,也不会阻塞系统服务的运行。
出处:https://blog.csdn.net/Jason_Lee155/article/details/116637925
1.异步调用
举个例子:假如Client端调用IPlayer.start(),而且Server端的start需要执行2秒,由于定义的接口是异步的,Client端可以快速的执行IPlayer.start(),不会被Server端block住2秒。
2.同步调用
举个例子:假如Client端调用IPlayer. getVolume(),而且Server端的getVolume需要执行1秒,由于定义的接口是同步的,Client端在执行IPlayer. getVolume()的时候,会被Server端block住1秒。
3.为什么会有同步调用和异步调用?
其实一般使用异步调用的时候,Client并不需要得到Server端执行Binder服务的状态或者返回值,这时候使用异步调用,可以有效的提高Client执行的效率。
阻塞和非阻塞:
https://www.zhihu.com/question/36529093/answer/67997184
阻塞是进程休眠等待内核;非阻塞是用户进程立即返回后再轮询,或者等待信号或回调。轮询是同步非阻塞,信号或回调是异步非阻塞。
同步不一定阻塞。
来简单看看epoll
epoll 是 Linux 内核的可扩展 I/O 事件通知机制,其最大的特点就是性能优异。下图是 libevent(一个知名的异步事件处理软件库)对 select,poll,epoll ,kqueue 这几个 I/O 多路复用技术做的性能测试。
三种轮询方式select、poll、epoll。
I/O多路复用的本质是使用select,poll或者epoll函数,挂起进程,当一个或者多个I/O事件发生之后,将控制返回给用户进程。以服务器编程为例,传统的多进程(多线程)并发模型,在处理用户连接时都是开启一个新的线程或者进程去处理一个新的连接,而I/O多路复用则可以在一个进程(线程)当中同时监听多个I/O事件,也就是多个文件描述符。select、poll 和 epoll 都是 Linux API 提供的 IO 复用方式。
epoll属于同步非阻塞IO模型。 (这是站在IO模型的角度去分类的,算非阻塞)
虽然I/O多路复用的函数也是阻塞的 ,但是I/O多路复用是阻塞在select,epoll 这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。
总结:
阻塞/非阻塞, 同步/异步的概念要注意讨论的上下文。
- 在进程通信层面, 阻塞/非阻塞, 同步/异步基本是同义词, 但是需要注意区分讨论的对象是发送方还是接收方。
- 发送方阻塞/非阻塞(同步/异步)和接收方的阻塞/非阻塞(同步/异步) 是互不影响的。
- 在 IO 系统调用层面( IO system call )层面, 非阻塞 IO 系统调用 和 异步 IO 系统调用存在着一定的差别, 它们都不会阻塞进程, 但是返回结果的方式和内容有所差别, 但是都属于非阻塞系统调用( non-blocing system call )
非阻塞系统调用(non-blocking I/O system call 与 asynchronous I/O system call) 的存在可以用来实现线程级别的 I/O 并发, 与通过多进程实现的 I/O 并发相比可以减少内存消耗以及进程切换的开销。
进程阻塞为什么不占用cpu资源?
工作队列
操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。运行状态是进程获得cpu使用权,正在执行代码的状态;等待状态是阻塞状态,比如上述程序运行到recv时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态。操作系统会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。
下图中的计算机中运行着A、B、C三个进程,其中进程A执行着上述基础网络程序,一开始,这3个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行。
等待队列
当进程A执行到创建socket的语句时,操作系统会创建一个由文件系统管理的socket对象(如下图)。这个socket对象包含了发送缓冲区、接收缓冲区、等待队列等成员。等待队列是个非常重要的结构,它指向所有需要等待该socket事件的进程。
https://zhuanlan.zhihu.com/p/63179839
当程序执行到recv时,操作系统会将进程A从工作队列移动到该socket的等待队列中(如下图)。由于工作队列只剩下了进程B和C,依据进程调度,cpu会轮流执行这两个进程的程序,不会执行进程A的程序。所以进程A被阻塞,不会往下执行代码,也不会占用cpu资源。
https://zhuanlan.zhihu.com/p/63179839
ps:操作系统添加等待队列只是添加了对这个“等待中”进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下。上图为了方便说明,直接将进程挂到等待队列之下。
阻塞和唤醒进程
假设计算机中正在运行进程A和进程B,在某时刻进程A运行到了epoll_wait语句。如下图所示,内核会将进程A放入eventpoll的等待队列中,阻塞进程。
epoll_wait阻塞进程如下图:
当socket接收到数据,中断程序一方面修改rdlist,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态(如下图)。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。
epoll唤醒进程
https://zhuanlan.zhihu.com/p/64746509
epoll 的整个工作流程
https://zhuanlan.zhihu.com/p/361750240
参考:
深入理解epoll背后的原理
彻底理解Android Binder通信架构
图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的!
如果这篇文章说不清epoll的本质,那就过来掐死我吧! (1)
如果这篇文章说不清epoll的本质,那就过来掐死我吧! (3)
关于epoll的IO模型是同步异步的一次纠结过程
linux 下进程间的同步机制有哪些
怎样理解阻塞非阻塞与同步异步的区别?
关于同步,异步,阻塞,非阻塞,IOCP/epoll,select/poll,AIO ,NIO ,BIO的总结
IO多路复用的本质(select、poll、epoll)
Socket的三种轮询方式select、poll、epoll之间的区别
阻塞&非阻塞和同步&非同步
一篇文章读懂阻塞,非阻塞,同步,异步
IO多路复用的三种机制Select,Poll,Epoll
《韩顺平_循序渐进学Java零基础》第19章IO流
Java IO与NIO的区别
NIO效率高的原理之零拷贝与直接内存映射
深入浅出MappedByteBuffer
Camera Buffer Management
java nio 详_java NIO 详解
10分钟看懂, Java NIO 底层原理
一文彻底揭秘linux操作系统之「零拷贝」!
网友评论