
本篇簡單介紹磁盤的讀寫分為兩種模式,順序IO和随機IO。 随機IO存在一個尋址的過程,是以效率比較低。而順序IO,相當于有一個實體索引,在讀取的時候不需要尋找位址,效率很高。最後通過Java多種讀寫檔案示例的性能測試比較
概述
對于磁盤的讀寫分為兩種模式,順序IO和随機IO。 随機IO存在一個尋址的過程,是以效率比較低。而順序IO,相當于有一個實體索引,在讀取的時候不需要尋找位址,效率很高。
基本流程
總體結構
我們編寫的使用者程式讀寫檔案時必須經過的OS和硬體互動的記憶體模型
讀檔案
使用者程式通過程式設計語言提供的讀取檔案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中。
作業系統通過iNode節點中的磁盤塊占用資訊去定位磁盤檔案資料。其細節如下
- ①根據檔案路徑從檔案目錄中找到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節點這個資料結構,裡面包含了檔案正文資料在磁盤實體位置上的分布情況。磁盤讀寫都是以塊為機關的。是以這個位置資訊其實也就是一個指向磁盤塊的實體位址指針。
了解認識
磁盤上檔案存儲資料結構是連結清單,每一塊檔案資料節點裡有一個指針指向下一塊資料節點。了解錯誤!
很多人都知道磁盤存儲一個檔案不可能是連續配置設定空間的。而是東一塊西一塊的存儲在磁盤上的。就誤以為這些分散的資料節點就像連結清單一樣通過其中一個指針指向下一塊資料節點。如下圖所示。
怎麼說呢?這種方案以前也是有一些檔案系統實作過的方案,但是現在常見的磁盤檔案系統都不再使用這種落後的方案。而是我前面提到的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情況
記憶體映射寫檔案IO情況
測試共100個230M檔案約22G資料,普通寫檔案共耗時321726毫秒約332秒,記憶體映射共耗時129924毫秒約130秒,估計和磁盤類型有關。10個2.2G檔案普通寫檔案16秒,記憶體映射10秒。
**本人部落格網站 **IT小神 www.itxiaoshen.com