Linux的IO

操作系统的IO分为:网络IO、磁盘IO、内存IO等,我们通常考虑的是网络IO和磁盘IO。

网络IO

网络IO本质是socket读取

sendfile

普通的网络传输步骤是:操作系统将数据从磁盘复制到操作系统内核页缓存中,应用将数据从内核缓存复制到应用缓存中,然后应用将数据写回内核的Socket缓存中,最后操作系统将数据从Socket缓存区复制到网卡缓存,然后将其通过网络发出。而通过sendfile则可以避免多次数据复制,操作系统可以将数据从内核页缓存中复制到网卡缓存。

java.nio.channels.FileChannel中的transferTo方法由native方法transferTo0来实现,它依赖底层操作系统的sendfile这个system call实现了零拷贝。(数据直接从内核的读缓冲区传输到Socket缓冲区,避免了用户态于内核态间的数据拷贝,将用户态与内核态间的上下文切换次数由4次减少到2次)参考:NIO的零拷贝与直接内存映射

磁盘IO

磁盘IO的访问方式如下:

缓存IO(标准IO)

大多数文件系统的IO操作默认都是标准IO,在Linux的标准IO机制中,数据先从磁盘复制到内核空间缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间

当一个进程准备读取磁盘上的文件内容时,操作系统会先查看待读取的数据所在的页(page)是否在页缓存(pagecache)中,如果存在(命中)则直接返回数据,从而避免了对物理磁盘的 I/O 操作;如果没有命中,则操作系统会向磁盘发起读取请求并将读取的数据页从磁盘拷贝到页缓存,之后再将数据拷贝到应用程序的用户空间返回给进程。(虽然多一次内存拷贝,但因为内存相对磁盘是高速设备,即使多拷贝也比真正读一次磁盘要快)

同样,如果一个进程需要将数据写入磁盘,那么操作系统也会检测数据对应的页是否在页缓存中,如果不存在,则会先在页缓存中添加相应的页,最后将数据写入对应的页(从用户地址空间的应用缓存复制到内核地址空间的页缓存)。被修改过后的页也就变成了脏页,操作系统会通过flush线程定期把脏页中的数据写入磁盘。在实际应用中,对于重要的不可丢失的数据,我们应该采用同步写机制。在应用程序中使用 sync、fsync、msync 等系统调用,使内核立刻将相应的数据写回到磁盘。

页缓存是操作系统实现的一种主要的磁盘缓存,以此用来减少对磁盘 I/O 的操作。具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问。为了弥补性能上的差异,现代操作系统越来越“激进地”将内存作为磁盘缓存,甚至会非常乐意将所有可用的内存用作磁盘缓存,这样当内存回收时也几乎没有性能损失,所有对于磁盘的读写也将经由统一的缓存。

直接IO

直接IO是应用程序直接访问磁盘数据,而不经过内核缓冲区,这样做的目的是减少从内核缓冲区到用户程序缓存的数据复制。比如说数据库管理系统这类应用,它们更倾向于选择它们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。

直接IO的缺点是如果访问的数据不在用户地址空间的应用程序缓存中,那么每次数据都会直接从磁盘加载,这种直接加载比较缓慢。通常直接IO与异步IO结合使用,会得到比较好的性能。

内存映射

内存映射是将内核空间和用户空间的虚拟地址映射到同一个物理地址,用户对这段内存数据的修改操作会直接由操作系统保证异步刷盘到硬盘的文件中,在内存映射的过程中可以省略中间的很多IO环节,而这个刷盘过程即使应用程序崩溃也能够完成。

Kafka中的PageCache和SendFile

PageCache

PageCache是系统级别的缓存,它把尽可能多的空闲内存当作磁盘缓存使用来进一步提高IO效率,同时当其他进程申请内存,回收PageCache的代价也很小。通过使用PageCache,Kafka同时可以避免在JVM内部缓存数据避免不必要的GC、以及内存空间占用。对于In-Process Cache,如果Kafka重启,它会失效,而操作系统管理的PageCache依然可以继续使用

对应到Kafka生产和消费消息中:Producer把消息发到broker后,数据并不是直接落入磁盘,而是先进入PageCache(mmap)。PageCache中的数据被内核中的处理线程采用同步或异步的方式写回到磁盘。Consumer消费消息时,也是会先从PageCache获取消息,获取不到才会去磁盘读取,并且会预读出一些相邻的块放入PageCache,以方便下一次读取。这意味着:(handoff) 如果producer的生产速率与consumer的消费速率相差不大,那么几乎只靠对broker的PageCache的读写就能完成整个生产和消费过程,磁盘访问非常少。(而消费历史数据就不得不从磁盘重新加载到PageCache,并且会因此污染当前实时数据的PageCache)

SendFile

Customer从broker读取数据,采用SendFile(对应FileChannel.transferTo() API)进行网络发送,避免了从内核空间的页缓存读入用户空间缓冲区,再写回到内核空间的多余操作。

free命令中的page cache和buffer cache

  • -/+ buffers/cache行:表示物理内存的缓存统计(如果缓存的值很大,说明缓存住的文件数很多,如果频繁访问到的文件都能被缓存住,那么磁盘的读IO必定会非常小)。
  • buffers列:表示当前的块缓存(buffer cache)占用量。
  • cached列:表示当前的页缓存(page cache)占用量。

磁盘的操作有逻辑级(文件系统) 和物理级(磁盘块),假设我们通过文件系统操作文件,那么文件将被缓存到page cache,如果需要刷新文件的时候,page cache将交给buffer cache去完成,因为buffer cache就是缓存磁盘块的。也就是说,直接去操作文件就是page cache缓存,用dd等命令直接操作磁盘块,直接对磁盘进行操作的数据才是buffer cache缓存的东西

buffer cache用于缓存块设备的块数据,page cache用于缓存文件的页数据,它们的目的都是为了加速数据IO(写数据时先写到缓存,读数据时先尝试读取缓存 / 操作系统总是积极地将所有空闲内存都用作page/buffer cache,当内存不够用时通过LRU等算法淘汰缓存页)。

buffer cache是对物理磁盘上的一个磁盘块进行的缓冲,其大小通常为1k,磁盘块也是磁盘的组织单位,设立buffer cache的目的是为在程序多次访问同一磁盘块时,减少访问时间。page cache是由好几个磁盘块构成,大小通常为4k,在64位系统上为8k,构成的几个磁盘块在物理磁盘上不一定连续,文件的组织单位为一页, 也就是一个page cache大小,文件读取是由外存上不连续的几个磁盘块,到buffer cache,然后组成page cache再供给应用程序。

在Linux 2.4版本的内核之前,page cache与buffer cache是完全分离的。但是,块设备大多是磁盘,磁盘上的数据又大多通过文件系统来组织,这种设计导致很多数据被缓存了两次,浪费内存。所以在2.4版本内核之后,两块缓存近似融合在了一起:如果一个文件的页加载到了page cache,那么同时buffer cache只需要维护块指向页的指针就可以了。只有那些没有文件表示的块,或者绕过了文件系统直接操作(如dd命令)的块,才会真正放到buffer cache里。

NIO的堆外内存和内存映射

直接缓冲区 [DirectByteBuffer]

ByteBuffer的子类MappedByteBuffer的子类,操作模式与普通的ByteBuffer一致,但其使用的这段连续内存是调用unsafe的native方法分配的堆外内存。优点是:可以突破JVM内存限制 / 避免堆外内存和堆内内存间的数据拷贝提升某些场景的I/O性能 / 改善GC停顿情况 ; 缺点是:容易造成堆外内存泄露 / 堆外内存只能通过序列化和反序列化来存储,保存对象速度比堆内内存慢,不适合存储很复杂的对象。此外ByteBuffer实现了DirectBuffer接口,维护一个Cleaner对象来完成内存回收。因此它既可以通过Full GC来回收内存,也可以调用clean()方法来进行回收。

内存映射缓冲区 [MappedByteBuffer]

使用的这段连续内存是内存映射的内存,因此对于这块缓冲区的数据修改会同步到对应的文件中。可以通过FileChannel提供了map方法来把文件映射为内存对象(返回MappedByteBuffer对象) MappedByteBuffer申请的也是直接内存,不受Minor GC控制,只能在发生Full GC时被回收。

DMA( Direct Memory Access)

传统的内存访问需要通过CPU的调度来完成;而DMZ可以让硬件子系统直接访问系统主内存,而不用依赖CPU;显卡、声卡、网卡等硬件都支持DMZ。

零拷贝

用来解决操作系统在处理IO操作时频繁复制数据的问题,主要实现方式有:sendfile、mmap+write等。

Reference

网络IO和磁盘IO详解

Kafka中的零拷贝

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值