从Unix IO模型到Java IO

[TOC]
从Unix的角度来看,无论底层使用的IO技术是基于中断驱动的CPU介入的IO技术还是DMA(现代计算机的主流IO技术)这种无需CPU介入的IO技术,都是UNIX IO系统函数的内部实现,对于应用而言由操作系统暴露出来的通用的IO系统接口已经屏蔽了底层的实现细节。所以从应用开发而言,只需要关注application和system call之间的关系。

系统调用和内核态用户态的切换

系统调用也只是一类函数,只不过这个函数比较特殊,是由操作系统实现的,底层涉及到复杂的硬件关联,是操作系统提供给上层的抽象,以降低复杂性。这也是各种系统及体系结构分层的目的。

由于系统函数的特殊性以及操作系统基于安全的考虑,用户的应用是无法直接访问硬件设备的,所以需要通过系统调用进入os层面(就是常说的陷入内核态),os是可以和底层交互的,系统函数执行硬件相关的操作,如从网卡的buffer读数据,然后把数据作为系统函数的返回值返回给caller,也就是用户,在系统函数返回时,实际上已经退出os层了(从内核态切回用户态),而所谓用户空间就是在应用程序开辟的内存空间,用于承载系统调用返回的数据。

内核态和用户态的来回切换是很消耗性能的,所以无论是应用层还是系统层(主要还是靠系统层的优化),都致力于减少系统调用的次数以及数据拷贝的次数来提升性能。

Unix的I/O模型

Unix下有五种I/O模型:

  1. Blocking I/O Model
  2. Nonblocking I/O Model
  3. I/O Multiplexing Model
  4. Signal-Driven I/O Model
  5. Asynchronous I/O Model

ps:这些都是在内核层面的IO模型,其涉及的操作都是内核中的系统函数。

  • Blocking IO是最为基础的IO模型,Unix I/O函数、RIO和ASNI C的标准IO库都是这种模型的实现,特征是blocking,无论数据是否就绪,系统函数都会阻塞直到操作完成。
  • Nonblocking的调用多了一层检查机制,如果数据未就绪,函数会立即返回不会阻塞线程,但是实际的读写操作必然是阻塞的,也就是如果数据就绪,就会阻塞读写数据。
  • I/O Multiplexing就是IO多路复用,OS提供了一个select系统函数来监听fd(文件描述符),它的优点在于一个select可以监听多个fd,当有fd就绪,select就会返回可用的fd,select是阻塞的,获取可用后的fd对其进行操作也仍是BIO过程。
  • signal-driven是利用系统的信号,当描述符就绪时内核会发送通知signal,这和Nonblocking的区别是,就绪的检查不需要主动调用读写而是通过被动等待信号,当信号到达时,使用阻塞的读写函数进行读写。
  • Asynchronous I/O即异步IO,异步的特征就是发起一个请求,然后就忘记这个请求,请求会在后台(不需要发起的线程介入)执行,执行完成后依据逻辑回调caller的函数来通知caller已经完成或者默默结束,实际的操作不需要caller介入。如发起一个异步的文件读操作,要求系统将文件A的数据读到内存a开始的数组中,如果需要读取完成的后续逻辑,主线程可以给这次请求配置一个callback函数,然后主线程执行其他操作,不再关心这个操作。系统完成指定操作后(不知道什么时候完成),会调用开始时配置的callback来“通知”主线程。这是完全异步的过程,发起请求后就不管了。

以上五种I/O模型,前四个都要主线程去主动介入数据的读写,数据的就绪和读写都是阻塞的过程,而等待完成等待可用这个过程,就是一个同步的过程,所以前4中都是同步IO;第五种的异步IO,正确的使用是主线程不会在发起请求后再介入数据的读写和后续操作(通俗的说就是发起请求之后就忘记了),如果主线程发起异步读后,还要主动去处理读出的数据(不是事先通过callback的方式在未来处理数据),比如Future式的异步,那不是真正的异步过程。

Java IO

编程语言是基于OS抽象的系统调用来完成和系统的交互的。

OIO

Java OIO是Java 1.0中的IO库,核心组件是InputStream、OutStream、Socket、File。采用的是ANSI C的标准IO库的流的抽象。实际上InputStream和OutputStream的读写操作就是基于native的C的实现。

  • UNIX IO read,OIO的read基于这个系统函数
#include <unistd.h> 
/* 
read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.
If count is zero, read() returns zero and has no other results. If count is greater than SSIZE_MAX, the result is unspecified
*/
ssize_t read(int fd, void *buf, size_t count); 
  • Java FileInputStream read
/**
 * Reads a byte of data from this input stream. This method blocks
 * if no input is yet available.
 *
 * @return     the next byte of data, or <code>-1</code> if the end of the
 *             file is reached.
 * @exception  IOException  if an I/O error occurs.
 */
private native int read0() throws IOException;

/**
 * Reads a subarray as a sequence of bytes.
 * @param b the data to be written
 * @param off the start offset in the data
 * @param len the number of bytes that are written
 * @exception IOException If an I/O error has occurred.
 */
private native int readBytes(byte b[], int off, int len) throws IOException;

每次调用read()方法都会陷入内核,调用系统的read函数,所以考虑到性能出现类用户缓冲区,一次多读一些数据(count>1)到用户的缓冲区,然后操作用户缓冲区的数据。

在OIO方式下的数据流动,以socket读写为例,socket读:socket缓冲区--->kernel缓冲区--->用户缓冲区,socket写:用户缓冲区--->kernel缓冲区--->Socket缓冲区,很明显,每次系统调用都会有2次数据拷贝过程。
虽然采用用户缓冲区的方式可以减少系统调用次数,但是每次调用的数据拷贝次数还是一样,对于大量的数据,来回的拷贝也很消耗性能,所以出现了内存映射和零拷贝技术。这些技术是OS层的,Java NIO中提供了语言层面的封装。

所以Java OIO的核心是:面向流、阻塞过程、标准IO。

NIO

Java NIO是NonBlocking 模型和IO multiplexing模型的实现,对应支持非阻塞的Channel和Selector选择器。核心组件:Buffer、Channel、Selector。
前面总结UNIX IO模型时说明过,非阻塞模型除了在数据未就绪时的读写为非阻塞外,真正的读写依然是阻塞的,所以其内部依旧是OIO的过程;IO多路复用是借助OS的select函数来完成对文件描述符的事件监听,事件触发(就绪)后的读写也依然是OIO的过程。那么NIO的优势在于何处。

  1. 面向缓冲区而不是流
  2. 借助select,只需要单个线程就可以处理多个fd(NIO抽象为channel,要求channel为非阻塞)

面向缓冲区而不是流是什么意思

Java NIO引入一个Buffer类,主要的堆内缓冲区"HeapByteBuffer"实际上就是对byte数组的封装。在OIO中,都是直接对流进行read或write操作,在NIO使用双向的channel对datasource进行了抽象(实际上在ANSI C的标准IO的流的抽象就是全双工的,由于socket的限制,标准IO中也不支持全双工,Java中这样读写分离的设计可能是为了简化使用,统一读和写分别使用了一个流吧),所有读channel的读写都是直接和Buffer交互,即读数据到Buffer中,然后操作Buffer,或者是put数据到buffer中,然后写buffer到channel中,数据的操作面向的是Buffer,而不是底层的流。这样的好处是不会直接暴露底层的API和细节。Buffer就是封装了缓冲区数组,以及支持在数组中前后移动的方法的封装,本质上的读写依然是标准IO的操作。可以参考Java NIO vs. IO

Selector

借助系统函数select或者epoll来通过一个线程管理多个fd,对于服务端来说可算是解放了每个socket一个线程的简陋操作。虽然select过程依然是阻塞的,但是可以只需要阻塞一个线程就可以监听多个fd,另外其基于事件的监听给fd的处理提供了很大的便利性。selector需要其监听的channel是非阻塞的。

Channel非阻塞的意义

对于同步IO来说,非阻塞channel的意义主要在于select,如果不使用select,对channel的读写就需要轮询,和阻塞等待的差别不大。

内存映射、直接缓冲区和零拷贝技术

MappedByteBuffer和DirectByteBuffer提供了内存映射和直接缓冲区的封装实现,另外FileChannel的transferTo、transFrom在特定的平台上(Linux kernel2.4及以后,支持gather operation)还支持零拷贝技术(基于Linux的sendfile系统函数)。

AIO

也称Java NIO2,是NIO的补充,主要新增了对异步的支持。AIO的设计一般有未来式(使用Future)和回调式(配置callback方法),因为future的get()实际上是阻塞方法,所以未来式的AIO不是真正意义的异步。

  • AsynchronousFileChannel by future
AsynchronousFileChannel asynchronousFileChannel = ...;
ByteBuffer buffer = ...;
Future<Integer> future = asynchronousFileChannel.read(buffer, position);
// 阻塞,等待完成
future.get();
// ... 后续继续处理数据,所以这个形式实际上是同步的操作。
  • AsynchronousFileChannel by callback(真正的异步)
//  配置回调函数
asynchronousFileChannel.read(buffer, 0, null, new CompletionHandler<Integer, Object>() {
    // 动作完成后回调
    @Override
    public void completed(Integer result, Object attachment) {
        if (result != -1) {
            buffer.flip();
            System.out.println(new String(buffer.array(), 0, buffer.limit(), StandardCharsets.UTF_8));
        }
    }

    // 发生异常时回调 
    @Override
    public void failed(Throwable exc, Object attachment) {
        exc.printStackTrace();
    }
});

除了异步文件通道AsynchronousFileChannel,网络模块也有对应的AsynchronousSocketChannel和AsynchronousServerSocketChannel,他们都支持未来式和回调式的操作。

Reference

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    JackChen1024阅读 7,534评论 1 143
  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    编码前线阅读 2,258评论 0 5
  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    zhisheng_blog阅读 1,112评论 0 7
  • 原文链接 攻破JAVA NIO技术壁垒 现在使用NIO的场景越来越多,很多网上的技术框架或多或少的使用NIO技...
    阳光的技术小栈阅读 499评论 2 3
  • 最近小五家里多了一只小狗,却因为是流浪狗,主人总是动不动对它大呼小叫的,并且总是拿剩饭剩菜喂它。一开始这只小狗还会...
    死宅文化研究所阅读 650评论 0 2