順序、隨機IO和Java多種讀寫文件性能對比


概述

對於磁盤的讀寫分為兩種模式,順序IO和隨機IO。 隨機IO存在一個尋址的過程,所以效率比較低。而順序IO,相當於有一個物理索引,在讀取的時候不需要尋找地址,效率很高。

基本流程

總體結構

我們編寫的用戶程序讀寫文件時必須經過的OS和硬件交互的內存模型

image-20220330164943899

讀文件

用戶程序通過編程語言提供的讀取文件api發起對某個文件讀取。此時程序切換到內核態,用戶程序處於阻塞狀態。由於讀取的內容還不在內核緩沖區中,導致觸發OS缺頁中斷異常。然后由OS負責發起對磁盤文件的數據讀取。讀取到數據后,先存放在OS內核的主存空間,叫PageCache。然后OS再將數據拷貝一份至用戶進程空間的主存ByteBuffer中。此時程序由內核態切換至用戶態繼續運行程序。程序將ByteBuffer中的內容讀取到本地變量中,即完成文件數據讀取工作。

寫文件

用戶程序通過編程語言提供的寫入文件api發起對某個文件寫入磁盤。此時程序切換到內核態用戶程序處於阻塞狀態,由OS負責發起對磁盤文件的數據寫入。用戶寫入數據后,並不是直接寫到磁盤的,而是先寫到ByteBuffer中,然后再提交到PageCache中。最后由操作系統決定何時寫入磁盤。數據寫入PageCache中后,此時程序由內核態切換至用戶態繼續運行。

用戶程序將數據寫入內核的PageCache緩沖區后,即認為寫入成功了。程序由內核態切換回用於態,可以繼續后續的工作了。PageCache中的數據最終寫入磁盤是由操作系統異步提交至磁盤的。一般是定時或PageCache滿了的時候寫入。如果用戶程序通過調用flush方法強制寫入,則操作系統也會服從這個命令。立即將數據寫入磁盤然后由內核態切換回用戶態繼續運行程序。但是這樣做會損失性能,但可以確切的知道數據是否已經寫入磁盤了。

詳細流程

讀文件

			// 一次讀多個字節
            byte[] tempbytes = new byte[100];
            int byteread = 0;
            in = new FileInputStream(fileName);//①
            ReadFromFile.showAvailableBytes(in);
            // 讀入多個字節到字節數組中,byteread為一次讀入的字節數
            while ((byteread = in.read(tempbytes)) != -1) { //②
                System.out.write(tempbytes, 0, byteread);
            }
  • 首先通過位置①的代碼發起一個open的系統調用,程序由用戶態切換到內核態。操作系統通過文件全路徑名在文件目錄中找到目標文件名對應的文件iNode標識ID,然后用這個iNode標識ID在iNode索引文件找到目標文件iNode節點數據並加載到內核空間中。這個iNode節點包含了文件的各種屬性(創建時間,大小以及磁盤塊空間占用信息等等)。然后再由內核態切換回用戶態,這樣程序就獲得了操作這個文件的文件描述。接下來就可以正式開始讀取文件內容了。

  • 然后再通位置②,循環數次獲取固定大小的數據。通過發起read系統調用,操作系統通過文件iNode文件屬性中的磁盤塊空間占用信息得到文件起始位的磁盤物理地址。再從磁盤中將要取得數據拷貝到PageCache內核緩沖區。然后將數據拷貝至用戶進程空間。程序由內核態切換回用戶態,從而可以讀取到數據,並放入上面代碼中的臨時變量tempbytes中。

image-20220401150736000

操作系統通過iNode節點中的磁盤塊占用信息去定位磁盤文件數據。其細節如下

image-20220401150833564

  • ①根據文件路徑從文件目錄中找到iNode ID。

  用戶讀取一個文件,首先需要調用OS中文件系統的open方法。該方法會返回一個文件描述符給用戶程序。OS首先根據用戶傳過來的文件全路徑名在目錄索引數據結構中找到文件對應的iNode標識ID。目錄數據是存在於磁盤上的,在OS初始化時就會加載到內存中,由於目錄數據結構並不會很龐大,一次性加載駐留到內存也不是不可以或者部分加載,等需要的時候在從磁盤上調度進內存也可以。根據文件路徑在目錄中查找效率應該是很高的,因為目錄本身就是一棵樹,應該也是類似數據庫的樹形索引結構。所以它的查找算法時間復雜度就是O(logN)。具體細節我暫時還沒弄清楚,這不是重點。

  iNode就是文件屬性索引數據了。磁盤格式化時OS就會把磁盤分區成iNode區和數據區。iNode節點就包含了文件的一些屬性信息,比如文件大小、創建修改時間、作者等等。其中最重要的是還存有整個文件數據在磁盤上的分布情況(文件占用了哪些磁盤塊)。

  • ②根據iNode ID從Inode索引中找到文件屬性。

  得到iNode標識的ID后,就可以去iNode數據中查找到對應的文件屬性了,並加載到內存,方便后續讀寫文件時快速獲得磁盤定位。iNode數據結構應該類似哈希結構了,key就是iNode標識ID,value就是具體某個文件的屬性數據對象了。所以它的算法時間復雜度就是O(1)。具體細節我暫時還沒弄清楚,這不是重點。

  我們系統中的文件它的文件屬性(iNode)和它的數據正文是分開存儲的。文件屬性中有文件數據所在磁盤塊的位置信息。

  • ③根據文件屬性中的磁盤空間塊信息找到需要讀取的數據所在的磁盤塊的物理位置

  文件屬性也就是iNode節點這個數據結構,里面包含了文件正文數據在磁盤物理位置上的分布情況。磁盤讀寫都是以塊為單位的。所以這個位置信息其實也就是一個指向磁盤塊的物理地址指針。

理解認識

磁盤上文件存儲數據結構是鏈表,每一塊文件數據節點里有一個指針指向下一塊數據節點。理解錯誤!

很多人都知道磁盤存儲一個文件不可能是連續分配空間的。而是東一塊西一塊的存儲在磁盤上的。就誤以為這些分散的數據節點就像鏈表一樣通過其中一個指針指向下一塊數據節點。如下圖所示。

img

  怎么說呢?這種方案以前也是有一些文件系統實現過的方案,但是現在常見的磁盤文件系統都不再使用這種落后的方案。而是我前面提到的iNode節點方案。也就是說磁盤上存儲的文件數據塊就是純數據,沒有其他指針之類的額外信息。之所以我們能順利定位這些數據塊,都全靠iNode節點屬性中磁盤塊信息的指針。

append文件尾部追加方法是順序寫,也就是磁盤會分配連續的空間給文件存儲。理解錯誤!

  • 這種觀點,包括網上和某些技術書籍里的作者都有這種觀點。實際上是錯誤的。或許是他們根本沒有細究文件存儲底層OS和磁盤硬件的工作原理導致。我這里就重新總結糾正一下這種誤導性觀點。

  • 前面說過,append系統調用是write的限制形式,即總是在文件末尾追加內容。看上去好像是說順序寫入文件數據,因為是在尾部追加啊!所以這樣很容易誤導大家以為這就是順序寫,即磁盤存儲時分配連續的空間給到文件,減少了磁盤尋道時間。

  • 事實上,磁盤從來都不會連續分配空間給哪個文件。這是我們現代文件系統的設計方案。前面介紹iNode知識時也給大家詳細說明了。所以就不再贅述。我們用戶程序寫文件內容時,提交給OS的緩沖區PageCache后就返回了。實際這個內容存儲在磁盤哪個位置是由OS決定的。OS會根據磁盤未分配空間索引表隨機找一個空塊把內容存儲進去,然后更新文件iNode里的磁盤占用塊索引數據。這樣就完成了文件寫入操作。所以append操作不是在磁盤上接着文件末尾內容所在塊位置連續分配空間的。最多只能說邏輯上是順序的。

mmap內存映射技術之所以快,是因為直接把磁盤文件映射到用戶空間內存,不走內核態。理解錯誤

  • 這也是一種常見的認知誤區,實際上這個技術是操作系統給用戶程序提供的一個系統調用函數。它把文件映射到OS內核緩沖區空間,同時共享給用戶進程,也可以共享給多個用戶進程。映射過程中不會產生實際的數據從磁盤真正調取動作,只有用戶程序需要的時候才會調入部分數據。總之也是和普通文件讀取一樣按需調取。那么mmap技術為什么在讀取數據時會比普通read操作快幾個數量級呢?

  • 上面我們講述了普通讀寫操作的內存模型。用戶程序要讀取到磁盤上的數據。要經歷4次內核態切換以及2次數據拷貝操作。那么mmap技術由於是和用戶進程共享內核緩沖區,所以少了一次拷貝操作(數據從內核緩沖區到用戶進程緩沖區)。從而大大提高了性能。

mmap內存映射技術寫文件快是因為順序寫磁盤。理解錯誤!

上面的問題基本已經讓我們理解了mmap技術的內存模型。同樣的,我們寫文件時,由於也少了一次數據從用戶緩沖區到內核緩沖區的拷貝操作。使得我們的寫效率非常的高。並不是很多人認為的數據直達磁盤,中間不經過內核態切換,並且連續在磁盤上分配空間寫入。這些理解都是錯誤的。

隨機讀寫文件比順序讀寫文件慢,是因為磁盤移動磁頭來回隨機移動導致。理解錯誤!

  • 這也是一種常見的誤區。我看過很多文章都是這樣認為的。其實所有的寫操作在硬件磁盤層面上都是隨機寫。這是由現代操作系統的文件系統設計方案決定的。我們用戶程序寫入數據提交給OS緩沖區之后,就與我們沒關系了。操作系統決定何時寫入磁盤中的某個空閑塊。所有的文件都不是連續分配的,都是以塊為單位分散存儲在磁盤上。原因也很簡單,系統運行一段時間后,我們對文件的增刪改會導致磁盤上數據無法連續,非常的分散。

  • 當然OS提交PageCache中的寫入數據時,也有一定的優化機制。它會讓本次需要提交給磁盤的數據規划好磁頭調度的策略,讓寫入成本最小化。這就是磁盤調度算法中的電梯算法了。這里就不深入講解了。

  • 至於讀文件,順序讀也只是邏輯上的順序,也就是按照當前文件的相對偏移量順序讀取,並非磁盤上連續空間讀取。即便是seek系統調用方法隨機定位讀,理論上效率也是差不多的。都是使用iNode的磁盤占用塊索引文件快速定位物理塊。

測試對比

FileWriter和FileRead的封裝類

package cn.itxs.filedemo;

import java.io.*;

public class RWHelper {
    public static void fileWrite(String filePath, String content) {
        File file = new File(filePath);
        //創建FileWriter對象
        FileWriter writer = null;
        try {
            //如果文件不存在,創建文件
            if (!file.exists())
                file.createNewFile();
            writer = new FileWriter(file);
            writer.write(content);//寫入內容
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void fileRead(String filePath) {
        File file = new File(filePath);
        if (file.exists()) {
            try {
                //創建FileReader對象,讀取文件中的內容
                FileReader reader = new FileReader(file);
                char[] ch = new char[1];
                while (reader.read(ch) != -1) {
                    System.out.print(ch);
                }
                reader.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }

        }
    }
}

FileOutputStream和FileInputStream封裝類

package cn.itxs.filedemo;

import java.io.*;
import java.nio.charset.StandardCharsets;

public class StreamRWHelper {
    public static void fileWrite(String filePath, String content) {
        FileOutputStream outputStream = null;
        try {
            File file = new File(filePath);
            boolean isCreate = file.createNewFile();//創建文件
            if (isCreate) {
                outputStream = new FileOutputStream(file);//形參里面可追加true參數,表示在原有文件末尾追加信息
                outputStream.write(content.getBytes());
            }else {
                outputStream = new FileOutputStream(file,true);//表示在原有文件末尾追加信息
                outputStream.write(content.getBytes());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void fileRead(String filePath) {
        File file = new File(filePath);
        if (file.exists()) {
            try {
                //創建FileInputStream對象,讀取文件內容
                FileInputStream fis = new FileInputStream(file);
                byte[] bys = new byte[1024];
                while (fis.read(bys, 0, bys.length) != -1) {
                    //將字節數組轉換為字符串
                    System.out.print(new String(bys, StandardCharsets.UTF_8));
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }

        }
    }

    /**
     * 文件合並
     */
//    public static void mergeFile(List<String> inputPaths, String outputPath) throws FileNotFoundException {
//
//        Vector<InputStream> inputStream = new Vector<InputStream>();
//
//        if (CollectionUtils.isEmpty(inputPaths)) {
//            throw new LogicException("合並文件路徑不能為空");
//        }
//
//        for (String inputPath : inputPaths) {
//            InputStream in = new  FileInputStream(new File(inputPath));
//            inputStream.add(in);
//        }
//
//        //構造一個合並流
//        SequenceInputStream stream = new SequenceInputStream(inputStream.elements());
//        BufferedOutputStream bos = null;
//        try {
//            bos = new BufferedOutputStream(
//                    new FileOutputStream(outputPath));
//
//            byte[] bytes = new byte[10240];
//            int len = -1;
//            while((len=stream.read(bytes))!=-1){
//                bos.write(bytes,0,len);
//                bos.flush();
//            }
//            log.info("文件合並完成!");
//        } catch (IOException e) {
//            e.printStackTrace();
//        }finally {
//            try {
//                if (null != bos ) {
//                    bos.close();
//                }
//                stream.close();
//            } catch (IOException ignored) {
//            }
//        }
//    }
}

BufferedWriter和BufferedReader封裝類

package cn.itxs.filedemo;

import java.io.*;

public class BuffredRWHelper {
    public static void fileWrite(String filePath, String content) {
        File file = new File(filePath);
        //創建FileWriter對象
        BufferedWriter writer = null;
        try {
            //如果文件不存在,創建文件
            if (!file.exists())
                file.createNewFile();
            writer = new BufferedWriter(new FileWriter(file));
            writer.write(content);//寫入內容
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void fileRead(String filePath) {
        File file = new File(filePath);
        if (file.exists()) {
            try {
                //創建FileReader對象,讀取文件中的內容
                BufferedReader reader = new BufferedReader(new FileReader(file));
                String line;
                while ((line = reader.readLine()) != null) {
                    System.out.print(line);
                }
                reader.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }

        }
    }
}

MappedByteBuffer讀寫封裝類

package cn.itxs.filedemo;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class BigFileHelper {
    public static long fileWrite(String filePath, String content, int index) {
        File file = new File(filePath);
        RandomAccessFile randomAccessTargetFile;
        MappedByteBuffer map;
        try {
            randomAccessTargetFile = new RandomAccessFile(file, "rw");
            FileChannel targetFileChannel = randomAccessTargetFile.getChannel();
            map = targetFileChannel.map(FileChannel.MapMode.READ_WRITE, 0, (long) 1024 * 1024 * 240);

            /**
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(content.length());
            byteBuffer.put(content.getBytes());
            byteBuffer.flip();
            while (byteBuffer.hasRemaining()) {
                targetFileChannel.write(byteBuffer);
            }
            //MappedByteBuffer再次獲取內存中的內容,獲取到的內容是woshihaoren
            byte[] bytes = new byte[content.length()];
            map.get(bytes);
            System.out.println(new String(bytes));
            **/

            map.position(index);
            map.put(content.getBytes());
            return map.position();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        }
        return 0L;
    }

//    String woshihaoren = "woshihaoren";
//        try (RandomAccessFile f = new RandomAccessFile("D:\\a.txt", "rw")) {
//        FileChannel fc = f.getChannel();
//        // 創建一個MappedByteBuffer,此時MappedByteBuffer獲取到的內容都是空
//        MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, woshihaoren.length());
//        // 將字符串寫入
//        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(woshihaoren.length());
//        byteBuffer.put(woshihaoren.getBytes());
//        byteBuffer.flip();
//        while (byteBuffer.hasRemaining()) {
//            fc.write(byteBuffer);
//        }
//
//        // MappedByteBuffer再次獲取內存中的內容,獲取到的內容是woshihaoren
//        byte[] bytes = new byte[woshihaoren.length()];
//        mbb.get(bytes);
//        System.out.println(new String(bytes));
//    } catch (Exception e) {
//        e.printStackTrace();
//    }

    public static String fileRead(String filePath, long index) {
        File file = new File(filePath);
        RandomAccessFile randomAccessTargetFile;
        MappedByteBuffer map;
        try {
            randomAccessTargetFile = new RandomAccessFile(file, "rw");
            FileChannel targetFileChannel = randomAccessTargetFile.getChannel();
            map = targetFileChannel.map(FileChannel.MapMode.READ_WRITE, 0, index);
            byte[] byteArr = new byte[10 * 1024];
            map.get(byteArr, 0, (int) index);
            return new String(byteArr);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        }
        return "";
    }
}

測試類

package cn.itxs.filedemo;


import java.util.Random;

/**
 * Hello world!
 *
 */
public class App 
{
    public static void main( String[] args )
    {
        long start = System.currentTimeMillis();
        System.out.println("start:"+start);
        int count = Integer.parseInt(args[1]);
        for (int i = 0; i < count; i++) {
            System.out.println("file start:"+System.currentTimeMillis());
            if (Integer.parseInt(args[0]) == 1) {
                String bigFileName = args[2] + i + ".txt";
                BigFileHelper.fileWrite(bigFileName,getFixStr(2000000),0);
            }else if (Integer.parseInt(args[0]) == 2){
                String commonFileName = args[2] + i + ".txt";
                RWHelper.fileWrite(commonFileName,getFixStr(2000000));
            }else {
                System.exit(0);
            }
            System.out.println("file end:"+System.currentTimeMillis());
        }
        long end = System.currentTimeMillis();
        System.out.println("end:"+end + ",time:" + (end-start));
    }

    public static String getRandomNumStr(long n) {
        Random random = new Random();
        StringBuilder randomStr = new StringBuilder();
        for (long i = 0; i < n; i++) {
            randomStr.append(random.nextInt(10));
        }
        return randomStr.toString();
    }
    public static String getFixStr(long n) {
        Random random = new Random();
        StringBuilder randomStr = new StringBuilder();
        for (long i = 0; i < n; i++) {
            randomStr.append("aaaasdfaaaaaaayyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyfffffaaaaaaaaaaaaaaasdfsdfsdfsdfsdfdafsdf");
        }
        return randomStr.toString();
    }
}

普通寫文件IO情況

image-20220331104233899

內存映射寫文件IO情況

image-20220331104219800

測試共100個230M文件約22G數據,普通寫文件共耗時321726毫秒約332秒,內存映射共耗時129924毫秒約130秒,估計和磁盤類型有關。10個2.2G文件普通寫文件16秒,內存映射10秒。

**本人博客網站 **IT小神 www.itxiaoshen.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM