文件I/O_03PageCache和Mmap

1.什么是PageCache

(1)假如没有PageCache:

CPU如果要访问外部磁盘上的文件,由于cpu可以直接访问的存储器是内存。所以磁盘的文件内容要先拷贝到内存上(DMA技术),cup才能读取到。cup访问内存是很快的高速,内存拷贝磁盘文件相对是慢的。(DMA, Direct Memory Access, 存储器直接访问, 允许在外部设备和存储器之间直接读写数据,既不通过CPU,也不需要CPU干预)

要优化慢这个问题,就需要提前把磁盘数据先读到内存用做缓存。这个在内存上建立的缓存就是PageCache,也叫页缓存。

(2)PageCache作用

有了pageCache,cpu要读的数据如果缓存命中,那速度就会快很多。就类似我们好读mysql的数据,如果提前放到redis上面了,速度就会快得多。

pagecache作用就是:缓存 I/O ,减少读盘的次数,从而提高性能

2.PageCache管理

从前面可以看到,pagecache是放在内存上的,内存同时还有linux内核kernet,app进程这些东西。内存又是有限的,那怎么管理pagecache分配,读取,写入,淘汰就需要有个程序来控制,这个程序就是内核kernet,pagecache的管理是有内核来维护的。

实际上在IO整个过程,其他地方还有缓存的概念

(1)app(应用程序)中的缓存区:buffer

为什么使用buffer会比不使用快?

buffer能一次读取磁盘8k内容先缓存,可以大大减少内核的io次数

Java中BufferedReader,BufferedWriter要比FileReader 和 FileWriter高效

答案就是这里,不用每次读写都调用内核的read/write,而是凑齐8190字节后再调用一次系统的read/write,本质是较少内核调用磁盘io的次数。

(2)kernel缓存区pagecache

应用程序在调用系统调用如read(),writer(),就会触发中断信号,此时用户态切换到内核态,内核执行完,数据返回了,cup又回来继续执行应用程序。

读数据:缓存磁盘热数据,命中直接返回,较少到磁盘次数

写数据:平衡高速设备和低速设备之间的速度

(3)磁盘上面的磁盘缓存区

磁盘的缓冲区是硬盘与外部总线交换数据的场所。 硬盘的读数据的过程是将磁信号转化为电信号后,通过缓冲区一次次地填充与清空,再填充,再清空,一步步按照PCI总线的周期送出

3.pageCache读写回收

(1)读cache:

当用户发起一个读请求(假如read()请求),首先操作系统执行中断,从用户态切换到内核态,内核开始调read()方法。内核会先检查目标数据在pagecache中是否有缓存过,有缓存命中(cache hit)直接返回。没有,缓存穿透,去磁盘中读取,然后把目标数据返回并缓存到pagecache。下次需要同样目标数据就可以在缓存中直接读取。

(2)写cache:

用户发起一个写请求(write()),中断到内核。内核会把要写的数据先写入到pagecache.这时内核并不会马上落盘。而且将page标记为dirty(赃页),并将其加入dirty_list中。内核会周期性的将dirty_list的数据落盘(Flusher Threads)。完成了这一步后cache和磁盘中的数据才会最终一致。

落盘策略(Flusher线程群):

1.用户进程调用sync() 和 fsync()系统调用

2.空闲内存低于特定的阈值(threshold)

3.Dirty数据在内存中驻留的时间超过一个特定的阈值

相关参数dity:

sysctl -a | grep dirty   
vm.dirty_background_bytes = 0       #和 dirty_background_ratio、dirty_ratio 表示同样意义的不同单位的表示
vm.dirty_background_ratio = 5       #表示当脏页占总内存的的百分比超过这个值时,后台线程开始刷新脏页。这个值如果设置得太小,可能不能很好地利用内存加速文件操作。如果设置得太大,则会周期性地出现一个写 IO 的峰值。
vm.dirty_bytes = 0                  #和 dirty_background_ratio、dirty_ratio 表示同样意义的不同单位的表示
vm.dirty_expire_centisecs = 3000    #示脏数据多久会被刷新到磁盘上。这里的3000表示 30秒
vm.dirty_ratio = 10                 #当脏页占用的内存百分比超过此值时,内核会阻塞掉写操作,并开始刷新脏页
vm.dirty_writeback_centisecs = 500  #表示多久唤醒一次刷新脏页的后台线程。这里的500表示5秒唤醒一次。

page cache数量也是有限的,不可能无限增加,那如何释放回收?

(3)cache回收:

内核使用的是LRU算法Two-List策略(实际上就是怎么回收算法最优的一个机制,其他缓存同样也会面临这个问题,也会有类似的策略)

LRU算法:least rencently used (最近最少使用),就是要释放最近最少使用的

Two-List策略:两个list,实际上是维护两个队列(active:活跃的和inactive:不活跃),两个链表。那很明显要干掉不活跃的(加尾砍头)。那这2个表怎么维护?

1.首次缓存的数据page会加入到inactive list中,inactive list中的page被再次访问,则移入active list.

2.如果active list数量远大于inactive list,那么active list头部的page会被移入inactive list,从而实现两个list平衡

(4)缓存淘汰策略:pageCache VS redis

4.mmap

(1)定义:

一种内存映射文件的方法,mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上(PageCache).

(2)作用:

mmap操作提供了一种机制,让用户程序直接访问设备内存。

什么意思呢,用户空间和内核空间有隔离性,内核才能访问内核空间。用户要直取内核pagecache数据,做不到。需要cup先要从内核的pagecache拷贝一份到用户的缓存区,用户才能访问。有了这个映射之后,用户可以直接操作内核空间的缓存数据。剩一步cup拷贝。这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。

(3)虚拟内存地址和物理地址

虚拟内存地址是连续的

物理内存地址是不连续的。

(4)零拷贝

假设socket进程需要从磁盘读取一份数据,进行网络传输,这里有3种方法可以实现:

  • read+ write

传统文件传输,经过两次系统调用,一次是 read() ,一次是 write() ,期间共发生了 4 次用户态与内核态的上下文切换,发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的。

结果 :2次系统调用,4次上下文切换,4次拷贝

  • mmap + write

有些通路不是必须的,这里我们可以用 mmap() 替换 read() 系统调用函数 ,由于mmap() 也是系统调用函数,可以直接访问pagecache数据,减少一次CPU数据拷贝。但是数据要写到socket缓存区还是需要cup做一次拷贝。

结果 :2次系统调用,4次上下文切换,3次拷贝

  • sendfile

从 Linux 内核 2.1 版本开始起,引进一个sendfile()。sendfile() 可以在2个文件描述符之间传递数据(完全在内核中),避免在内核缓冲区和用户缓冲区之间进行数据拷贝,效率很高。可以替代替代前面的 read() 和 write() 这两个系统调用,这样只需一次系统调用。

但是这还不是真正的零拷贝技术。

Linux 内核 2.4开始,又优化了,如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程

Block DMA:在传输完一块物理上连续的数据后引起一次中断,然后再由主机进行下一块物理上连续的数据传输

SG-DMA:使用一个链表描述物理上不连续的存储空间。DMA master在传输完一块物理连续的数据后,不用发起中断,而是根据链表来传输下一块物理上连续的数据,直到传输完毕后再发起一次中断.sg-dma有三种工作模式:

Memory-to-Stream即存储接口到流接口

Stream-to-Memory即流接口到存储接口

Memory-to-Memory的存储器到存储器(mmap类似)

第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区(pagecache)里;

第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;

结果 :1次系统调用,2次上下文切换,2次拷贝

这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。

  • splice

sendfile 只适用于将数据从文件拷贝到 socket 套接字上,同时需要硬件的支持,这也限定了它的使用范围。Linux 在 2.6.17 版本引入 splice 系统调用,不仅不需要硬件支持,还实现了两个文件描述符之间的数据零拷贝。splice 的伪代码如下:

splice(fd_in, off_in, fd_out, off_out, len, flags);

splice 系统调用可以在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline),从而避免了两者之间的 CPU 拷贝操作

 

结果 :1次系统调用,2次上下文切换,2次拷贝

(5)使用零拷贝技术的项目

Kafka:这也是 Kafka 在处理海量数据为什么这么快的原因之一

Nginx:Nginx 支持零拷贝技术,一般默认是开启零拷贝技术,参数:http{... sendfile on; ...}

(6)写时复制

内核缓冲区可能被多个进程所共享,如果某个进程想要这个共享区进行 write 操作,由于 write 不提供任何的锁操作,那么就会对共享区中的数据造成破坏,写时复制的引入就是 Linux 用来保护数据的。写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么就需要将其拷贝到自己的进程地址空间中。这样做并不影响其他进程对这块数据的操作,每个进程要修改的时候才会进行拷贝,所以叫写时拷贝。这种方法在某种程度上能够降低系统开销,如果某个进程永远不会对所访问的数据进行更改,那么也就永远不需要拷贝。

(7)缓冲区共享

缓冲区共享方式完全改写了传统的 I/O 操作,因为传统 I/O 接口都是基于数据拷贝进行的,要避免拷贝就得去掉原先的那套接口并重新改写,所以这种方法是比较全面的零拷贝技术,目前比较成熟的一个方案是在 Solaris 上实现的 fbuf(Fast Buffer,快速缓冲区)。fbuf 的思想是每个进程都维护着一个缓冲区池,这个缓冲区池能被同时映射到用户空间(user space)和内核态(kernel space),内核和用户共享这个缓冲区池,这样就避免了一系列的拷贝操作。

(8)直接I/O

绕开 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 则叫缓存 I/O

  (9)直接I/O应用场景

大文件传输(GB级别)

问题:由于文件太大,可能某些部分的文件数据被再次访问的概率比较低,pagecache命中率低,这时发挥不出缓存的优势,由于没有享受到缓存带来的好处,但却耗费 DMA 多拷贝到 PageCache 一次,反而性能更低;

针对大文件的传输,不应该使用 PageCache,也就是说不应该使用零拷贝技术。

可以使用异步I/O+直接I/O的方案。

传统文件传输,read()之后,会切换到内核态,一直等到:磁盘数据准备好数据,拷贝到pagecache,再由pagacache拷到用户缓存区,read()返回结果才继续。这里read()阻塞问题可以用异常I/O来解决。在read()切换到内核之后,结果可以先返回,继续执行。等磁盘准备发起中断信好,内核将数据直接由磁盘拷贝到用户空间(对于磁盘,异步 I/O 只支持直接 I/O)。

MySQL:

应用程序已经实现了磁盘数据的缓存,那么可以不需要 PageCache 再次缓存,减少额外的性能损耗。在 MySQL 数据库中,可以通过参数设置开启直接 I/O,默认是不开启

(10)传统,mmap,sendfile,splice

非超大文件:,mmap ,sendfile,splice 的⽅式要远远优于传统的⽂件拷贝。对于 mmap 和 sendfile/splice 在⽂件较⼩的时候, mmap 耗时更短,当⽂件较⼤时 sendfile/splice 的⽅式最优。

nginx可以根据文件大小来配置参数:

location /video/ { 
    sendfile on; 
    aio on; 
    directio 1024m; 
}

文件大小大于 directio 值后,使用「异步 I/O + 直接 I/O」,否则使用「零拷贝技术」。 

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这里是一个简单的内核和应用层内存映射 I/O 的完整源码,包含了内核模块和用户程序部分: ##### 内核模块部分 ```c #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/slab.h> #include <linux/uaccess.h> #include <linux/mm.h> #define DEVICE_NAME "my_mmap" MODULE_LICENSE("GPL"); static int major; static char* buffer; static int mmap_fault(struct vm_area_struct* vma, struct vm_fault* vmf) { unsigned long offset = vmf->pgoff << PAGE_SHIFT; struct page* page = virt_to_page(buffer + offset); get_page(page); vmf->page = page; return 0; } static const struct vm_operations_struct mmap_vm_ops = { .fault = mmap_fault }; static int mmap_mmap(struct file* filp, struct vm_area_struct* vma) { unsigned long size = vma->vm_end - vma->vm_start; unsigned long pfn = virt_to_phys(buffer) >> PAGE_SHIFT; int ret = io_remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot); if (ret) { printk(KERN_ERR "io_remap_pfn_range failed\n"); return ret; } vma->vm_ops = &mmap_vm_ops; mmap_fault(vma, NULL); return 0; } static struct file_operations fops = { .owner = THIS_MODULE, .mmap = mmap_mmap }; static int __init mmap_init(void) { major = register_chrdev(0, DEVICE_NAME, &fops); if (major < 0) { printk(KERN_ERR "Failed to register_chrdev\n"); return major; } buffer = kmalloc(PAGE_SIZE, GFP_KERNEL); if (!buffer) { printk(KERN_ERR "Failed to allocate buffer\n"); unregister_chrdev(major, DEVICE_NAME); return -ENOMEM; } memset(buffer, 'A', PAGE_SIZE); return 0; } static void __exit mmap_exit(void) { kfree(buffer); unregister_chrdev(major, DEVICE_NAME); } module_init(mmap_init); module_exit(mmap_exit); ``` 这份代码实现了一个简单的内核模块,它注册了一个名为 "my_mmap" 的字符设备,提供了一个 mmap 方法用于内存映射操作。mmap 方法的实现过程中,使用了 io_remap_pfn_range 函数将内核缓冲区的物理地址映射到用户进程的虚拟地址空间中。具体实现细节可以参考代码注释。 ##### 用户程序部分 ```c #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #define DEVICE_NAME "/dev/my_mmap" int main(int argc, char** argv) { int fd; char* buffer; size_t offset, length; if (argc < 3) { printf("Usage: %s <offset> <length>\n", argv[0]); return EXIT_FAILURE; } offset = atol(argv[1]); length = atol(argv[2]); fd = open(DEVICE_NAME, O_RDONLY); if (fd == -1) { perror("open"); return EXIT_FAILURE; } buffer = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, offset); if (buffer == MAP_FAILED) { perror("mmap"); close(fd); return EXIT_FAILURE; } for (size_t i = 0; i < length; ++i) { putchar(buffer[i]); } if (munmap(buffer, length) == -1) { perror("munmap"); close(fd); return EXIT_FAILURE; } close(fd); return EXIT_SUCCESS; } ``` 这份代码实现了一个用户程序,它打开了内核模块注册的字符设备 "/dev/my_mmap",使用 mmap 函数将设备文件的一部分映射到进程的虚拟地址空间中,并输出映射区域的内容。具体实现细节可以参考代码注释。 需要注意的是,用户程序和内核模块需要分别编译并加载到内核和用户空间中。可以使用 Makefile 管理编译过程。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值