2、java文件系统io

image
app是一个应用程序,应用程序有缓冲区,我们在写代码的时候,都用过Buffer。
应用程序肯定要访问内核,内核其实也是一个程序,应用程序和内核通信的时候,会调用一个system call(系统调用)。
system call 的实现是int 0x80,int是cpu的指令,0x80是16进制的,所以它的值是128,如果是二进制的话,就是10000000,它是一个值,这个值放在cpu的寄存器,然后和中断描述符(和中断控制器应该是一个意思)去匹配,终端描述符的128表示的就是call back方法,去调用的这个方法。

所以应用程序调用某一个系统调用的时候,其实完成了一个int 0x80,这条指令一但被cpu读到以后,就开始调用内核里的call back方法,然后就开始保护现场,从用户态切换到内核态。

cpu读应用程序去执行,也会去读内核里的指令去执行,中间不停的切换读取用户态(用户空间,应用程序)和内核态(内核空间),然后从哪开始读取指令呢?由这个call back方法告诉cpu,然后执行调度。

比如cpu要调用kernel去读取硬盘上面的数据给读取到,调用的read,其实调用的是它的文件描述符(fd),读的时候比如数据很多,会分成很多个4k大小的pagecache,这是内核的缓冲区,pagecache会分成消耗内存去压榨内存,是不会主动释放的。

数据从硬盘读取到内核缓冲区,硬盘里也有缓冲区,读取的时候有个东西叫协处理器(DMA),这是什么?
如果没有协处理器,cpu读取数据的时候,从硬盘读取一个字节放在cpu自己的寄存器里,然后再交给内核,然后再拷贝到buffer,数据拷贝这么多次,这种做法是很缓慢的。
协处理器可以直接把数据拷贝到内核里,这样的话cpu和协处理器可以交叉使用,都可以去读取硬盘的数据到内存里,并且这个时候cpu还可以去做其他事情,不至于说直接停在那里只在拷贝数据,这样会快很多。所以叫做协处理器

linux正常关机和断电

linux虚拟机有2种关机方式,如果是图下这种方式,属于是正常关机,就等于是点了一个关机键。这个电源会产生一个中断,会告诉cpu,你的主人要关机了,然后开始调用关机的过程,会把脏数据,pagecache缓存,有一个回写磁盘的过程,会保存数据。

如下图这个关机,等同于直接把断电了,这样的话来不及把内存的脏数据和pagecache缓存的数据给刷出去。

验证:
打开虚拟机,查看mysh这个文件内容

这个文件是个脚本,内容如下:

删除当前目录包含out的所有文件,并且用jdk1.8 javac编译 OSFileIO.java 这个文件,然后启动这个OSFileIO.java 这个类,传进入一个参数1

然后查看一下这个OSFileIO.java 类的代码:
源代码

import org.junit.Test;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class OSFileIO {

    static byte[] data = "123456789\n".getBytes();
    static String path =  "/root/testfileio/out.txt";

    public static void main(String[] args) throws Exception {

        switch ( args[0]) {
            case "0" :
                testBasicFileIO();
                break;
            case "1":
                testBufferedFileIO();
                break;
            case "2" :
                testRandomAccessFileWrite();
            case "3":
//                whatByteBuffer();
            default:

        }
    }


    //最基本的file写

    public static  void testBasicFileIO() throws Exception {
        File file = new File(path);
        FileOutputStream out = new FileOutputStream(file);
        while(true){
            Thread.sleep(10);
            out.write(data);

        }

    }

    //测试buffer文件IO
    //  jvm  8kB   syscall  write(8KBbyte[])

    public static void testBufferedFileIO() throws Exception {
        File file = new File(path);
        BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
        while(true){
            Thread.sleep(10);
            out.write(data);
        }
    }

    //测试文件NIO
        public static void testRandomAccessFileWrite() throws  Exception {


        RandomAccessFile raf = new RandomAccessFile(path, "rw");

        raf.write("hello mashibing\n".getBytes());
        raf.write("hello seanzhou\n".getBytes());
        System.out.println("write------------");
        System.in.read();

        raf.seek(4);
        raf.write("ooxx".getBytes());

        System.out.println("seek---------");
        System.in.read();

        FileChannel rafchannel = raf.getChannel();
        //mmap  堆外  和文件映射的   byte  not  objtect
        MappedByteBuffer map = rafchannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);


        map.put("@@@".getBytes());  //不是系统调用  但是数据会到达 内核的pagecache
            //曾经我们是需要out.write()  这样的系统调用,才能让程序的data 进入内核的pagecache
            //曾经必须有用户态内核态切换
            //mmap的内存映射,依然是内核的pagecache体系所约束的!!!
            //换言之,丢数据
            //你可以去github上找一些 其他C程序员写的jni扩展库,使用linux内核的Direct IO
            //直接IO是忽略linux的pagecache
            //是把pagecache  交给了程序自己开辟一个字节数组当作pagecache,动用代码逻辑来维护一致性/dirty。。。一系列复杂问题

        System.out.println("map--put--------");
        System.in.read();

//        map.force(); //  flush



        raf.seek(0);

        ByteBuffer buffer = ByteBuffer.allocate(8192);
//        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

        int read = rafchannel.read(buffer);   //buffer.put()
        System.out.println(buffer);
        buffer.flip();
        System.out.println(buffer);

        for (int i = 0; i < buffer.limit(); i++) {
            Thread.sleep(200);
            System.out.print(((char)buffer.get(i)));
        }


    }


    @Test
    public  void whatByteBuffer(){

//        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("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);

    }
}

编辑一下


定义了data 和 path 两个属性,然后main方法有个switch,如果参数为0,就调用testBasicFileIO()方法,
testBasicFileIO()方法中写的是,通过new File() 获取一个文件,然后往这个文件输出data的数据,data是在上面定义好了的,在while死循环中不停的写入数据。

查看完代码以后,调用这个mysh脚本,传入参数0,就可以调用testBasicFileIO()方法了

启动后,不停的使用ll 命令查看out.txt文件,

上图可以看见内容大小在不断的变大,在这个时候直接点击这个关机,等于蒿掉电源,直接断电了

关机以后在重启启动这个虚拟机,查看out.txt文件可以看到,这个文件大小为0

所以就验证了,数据在内存中pagecache不停的修改写入数据,在它还没有来得及把数据刷出去保存到磁盘上面的时候,直接给他断电,数据就丢失了。

配置pagecache的数据写入磁盘

sysctl -a | grep dirty:通过管道查询dirty的配置项
image

这些配置项都是阈值 对应着字节数

接下来给这个配置项做一个修改,这个配置项在/etc/sysctl.conf
image

  • vm.dirty_background_radio:90 表示90%,内核内存比如有10G,当pagecache占用了9G时候,内核会有里另一个线程把数据同步到磁盘,剩下的10%内存会接着给用户程序使用。但如果写入的数据很快,可能会报错,因为内核还没来得及把数据写入磁盘。
  • vm.dirty_ratio:这个90表示pagecache的容量达到了内存90%,直接阻塞,用户程序不允许继续往内存写数据,会先把数据同步到磁盘,然后继续允许用户写数据。
  • vm.dirty_writeback_centisecs:单位是1%秒,表示多久写入一次数据到磁盘,5000就是50秒
  • vm.dirty_expire_centisecs:单位是1%秒,数据在pagecache存放时间,3000就是30秒

改完sysctl.conf 文件以后,sysctl -p执行一下,不然不生效
想象一下redis的日志保存和mysql的binLog保存信息,它们有3种方式保存数据:每秒钟保存一次、每操作一次保存一次、根据内核来保存。
为什么要有这3种方式?因为数据写入在pagecache,不会立马写入磁盘,发生意外有可能会丢失很多数据!

使用直接io和buffer的区别

普通io

使用ll -h && pcstat out.txt命令,查看这个out.txt 文件大小,和在内核中的分页情况
image
最好的100.000 表示百分比是多少(文件总页和内核中分配的页数量占比)

查看mysh 文件内容:
image

然后执行这个mysh 执行文件
image

执行后一直不停的使用ll -h && pcstat out.txt命令查看文件大小情况
image
这个就等于有一个字节就立马写入服务器的内存,所以会一直不断的切换核态,pagecache使用情况也一直都是100%。

如果这个时候给它断电,数据是整个都丢失了,不会保存到磁盘。
image

然后开机,继续查看那个out.txt文件,发现刚刚写的好几M大小的文件变成了0大小,数据丢失了
image

使用buffer

接下来调用这个testBufferedFileIO()方法
image

使用mysh这个执行文件,参数1表示调用这个testBufferedFileIO()方法
image

然后不停的在另一个窗口查看out.txt文件大小信息
image

这个等于使用buffer,可以很明显的发现这个out.txt 文件大小增长的非常快(这也说明了为什么java一定要用buffer的io),它使用了buffer,每攒够10个字节的数据就调用一次操作系统内核,所以相比较使用直接io的话,减少了调用内核的次数,避免了不停的切换核态。

image
刚开始发现pagecache使用率也是100%,但是等到写入数据的大小超过内存的3G大小以后,pagecache使用率慢慢的变小,因为这个时候说明已经在往磁盘写入数据了。

如果把这个文件改名为OOXX.txt 文件,可以查看到pagecache里面还是有和这个文件的关联数据。
image

如果这个时候再重新启动这个buffer的io,那它肯定会重新写入一个新的out.txt文件,然后疯狂的给内核缓冲区分配页缓存,肯定会占用那个OOXX.txt 文件之前的页缓存

可以看到这个ooxx.txt 文件现在的页缓存比例,说明还是有缓存很多东西在内核缓冲区的
image

然后再次启动mysh执行文件,重新向一个新的out.txt文件写入
image

然后使用pcstat out.txt && pcstat ooxx.txt命令,同时查看这两个文件的页缓存情况
不停的查看
image

但这个时候可以发现之前这个OOXX.txt 文件的pagecache使用率慢慢减少,被淘汰掉了(淘汰掉之前会把数据同步到磁盘),从而保证这个新的out.txt文件有相对应的文件页缓存

通过buffer的io写入数据虽然很快,但是如果断电的话,数据会丢失很多。
虽然看到的这个out.txt文件已经变得很大了,但实际它查询的是虚拟文件系统,并没有实际写入磁盘。

所以pagecache的优点就是提升io性能,缺点就是丢失数据。

nio

nio就理解是new io,意思是新的io体系。
新的io体系有两个概念:channel、byteBuffer
nio也是基于buffer的,channel等于是把输入输出整合到了一起。

ByteBuffer

ByteBuffer就理解成字节数组,有一些对应的api和概念。

allocate() 和 allocateDirect() 不影响最终结果,只是分配方式不一样。
测试代码:

	@Test
    public  void whatByteBuffer(){

//        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("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);

    }

执行结果:
image

  • postition: 表示写入时候的指针位置
  • limit:写入和读取时候指针截止位置(写入和读取时会切换位置)
  • capacity:表示buffer剩余空间,总大小。如果前面有已经读取过的字节,不能算在内。所以写入的时候需要capacity一下

postition 和 limit 两个指针位置在读或写的时候联动发生变化

抽象逻辑图:
刚开始:

put了123,3个字节后

flip() 翻转,翻转的目的是读写交替时候

compact()后,得到的buffer剩余的空间

基于文件的NIO

测试代码:

//测试文件NIO
        public static void testRandomAccessFileWrite() throws  Exception {

			// 使用RandomAccessFile(); 给出文件路径,给出权限,r表示可读,w表示可写
			RandomAccessFile raf = new RandomAccessFile(path, "rw");
			// write()表示普通的写,
			raf.write("hello mashibing\n".getBytes());
			raf.write("hello seanzhou\n".getBytes());
			System.out.println("write------------");
			System.in.read();
			// seek()指定字符下标指针偏移位置,写的内容会覆盖掉之前的字符。
			// 因为向文件写入数据的时候只能一直向后写,如果想要postition指针向前,是不行的,所以调用seek()方法可以修改postition指针指定位置。
			raf.seek(4);
			raf.write("ooxx".getBytes());

			System.out.println("seek---------");
			System.in.read();
			// getChannel()获取io通道,
			FileChannel rafchannel = raf.getChannel();
			//mmap  堆外  和文件映射的   byte  not  objtect
			MappedByteBuffer map = rafchannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
			// 只有文件系统有map()方法,只有map()方法返回MappedByteBuffer(堆外的,并且和文件映射的)
			// 如果是ByteBuffer上面获取,只能获取堆上的和堆外的内存空间

			map.put("@@@".getBytes());  //不是系统调用  但是数据会到达 内核的pagecache
				//曾经我们是需要out.write()  这样的系统调用,才能让程序的data 进入内核的pagecache
				//曾经必须有用户态内核态切换
				//mmap的内存映射,依然是内核的pagecache体系所约束的!!!
				//换言之,pagecache体系会丢数据
				//你可以去github上找一些 其他C程序员写的jni扩展库,使用linux内核的Direct IO(直接io)
				//直接IO是忽略linux的pagecache
				//是把pagecache  交给了程序自己开辟一个字节数组当作pagecache,动用代码逻辑来维护一致性/dirty(脏数据)。。。一系列复杂问题

			System.out.println("map--put--------");
			System.in.read();

	//        map.force(); //  等于flush,刷写数据到磁盘



			raf.seek(0);

			ByteBuffer buffer = ByteBuffer.allocate(8192);
	//        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

			int read = rafchannel.read(buffer);   //buffer.put()
			System.out.println(buffer);
			buffer.flip();
			System.out.println(buffer);

			for (int i = 0; i < buffer.limit(); i++) {
				Thread.sleep(200);
				System.out.print(((char)buffer.get(i)));
			}


    }

抽象原理逻辑图:
image

在整个操作系统中,有txt(编写的程序),data数据,堆内存(操作系统的)
txt自己编写的程序会向操作系统的堆内存分配一块空间,放在了jvm的堆里面
如果使用ByteBuffer使用allocate,相当于分配到了jvm的堆上(on heap),如果使用allocateDirect相当于分配到了java进程的堆空间里面(off heap)

FileChannel对象会有一个map方法,得到一个MappedByteBuffer,
MappedByteBuffer对象通过put方法映射到内核的pagecache,不会有系统调用。如果有脏数据,最终是写入磁盘的

但如果是通过channel的read或者write方法,都是需要系统调用的,虽然都是最终到达内核的pagecache,都会有数据丢失的可能,这个目前没法避免。

从逻辑上看allocateDirect 确实要比allocate 性能要高一些,
所以当一个对象可以自己控制的时候,就尽量使用allocateDirect方法,当这个对象自己没法得到一个具体的时候,只能使用allocate

性能对比:on heap < off heap < mapped(只限于文件系统)

既然pagecache没法确保数据不丢失,为什么还要使用?
**OS操作系统 没有觉得数据可靠性为什么设计pagecache: **
减少硬件 IO 调用,这样可以提速,优先使用内存。
即便你想要可靠性,调成最慢的方式,但是单点问题会让你的性能损耗,一毛钱收益都没有。
所以要主从复制,主备HA,kafka/ES 都有副本概念(通过socket io)  (副本又分 同步/异步)。。。

posted @ 2022-11-25 15:52  aBiu--  阅读(38)  评论(0编辑  收藏  举报