zoukankan      html  css  js  c++  java
  • IO学习笔记3

    1.4 pageCache

    pagecache是kernel中的一个折中方案。

    可以没有pagecache,如果没有pagecache的话,那么如果应用想要访问文件的话,应用程序只需要调用kernel,然后kernel访问磁盘,拿到数据后直接返回就结束了,但是磁盘是比较慢的,为了提升效率所以加了pagecache这一层缓存。

    就像我们使用java读文件时会使用bytebuffer提升效率一样,kernel中会使用pagecache来提升效率。

    当我们的应用程序需要向文件写数据时,一般会经过如下流程:

    每一个pagecache就对应着磁盘文件的4K的内容,因此如果需要从磁盘中读取数据时也是一样的流程:

    1. 应用程序调用读文件的方法。
    2. 进行系统调用,用户态切换到内核态。
    3. 从pagecache中读数据。当pagecache中没有这一页数据时,就会抛出缺页异常,然后操作系统从磁盘文件中加载数据到pagecache中,然后在将数据返回给应用程序。

    缺页异常

    在linux系统中,文件存放在磁盘中,是按照块存储的。一块大约4K。当创建一个文件时,就会为这个文件分配一个Inode对象,这个Inode中存放的就是这个文件的块地址映射信息。因此创建一个空的文件时,也会占用4K的大小,1个块,当这个块填满时,如果追加内容,那么则会重新开辟一个块来存储。

    当应用程序从文件中读数据时,因为内存大小是有限的,需要给很多应用提供服务,因此不可能把所有的空间都用来存放加载的文件数据。因此内存中使用pagecache来缓存磁盘中加载的数据,每一个pagecache对应磁盘中的一个块,同时内存中维护一张表来存放已经加载的块。在内存中pagecache是不连续的。

    看上图,应用程序默认是整个内存空间都是属于自己可用的。但是实际使用中,一般都是运行多个进程,多个进程共享一个内存空间。如果应用程序直接使用物理内存的话,那么就会发生如下问题:

    1. 多个进程抢占同一块内存空间。
    2. 多个进程内数据分配地址不连续,寻址慢。
    3. 因为直接访问物理内存,那么进程之间可以访问到其他进程的数据,这是不安全的。

    为了解决上面的问题,所以抽象出了虚拟内存这一层概念。

    每一个进程都有一个自己的虚拟内存空间,这个虚拟内存空间只有自己可以使用,这样就实现了进程间资源的隔离,同时也避免了多个进程抢占同一块内存空间。而且整个虚拟内存只有自己可用,那么数据在分配内存地址时,内存地址是连续的,提升了效率。

    而虚拟内存中的地址,会映射到一个实际的物理内存地址。

    那么当两个进程同时需要操作一个文件时,因为进程间内存不可互相访问,因此,这个磁盘中的文件内容就会被加载到内存中两次。

    这样就会造成内存的浪费,同时可能两个进程只需要读取文件的一小部分数据,而加载整个文件到内存中,如果文件非常大的话,就会造成内存空间不够的问题。为了解决上面的问题,因此出现了pagecache。

    当两个进程都需要访问文件X时,其实内存中会维护一个pagecache的表,表示缓存的文件块范围。如果当前没有缓存要访问的内容,那么就会发生缺页异常,中断,从用户态切换到内核态。然后读取数据到pagecache中,然后返回给应用程序。多个进程访问同一块pagecache时,各自会有一个fd文件。

    dirty脏页

    文件会优先加载到pagecache中,只要内存还够用,那么就会一直加载。使用脚本查看文件的pagecache缓存状态。

    import java.io.*;
    import java.nio.ByteBuffer;
    import java.nio.MappedByteBuffer;
    import java.nio.channels.FileChannel;
    
    /**
     * @author 赵帅
     * @date 2021/4/20
     */
    public class OSFileIO {
    
        private static final String PATH = "/root/testfileio/output.txt";
    
        private static final byte[] CONTENT = "123456789
    ".getBytes();
    
        public static void main(String[] args) throws Exception {
            String operation = args[0];
            switch (operation) {
                case "0":
                    basicFileIO();
                    break;
                case "1":
                    bufferFIleIO();
                    break;
                case "2":
                    randomAccessFileIO();
                    break;
                case "3":
                    byteBuffer();
                    break;
                default:
                    break;
            }
        }
    
        private static void basicFileIO() throws Exception {
            File file = new File(PATH);
            FileOutputStream os = new FileOutputStream(file);
            while (true) {
                os.write(CONTENT);
            }
        }
    
        private static void bufferFIleIO() throws Exception {
            File file = new File(PATH);
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
            while (true) {
                bos.write(CONTENT);
            }
        }
    
        private static void randomAccessFileIO() throws Exception{
            RandomAccessFile raf = new RandomAccessFile(PATH, "rw");
            raf.write("hello world".getBytes());
            raf.write("hello java".getBytes());
            System.out.println("write ok........................");
    
            // 阻塞
            System.in.read();
    
            raf.seek(4);
            raf.write("ooxx".getBytes());
            System.out.println("seek finished.....................");
    
            System.in.read();
    
            FileChannel rafchannel = raf.getChannel();
            MappedByteBuffer map = rafchannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
            map.put("@@@".getBytes());
            System.out.println("map--put--------");
        }
        
        private static void byteBuffer() {
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    
    
            System.out.println("postition: " + buffer.position());
            System.out.println("limit: " +  buffer.limit());
            System.out.println("capacity: " + buffer.capacity());
            System.out.println("mark: " + buffer);
    
            buffer.put("123".getBytes());
    
            System.out.println("-------------put:123......");
            System.out.println("mark: " + buffer);
    
            buffer.flip();   //读写交替
    
            System.out.println("-------------flip......");
            System.out.println("mark: " + buffer);
    
            buffer.get();
    
            System.out.println("-------------get......");
            System.out.println("mark: " + buffer);
    
            buffer.compact();
    
            System.out.println("-------------compact......");
            System.out.println("mark: " + buffer);
    
            buffer.clear();
    
            System.out.println("-------------clear......");
            System.out.println("mark: " + buffer);
        }
    }
    

    准备上面的java文件,将这个文件运行在linux中,编辑启动脚本:

    #!/bin/bash
    # author: 赵帅
    
    if [ $# -lt 1 ];
    then
      printf "Usage ./start.sh {0|1|2}
    "
      exit
    fi
    
    operation=$1
    rm -rf ./out.*
    file_name='OSFileIO'
    class_file="${file_name}.class"
    
    if test ! -e $class_file ; then
        javac ${file_name}.java
    fi
    
    strace -ff -o out /root/jdk1.8.0_221/bin/java $file_name $operation
    

    执行启动脚本,并新开一个窗口不断执行ll -h&& ./pcstat output.txt

    可以看到pagecache缓存页数在不停的增加。

    但是内存是有限的,当没有可用内存用来新增pagecache时,就要清理掉一部分pagecache用来增加新的pagecache。该清理掉哪儿写部分呢?这时就有一个dirty脏页的概念。

    什么是脏页?

    当应用程序向文件中写数据时,优先写在pagecache中,只要pagecache中的数据被修改了,而且修改的内容还没有被刷写到磁盘中,那么这一页pagecache就会被操作系统标记为dirty脏页。脏页中的数据什么时候写入磁盘?

    [root@node01 testfileio]# sysctl -a |grep dirty
    sysctl: reading key "net.ipv6.conf.all.stable_secret"
    sysctl: reading key "net.ipv6.conf.default.stable_secret"
    sysctl: reading key "net.ipv6.conf.ens33.stable_secret"
    sysctl: reading key "net.ipv6.conf.lo.stable_secret"
    # 控制脏页内存数量,超过dirty_background_bytes时,内核的flush线程开始回写脏页
    vm.dirty_background_bytes = 0
    # 控制脏页占可用内存(空闲+可回收)的百分比,达到dirty_background_ratio时,内核的flush线程开始回写脏页。默认值: 10
    vm.dirty_background_ratio = 10
    # 控制脏页内存数量,达到dirty_bytes时,执行磁盘写操作的进程开始回写脏页
    vm.dirty_bytes = 0
    # 控制脏页所占可用内存百分比,达到dirty_ratio时,执行磁盘写操作的进程自己开始回写脏数据。默认值:20
    vm.dirty_ratio = 30
    # 这个值表示page cache中的数据多久之后被标记为脏数据。只有标记为脏的数据在下一个周期到来时pdflush才会刷入到磁盘,这样就意味着用户写的数据在30秒之后才有可能被刷入磁盘,在这期间断电都是会丢数据的。
    vm.dirty_expire_centisecs = 3000
    # 5s的时间内核flush线程就会被唤起去刷新脏数据
    vm.dirty_writeback_centisecs = 500
    

    可以通过以上参数控制脏页的刷写。

    vm.dirty_background_bytesvm.dirty_background_ratio这两个参数只能指定一个,先设定的先生效,另一个会被清零。

    vm.dirty_bytesvm.dirty_ratio也是只能设置一个。

    可以将上面配置写入/etc/sysctl.conf文件中。

    从上面参数可以看出,dirty的刷写是由阈值或者指定大小控制的,在页面上看到的写入的数据并不一定被写入了磁盘。加了一层pagecache本来是为了提高IO效率,但是响应的就会带来一个丢数据的风险。

    pagecache就是一个4K大小的内存空间,linux内核中会为每一个pagecache维护一个索引,dirty标记就是在索引中

    butebuffer

    首先了解为什么bufferedIO比普通的IO快。

    执行./start.sh 0,等几秒钟后停止:

    [root@node01 testfileio]# ./start.sh 0
    ^C[root@node01 testfileio]# ll
    总用量 3032
    -rw-r--r--. 1 root root    1922 4月  21 09:51 OSFileIO.class
    -rw-r--r--. 1 root root    1563 4月  21 09:50 OSFileIO.java
    -rw-r--r--. 1 root root    9909 4月  21 17:17 out.7466
    -rw-r--r--. 1 root root  261066 4月  21 17:17 out.7467
    -rw-r--r--. 1 root root    2710 4月  21 17:17 out.7468
    -rw-r--r--. 1 root root    1255 4月  21 17:17 out.7469
    -rw-r--r--. 1 root root    1084 4月  21 17:17 out.7470
    -rw-r--r--. 1 root root    2121 4月  21 17:17 out.7471
    -rw-r--r--. 1 root root    5660 4月  21 17:17 out.7472
    -rw-r--r--. 1 root root    3295 4月  21 17:17 out.7473
    -rw-r--r--. 1 root root     960 4月  21 17:17 out.7474
    -rw-r--r--. 1 root root   15397 4月  21 17:17 out.7475
    -rw-r--r--. 1 root root    1832 4月  21 17:17 out.7476
    -rw-r--r--. 1 root root    3770 4月  21 17:17 output.txt
    -rwxr-xr-x. 1 root root 2759061 4月  20 14:01 pcstat
    -rwxr-xr-x. 1 root root     312 4月  20 18:55 start.sh
    [root@node01 testfileio]# jps -l
    7478 sun.tools.jps.Jps
    [root@node01 testfileio]# 
    

    最大的out文件就是主线程的文件,打开out.7467,搜索123456789 可以看到如下内容:

    ................
    write(4, "123456789
    ", 10)             = 10
    write(4, "123456789
    ", 10)             = 10
    write(4, "123456789
    ", 10)             = 10
    write(4, "123456789
    ", 10)             = 10
    write(4, "123456789
    ", 10)             = 10
    write(4, "123456789
    ", 10)             = 10
    ..........
    

    会看到非常多的write(4, "123456789 ", 10) = 10,每一个write调用都是一次系统调用,都会产生用户态和内核态的切换,而每次都只写了8个字节的数据。

    再执行./start.sh 1,重复上面操作,可以看到如下内容:

    ..............
    write(4, "123456789
    123456789
    123456789
    12"..., 8190) = 8190
    write(4, "123456789
    123456789
    123456789
    12"..., 8190) = 8190
    write(4, "123456789
    123456789
    123456789
    12"..., 8190) = 8190
    write(4, "123456789
    123456789
    123456789
    12"..., 8190) = 8190
    write(4, "123456789
    123456789
    123456789
    12"..., 8190) = 8190
    write(4, "123456789
    123456789
    123456789
    12"..., 8190) = 8190
    ...........
    

    可以看到调用bufferedIO后,每次调用write方法时,写入的数据为8KB,相比一般IO每次系统调用只写8字节相比。BufferedIO减少了非常多的用户态和内核态的切换过程。所以bufferedIO比一般IO更快。

    上面都是阻塞式IO,即传统的BIO(java.io.*),现在java有了nio(java.nio.*),下面尝试使用nio来操作文件。首先了解byteBuffer类,查看OSFileIObyteBuffer方法:

    private static void byteBuffer() {
            // 创建一个堆内内存的缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 创建一个堆外内存的缓冲区
            //ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    
            System.out.println("postition: " + buffer.position());
            System.out.println("limit: " +  buffer.limit());
            System.out.println("capacity: " + buffer.capacity());
            System.out.println("mark: " + buffer);
    
            buffer.put("abc".getBytes());
    
            System.out.println("-------------put:abc......");
            System.out.println("mark: " + buffer);
    
            buffer.flip();   //读写交替
    
            System.out.println("-------------flip......");
            System.out.println("mark: " + buffer);
    
            buffer.get();
    
            System.out.println("-------------get......");
            System.out.println("mark: " + buffer);
    
            buffer.compact();
    
            System.out.println("-------------compact......");
            System.out.println("mark: " + buffer);
    
            buffer.clear();
    
            System.out.println("-------------clear......");
            System.out.println("mark: " + buffer);
        }
    

    首先查看ByteBuffer内的属性,执行System.out.println("mark:" + buffer)后输出如下:

    mark: java.nio.HeapByteBuffer[pos=0 lim=1024 cap=1024]
    

    有三个属性:

    • postition: 可以理解为头指针
    • limit: 尾指针
    • capacity:容量

    刚初始化时,bytebuffer内部就长着个样子。当调用put方法存入字节数时buffer.put("123".getBytes());

    mark: java.nio.HeapByteBuffer[pos=3 lim=1024 cap=1024]
    

    每放一个字节,postition指针右移一位。当postitionlimit重合时表示缓冲区满了。

    当需要从缓冲区读数据时,需要先调用buffer.flip();进行翻转。翻转后的指针状态为:

    mark: java.nio.HeapByteBuffer[pos=0 lim=3 cap=1024]
    

    postition表示下一个可读的字节位置,limit表示最后一个可读的字节位置。每调用一次get方法,postition就会右移一位。

    compact: 压缩缓冲区,当在上面的基础上取出第一个字节后,postition就会移动到b所在下标,那么此时如果要继续写数据的话,第一个字节位置就空出来了,可以调用compact方法压缩空间,调用后内部结构如下:

    调用压缩方法后,就可以继续写入数据了。

    mmap

    在pagecache中写到每个进程都会分配一个Heap堆,这个堆事C语言的堆,也就是为这个进程分配的堆。那么在运行java应用程序时。这个堆也就是为java这个进程分配的堆。但是java的运行是多线程的。会有一个jvm的守护线程。这个线程也会有一个堆,java程序的对象都存放在这个堆中。那么关系图就是下面这样:

    前面说了进程写入文件的所有数据最终都写入了pagecache中,那么当java进程想要将数据写入文件时,会有这么一个过程:

    如果写在堆内,就会经过一个堆内数据向堆外数据的拷贝过程,因此堆外比堆内的效率更高。

    在文件IO中,还有一种性能更高的方案 mmap。直接写入堆外内存,可以减少一次copy的过程,但是从堆外内存写入pagecache中,仍然存在用户态到内核态的切换,使用mmap就可以在不切换内核态的情况下,在用户态直接将数据写入pagecache中。

    什么是mmap

    只有文件IO才会有mmap,mmap是通过在应用程序的虚拟地址空间创建一个映射,直接映射到内存的一块pagecache中。这样应用程序写的数据可以直接送到pagecache中,避免了系统调用。

    mmap虽然可以直接映射到pagechche中,不需要状态的切换,但是在pagecache将数据刷写到磁盘时,仍然还是需要切换到内核态的,而且使用mmap仍然存在pagecache可能丢数据的缺点。

    mmap使用方式:

    // 输入输出流获取fileChannel
    FileInputStream fis = new FileInputStream();
    FileChannel fc = fis.getChannel();
    // fc获取mmap
    MappedByteBuffer map = rafchannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
    
  • 相关阅读:
    面试:第四章:项目介绍
    面试:第八章:SpringMVC、Springboot、Mybatis、Dubbo、Zookeeper、Redis、Elasticsearch、Nginx 、Fastdfs、ActiveMQ
    HDU2021发工资咯:)
    HDU2029Palindromes _easy version
    js的Prototype属性 解释及常用方法
    backgroundposition 用法详细介绍
    递归算法与迭代算法的区别
    CSS Position 定位属性介绍
    JavaScript的事件绑定及深入
    CSS网页中的相对定位与绝对定位
  • 原文地址:https://www.cnblogs.com/Zs-book1/p/14693934.html
Copyright © 2011-2022 走看看