系列文章
第一篇 Java NIO(一)I/O简介
第一篇 Java NIO(二)BIO
第一篇 Java NIO(三)NIO之Buffer
第一篇 Java NIO(四)NIO之Channel
第一篇 Java NIO(五)NIO之Selector
第一篇 Java NIO(六)Netty
Java NIO(一)IO简介
本文章是对《Netty 4核心原理于手写RPC框架实战》该书研读的总结和记录,如有侵权,望及时联系
目录
前言
高级编程语言都是对于计算机操作系统底层的一次再封装,使其能够为人们所理解和运用,但是封装必定会导致不可见性,这就会使得从上层(高级编程语言本身)来理解相关知识变得困难。因此本文从根本从发,从操作系统底层开始摸索I/O的相关知识,以下内容纯属个人的学习认知,如有错误,欢迎大家雅正。一、I/O
1 什么是I/O
我们都知道,在UNIX的世界里,一切皆是文件,而文件是什么呢?文件就是一串二进制流,无论是Socket,还是FIFO,管道、终端、设备,对于计算机来说,一切都是文件,都是流。在信息交换的过程中,计算机都是对这些文件流进行数据的收发操作,简称为I/O操作(Input/Output)。从流中读取数据,系统调用read,往流中写数据,系统调用write。
可是计算机是如何分辨如此众多的文件流,又是如何知道需要操作哪一个文件流的呢?实际上这是由操作系统内核创建的文件描述符(fd,file description)来标识的。一个fd是一个非负整数。所以对文件流的操作就转换为对该fd进行操作。fd是对底层抽象文件流的一种具化形式
2 I/O的交互过程
我们知道,在CPU的所有指令中,有些指令是比较危险的,一旦错用,很可能导致系统崩溃,如清理内存,设置时钟等。因此出于系统安全、稳定等考虑,操作系统采用分级思想,将CPU特权分为多个等级,(Ring0~Ring3)并将将内存地址空间划分为用户空间和内核空间,用户空间用于运行用户自我的应用程序,内核空间由操作系统的内核来使用。(Linux只使用了Ring0和Ring3,Ring3供用户程序使用,此时进程为用户态;Ring0供内核代码使用,此时进程为内核态)对于一些级别比较重要的(危险的)指令,只能运行在内核态中,由内核来执行。
有了上述的知识储备之后,我们来看下I/O的交互过程,由于用户态和内核态的划分,I/O的交互过程可以分为两个阶段。首先是经过内核空间,由操作系统处理;紧接着是到用户空间,交由应用程序处理,交互流程如下图所示:
注:个人理解,其中的磁盘指的应该是外界设备,包括磁盘和网卡。
操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者是不能简单的通过指针来传递数据的。因为Linux使用的虚拟内存的机制,必须通过系统调用请求Kernel(内核)来协助完成I/O操作。因次具体的I/O交互过程可以这么来理解,以网络输入I/O为例说明:
- 等待网络数据到达网卡,然后将数据读取到内核缓冲区
- 从内核缓冲区复制数据,然后拷贝到用户空间
3 I/O的分类
I/O有内存I/O、网络I/O、磁盘I/O,通常我们所说的I/O指的是后两者,网络和磁盘I/O,下图是I/O通信过程的调度过程:
二、五种I/O通信模型
1.阻塞式I/O模型
阻塞I/O通信模型的通信过程如下所示:
当用户进程调用了recvfrom这个系统调用之后,内核就开始了I/O的第一阶段:数据准备。很多时候,数据在一开始还没有到达或者没有收到一个完整的网络包,这个时候内核就需要等待数据到来,表现在用户进程这边就是整个进程被阻塞,而当数据准备好后,内核就会将数据从内核拷贝到用户内存,然后返回结果,用户进程这时结束阻塞状态,进行数据处理
总结如下:
特 点 | 在I/O执行的两个阶段(等待数据和拷贝数据)都被阻塞 |
典型应用 | 阻塞Socket,Java BIO |
优 点 |
|
缺 点 |
|
2.非阻塞式I/O模型
非阻塞I/O通信模型的通信过程如下图所示:
当用户进程发出read操作时,如果内核中数据还没准备好,它并不会阻塞进程,而是立即返回一个error。从用户角度讲,它在发起read操作后可以立即拿到结果,用户判断结果是error是,它就知道数据没有准备好。于是它可以选择再次发送read操作,一旦内核中数据准备好了,那么在再次收到用户进程的系统调用后,内核会马上将数据拷贝到用户内存,供应用程序消费使用。
总结如下:
特 点 | 用户进程需要不断地注定询问Kernel(内核)数据准备好了没有 |
典型应用 | Socket设置NON_BLOCK |
优 点 | 实现难度低,开发应用相对阻塞式I/O模型较难 |
缺 点 |
|
3.多路复用I/O模型
多路复用I/O通信模型的通信过程如下图所示:
多个进程的I/O可以注册到一个复用器(Selector)上,当用户进程调用该Selector时,Selector会监听注册进来的所有I/O,如果Selector监听的所有I/O在内核缓冲区都没有数据可读,select调度进程会被阻塞,而当任一I/O在内核缓冲区有可读数据时,select调用就会返回,而后select调用进程可以自己或者通知另外的进程(注册进程)再次发起读取I/O,读取内核中准备好的数据。这样多个进程注册I/O后,只有select调用进程会被阻塞
事实上,多路复用模型和阻塞模型并没有太大的不同,严格来说,甚至更差一些。一方面是因为它需要两次系统调用,另外一方面,虽然只有调用select的进程会被阻塞住以外,其他注册的I/O进程,我个人认为多路复用相对阻塞I/O模型来说,仅仅是取消了I/O第一阶段,没有数据准备好时候的阻塞,并把后续拷贝数据包的阻塞延后了而已,而且从某种意义上来说,多路复用在select调用的时候,如果注册在Selector上的进程强依赖该I/O数据,那么这段时间,该进程未阻塞和阻塞并没有太大的区别。因此说,多路复用它解决的最本质问题并不是单个连接能处理的更快,而是可以处理更多的连接。所以在某种情况下,多路复用的模型并不一定比阻塞式的要好。还是需要开发者具体情况具体处理。
总结如下:
特 点 | 对于每一个Socket,一般都设置成非阻塞,但是整个用户进程其实一直是阻塞的,只不过进程不是被Socket I/O阻塞,而是被select调用阻塞的 |
典型应用 | Java NIO Nginx(epoll、poll、select) |
优 点 |
|
缺 点 | 实现和开发难度比较大 |
4.信号驱动I/O模型
多路复用I/O通信模型的通信过程如下图所示:
信号驱动模型是指进程预先告知内核,向内核注册一个信号处理函数,然后用户进程返回不阻塞,当内核数据准备就绪的时候,会发送一个信号给进程,用户进程便在信号处理函数中调用I/O读取数据,实际上,整个I/O过程中,数据从内核拷贝到用户空间阶段还是阻塞的,信号驱动I/O并没有做到真正的异步,因为在数据准备就绪后,还是得由进程来完成剩下的I/O操作
总结如下:
特 点 | 并不属于异步,属于伪异步,实际中不常使用 |
典型应用 | 应用场景较少 |
优 点 | / |
缺 点 | 实现和开发难度大 |
5.异步I/O模型
多路复用I/O通信模型的通信过程如下图所示:
从用户进程监督来说,在发起aio_read操作后,给内核传递来和read相同的描述符、缓冲区指针(为了接受数据)、缓冲区大小三个参数,以及文件偏移,告诉内核当整个I/O操作完成后,如何通知我们立刻开始去做其他的事情;而从内核角度来说,内核在收到一个aio_read的操作指令后,内核会立即返回,以至于不会阻塞进程,然后内核等待数据准备好,然后内核将数据拷贝的用户内存,完成后给用户进程发送一个信号,告诉用户进程aio_read操作完成(依据之前用户进程传递的参数)
这里需要注意到的是,异步模型和信号驱动模型的不同在于,异步模型中整个I/O操作是需要用户进程做什么的(无论是第一阶段数据等待还是第二阶段数据拷贝),内核帮用户进程做完整个I/O之后,通知用户进程进行数据的消费处理。而信号驱动模型是内核通知到用户进程,数据已经准备好,用户进程可以去内核缓冲区去拷贝数据过来进行消费处理了
总结如下:
特 点 | 真正实现了异步I/O,是五种模型中唯一一个异步模型 |
典型应用 | Java7 AIO 高性能服务器应用 |
优 点 |
|
缺 点 |
|
三、同步、异步、阻塞、非阻塞
最后,我们来看一下I/O学习中最容易混淆的概念,同步异步、阻塞非阻塞
1 同步VS异步
同步和异步指的是CPU时间片上的利用。主要看请求发起方对消息结果的获取是主动发起的还是被动通知的,
- 同步:请求方主动发起
- 异步:请求方被动通知
2 阻塞非阻塞
阻塞非阻塞在计算机里面通常是针对I/O操作来说的,这两个概念的区别在于,在调用了一个函数后,在等待这个函数返回结果之前,当前的进程(线程)是挂起状态还是运行状态。
- 阻塞:处于挂起状态
- 非阻塞:处于运行状态
总结
以上就是本人对于IO操作系统底层学习的一个总结,分了三个部分总结,I/O的预热知识、I/O的五种通信模型、
I/O的最易混淆的一些概念。希望对大家能有所帮助,如有问题,欢迎大家雅正或联系我,谢谢!