文件 IO

应用读写文件基本流程

在这里插入图片描述

  • 用户缓冲区:在这里可以理解为 byte b[] 在 JVM 堆内存占用的空间

1. 读文件

  用户程序通过内核程序提供的读取文件 api 发起对某个文件读取。此时程序从用户态切换到内核态,用户程序处于阻塞状态,由于读取的内容还不在内核缓冲区中,导致触发 OS 缺页中断异常。然后由 OS 负责发起对磁盘文件的数据读取,这个读取动作交由 DMA 去完成;读取到数据后,先存放在 OS 内核的主存空间(PageCache);然后 OS 再将数据拷贝一份至用户进程空间的主存 ByteBuffer 中。此时程序由内核态切换至用户态继续运行程序。程序将 ByteBuffer 中的内容读取到本地变量中,即完成文件数据读取工作。

DMA 技术?

  简单理解就是,在进行 I/O 设备和内核内存的数据传输时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。

时序图如下

在这里插入图片描述

2. 写文件

  用户程序通过内核程序提供的读取文件 api 发起对某个文件写入磁盘。此时程序从用户态切换到内核态,用户程序处于阻塞状态,由 OS 负责发起对磁盘文件的数据写入。用户写入数据后,并不是直接写到磁盘的,而是先写到 ByteBuffer 中,然后再提交到 PageCache 中,最后由操作系统决定何时写入磁盘。数据写入 PageCache 中后,此时程序由内核态切换至用户态继续运行。

  PageCache 中的数据最终写入磁盘是由操作系统异步提交至磁盘的。一般是定时或 PageCache 满了的时候写入。如果用户程序通过调用 flush() 方法强制写入,则操作系统也会服从这个命令。立即将数据写入磁盘然后由内核态切换回用户态继续运行程序。但是这样做会损失性能,但可以确切的知道数据是否已经写入磁盘了。
在这里插入图片描述

3.文件在磁盘上的存放

文件内容在磁盘上怎么存放的呢?

文件再磁盘上的存放

// 还没想好怎么写,TODO

4. 顺序 IO 和 随机 IO

  其实所有的写操作在磁盘上都是随机写,这是由现代操作系统的文件系统设计方案决定的。当用户程序写入数据到page cache后,操作系统决定何时写入磁盘中的某个空闲块,所有的文件都不是连续分配的,都是以块为单位分散存储在磁盘上。

  但有一种情况例外,mmap() 函数,它将磁盘物理空间和page cache内存空间进行映射,如果磁盘有连续的物理空间,它将预占用大小的磁盘连续物理空间,这个被映射的磁盘物理空间不会被其他文件写入数据;当我们把数据写入page cache,操作系统将会把数据连续写入这个映射好的磁盘连续物理空间。

mmap() 函数,我们后面再讲

5. 缓存 IO 和 直接 IO

  使用了page cache技术的 IO,叫缓存 IO;与之相对应不使用page cache技术的 IO,叫直接 IO。

Page Cache 有什么作用?

  缓存最近被访问的数据:读写磁盘相比读写内存的速度慢太多了,所以我们应该想办法把「读写磁盘」替换成「读写内存」。于是,我们会通过 DMA 把磁盘里的数据搬运到内核page cache,这样就可以用读内存替换读磁盘。

当然,这个数据被多次读加入page cache 才有效果,不然只读一次,那还不如从磁盘上拷贝到用户缓冲区,少了一次 CPU 拷贝过程。

  预读功能:page cache有「预读功能」。假设read()方法每次只会读 32 KB 的字节,虽然read() 刚开始只会读 0 ~ 32 KB 的字节,但内核会把其后面的 32~64 KB 也读取到 page cache ,这样后面读取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出page cache 前,进程读取到它了,收益就非常大。

Page Cache 一定好?

  通过上面分析,使用 page cache优点这么多,那么缓存 IO 一定比 直接 IO 要好。那当然不是,在数据不需要被访问,只需要一次写或者一次读,那么可以不用 page cache 减少 CPU 拷贝消耗;传输大文件的时候,由于大文件难以命中 page cache缓存,而且会占满 PageCache 导致「热点」文件无法充分利用缓存,从而增大了性能开销,因此,这时应该使用直接 I/O。

暂未发现 Java 使用直接 IO 的 API

6. 零拷贝(Zero-copy)和 mmap

  零拷贝( zero-copy )技术可以有效地改善数据传输的性能,在内核驱动程序(比如网络堆栈或者磁盘存储驱动程序)处理 I/O 数据的时候,零拷贝技术可以在某种程度上减少甚至完全避免不必要 CPU 数据拷贝操作。

实现零拷贝读写方式

  • mmap()
  • sendfile()

6.1 mmap()

  我们知道,read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用 mmap() 替换 read() 系统调用函数。

  mmap() 系统调用函数会把文件磁盘地址「映射」到内核缓存区(page cache),而内核缓存区会 「映射」到用户空间(虚拟地址)。这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝。

注意,这里用户空间(虚拟地址)不是直接映射到文件磁盘地址,而是文件对应的 page cache,用户虚拟地址一般是和内存物理地址「映射」的(代表这块内存属于用户空间)。如果使用内存映射技术,则用户虚拟地址可以和内核内存地址「映射」,代表这块内核内存共享给用户空间。
根据维基百科给出的定义:在大多数操作系统中,映射的内存区域实际上是内核的 page cache,这意味着不需要在用户空间创建副本。多个进程之间也可以通过同时映射 page cache,来进行进程通信)

在这里插入图片描述

mmap() 函数简介

void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)
  • start:要映射到的内存区域的起始地址,通常都是用NULL(NULL即为0)。NULL表示由内核来指定该内存地址
  • offset:以文件开始处的偏移量, 必须是分页大小的整数倍, 通常为0, 表示从文件头开始映射
  • length:映射多大空间
  • prot: 映射区的保护方式(PROT_EXEC: 映射区可被执行、PROT_READ: 映射区可被读取、PROT_WRITE: 映射区可被写入、PROT_NONE: 映射区不能存取)
  • flags: 映射区的特性
  • fd:文件描述符(由open函数返回)

mmap() + write()

  以读取磁盘数据,通过网卡发送给其他服务器为例,来看看 mmap() 是怎样的。

在这里插入图片描述

  应用程序调用 mmap()函数后,用户虚拟地址和 page cache建立映射关系。之后应用程序发起读数据操作,DMA 会把磁盘数据拷贝到 page cache,CPU 不需要把数据再拷贝到用户缓存区了,因为应用已经能直接访问 page cache

  应用进程再调用 write(),CPU 将数据拷贝至 Socket 发送缓冲区,最后,DMA 把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区,网卡发送数据。

  从上面流程看,mmap()比传统缓冲 IO,少了一次 CPU 数据拷贝的过程。但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换。

Java 简用 Mmap()

	public static void fileWrite(String filePath) {
        try {
            RandomAccessFile accessFile = new RandomAccessFile(new File(filePath), "rw");
            FileChannel fileChannel = accessFile.getChannel();         
            //  操作系统提供的一个内存映射的机制的类
            MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, (long) 1024);
            map.put(("hello").getBytes());
        } catch (IOException e) {
        } finally {
        }
    }

6.2 sendfile()

size_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
  • out_fd:源文件描述符,也是被拷贝的文件
  • in_fd:目标文件描述符,也就是 socket
  • offset:偏移量
  • count:内容大小

  它可以替代前面的 read()write() 这两个系统调用,这样就可以减少一次系统调用;其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里。

相比mmap(),它不需要将数据拷贝到线程栈,应用程序也就无法对数据进行加工

在这里插入图片描述

  但是这还不是真正的零拷贝技术,如果网卡支持 SG-DMAThe Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。

  你可以在你的 Linux 系统通过下面这个命令,查看网卡是否支持 scatter-gather 特性:

$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on
  • 通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
  • 缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝

在这里插入图片描述

使用零拷贝技术的项目
  事实上,Kafka 这个开源项目,就利用了「零拷贝」技术,从而大幅提升了 I/O 的吞吐率,这也是 Kafka 在处理海量数据为什么这么快的原因之一。

  如果你追溯 Kafka 文件传输的代码,你会发现,最终它调用了 Java NIO 库里的 transferTo方法:

@Overridepublic 
long transferFrom(FileChannel fileChannel, long position, long count) throws IOException { 
    return fileChannel.transferTo(position, count, socketChannel);
}

  如果 Linux 系统支持 sendfile() 系统调用,那么 transferTo() 实际上最后就会使用到 sendfile() 系统调用函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值