剛接觸 Java IO
的時候, 一直有一個 困惑:爲何 BufferedInputStream
比 FileInputStream
快? 隨着對 Linux
瞭解,這個問題也獲得解決。最近也在看 Linux 內核
方面的書,想了解程序在 Linux
上運行的過程,感受收穫仍是不少的。html
基於安全考慮,只有 Linux內核
才能權限去訪問計算機的硬件,Linux內核
會提供一些接口(系統調用)讓咱們能夠和硬件交互。不過數據通常都是從硬件
到內核態
,再從 Linux內核
複製到 用戶態
進程的內存空間中,這樣進程才能對讀取的數據進行處理。java
本文內容:node
虛擬文件系統(Virtual File System,簡稱VFS)是Linux內核的子系統之一,它爲操做文件(普通文件,socket 等)提供了統一的接口,屏蔽不一樣的硬件差別和操做細節。咱們只需調用 open
、read
、write
、close
、fsync
這些系統調用,達到操做文件的目的。linux
咱們實際看到的 linux的目錄,實際就是 VFS 中的路徑,咱們能夠經過將硬盤中的分區掛載到 linux
中的路徑下,訪問虛擬文件系統中的路徑既能夠訪問硬盤中的內容。vim
df -i
能夠看到 VFS 中路徑掛載的分區。api
# 將分區掛載到虛擬文件系統的 /boot 目錄下
mount /dev/sda1 /boot
# 卸載分區
umount /boot
複製代碼
操做系統會將硬盤分紅兩個區域,一個是數據區,用於保存文件的數據;還有一個 Inode
區用於保存文件的元數據(文件建立者,文件建立時間,文件權限,文件大小,塊位置等)。緩存
硬盤的最小存儲單位叫作"扇區"(Sector),每一個扇區儲存512字節(至關於0.5KB)。Linux 內核
從硬盤讀取內容時,不會一個扇區一個扇區讀,而是一次性讀取多個扇區,即一次性讀取一個 塊(Block)
。文件的數據內容儲存在 塊
中。安全
基於以上介紹,能夠知道,實際一個文件必須佔有一個 Inode
和 至少一個 block
。bash
df -i
能夠查看分區中,inode
的使用狀況和分區對應 Linux
下的文件路徑。微信
查看文件的 Inode
及 塊
的基本大小(通常 4KB)
當應用程序調用系統調用 open
,會返回一個文件描述符 (簡稱 FD,File Decsriptor)。咱們能夠把 FD
理解爲文件的指針,這個指針會指向一個Inode
。多個 FD
能夠指向同一個 Inode
,FD 會維護一個對文件內容操做的偏移量(讀寫到什麼地方了)。FD
是上層應用程序使用的,Inode
是內核維護使用的。
可是進程打開的 FD
是有限制的,因此咱們須要關閉流(實際上就是釋放申請的計算機資源),否則 FD
不釋放,程序發起系統調用沒有 FD
可用就會報錯。
ulimit -n
能夠查看系統限制的進程打開 FD
的數量,當程序併發很高的時候,須要調大此值,否則會報 (Too many open files)
public class ErrorOpenFile {
public static void main(String[] args) throws IOException, InterruptedException {
final Path path = Paths.get("/root/testfileio/out.txt");
int count = 0;
while (true) {
// 爲了查看 FD 的增加,因此設置阻塞五秒
Thread.sleep(5000);
count++;
Files.newBufferedReader(path);
System.out.println("打開一個文件描述符");
}
}
}
複製代碼
/proc/pid/fd
下能夠看到一個進程打開的 FD
,其中的 0、一、2
是默認輸入(System.in),輸出(System.out),錯誤輸出(System.err),每一個程序都會有。
BufferedInputStream
比 FileInputStream
快?下面的程序,FileOutputStream
和 BufferedOutputStream
循環 10000 次,寫入相同大小的數據,FileOutputStream
用時 468 毫秒。BufferedOutputStream
用時 3 毫秒。
public class IoOperation {
static byte[] data = "1234567890\n".getBytes();
static String path = "/root/testfileio/out.txt";
static int count = 0;
public static void main(String[] args) throws Exception {
switch (args[0]) {
case "0":
testBasicFileIO();
break;
case "1":
testBufferedFileIO();
break;
default:
}
}
// 468 毫秒執行完
public static void testBasicFileIO() throws Exception {
File file = new File(path);
FileOutputStream out = new FileOutputStream(file);
final long start = System.currentTimeMillis();
while (count < 10000) {
out.write(data);
count++;
}
System.out.println(System.currentTimeMillis() - start);
out.close();
}
// 3 毫秒執行完
public static void testBufferedFileIO() throws Exception {
File file = new File(path);
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
final long start = System.currentTimeMillis();
while (count < 10000) {
out.write(data);
count++;
}
System.out.println(System.currentTimeMillis() - start);
out.close();
}
}
複製代碼
VFS
抽象出來的系統調用(open,read,write,close)是讓應用程序調用的。咱們能夠在 Linux 中使用 man open(read/write/close)
查看系統調用的意思
也能夠在 Linux 手冊 https://man7.org/linux/man-pages/dir_section_2.html
看系統調用。
ssize_t write(int fd, const void *buf, size_t count);
複製代碼
write 系統調用
,是把緩存區 buf 中的前 count 個字節寫入到 fd 中,返回的是實際寫入到文件中的字節數 ssize_t,ssize_t 可能小於 count。
write 系統調用
會觸發進程從用戶態切換到內核態,Cpu 須要保存進程用戶態的上下文(代碼執行到哪裏了,相關數據等),再執行內核代碼,執行完內核代碼,還要切換回用戶態,將進程的上下文再還原,相對來講進程態的切換是比較消耗 Cpu 資源的,咱們應該減小 Cpu 資源的切換。
# 執行上面代碼,並追蹤系統調用
strace -ff -o /root/testfileio/out java com.fly.io.IoOperation $1
複製代碼
FileInputStream
會調用 10000 次系統調用,進程用戶態到內核態切換了 10000 次,因此代碼執行時間比較長。
BufferedOutputStream
有一個 8192
字節的緩衝區,當調用 BufferedOutputStream.write
會先寫入這個緩衝區,在這個緩衝區滿的時候,會將這個緩衝區的數據發起系統調用,這樣減小了系統調用,因此用時比較少。
文件數據的持久化,也被稱爲 落盤
。內存
的速度是 硬盤
N 倍,他倆不是一個量級的。 因此 Linux
引入 Page Cache
來做爲數據的緩存,當 Page Cache
被修改以後變爲了 Dirty Page
,Linux 會在適當時機(能夠經過參數調節),將髒頁的數據,刷新到硬盤中。也能夠調用系統調用(fsync),將髒頁刷新到硬盤。
當 JAVA 程序
調用 FileOutputStream.write
的時候,實際是將用戶態的數據,寫入到了內核態中的 Page Cache
(一個 Page Cache 大小爲 4KB 左右),當咱們調用 FileOutputStream.close
的時候,實際只是調用了系統調用 close
,而沒有落盤,這時對計算機斷電,數據是沒有持久化的。
當咱們調用了 FileOutputStream.getFD().sync()
會觸發系統調用 fsync
,將數據落盤。
Linux 內核進行 Io 調度,來控制數據落盤,時機是:
sync(2)
、fsync(2)
、fdatasync(2)
系統調用時,內核會執行相應的寫回操做。一下是內核參數配置,進行控制內核的調度
sysctl -a | grep dirty
能夠查看當前系統生效的配置
#若髒頁佔總物理內存10%以上,則觸發flush把髒數據寫回磁盤。內核後臺線程寫。
vm.dirty_background_ratio = 10
vm.dirty_background_bytes = 1048576
# 向內存寫 pagecage 時,內核判斷當前髒頁佔用物理內存的百分比,當超過這個值, 內核會阻塞掉寫操做,並開始刷新髒頁
vm.dirty_ratio = 10
vm.dirty_bytes = 1048576
# flush每隔5秒執行一次
vm.dirty_writeback_centisecs = 5000
#內存中駐留30秒以上的髒數據將由flush在下一次執行時寫入磁盤
vm.dirty_expire_centisecs = 30000
複製代碼
代碼驗證 FileOutputStream.close
不會引發數據的落盤。爲避免 Linux Io 調度的影響,我修改了內核的配置參數,這樣數據只要沒有調用系統調用 fsync
就不會觸發系統調用。
# 編輯配置文件,將參數配置填入文件中
vim /etc/sysctl.conf
# 使配置生效
sysctl -p
複製代碼
vm.dirty_background_ratio = 90
vm.dirty_ratio = 90
vm.dirty_expire_centisecs = 300000
vm.dirty_writeback_centisecs = 50000
複製代碼
代碼的邏輯爲:往一個文件中寫數據,而後關閉流,可是阻塞程序中止,程序中止,數據會刷新到磁盤中,而後模擬斷電關閉虛擬機。
當打印 沒有落盤的時候
,cat /root/testfileio/out.txt
是能夠看到數據的,當我斷電重啓以後,數據就沒有了。說明 close
不能觸發數據的落盤。
public class IoOperation1 {
static byte[] data = "1234567890\n".getBytes();
static String path = "/root/testfileio/out.txt";
static int count = 0;
public static void main(String[] args) throws Exception {
File file = new File(path);
final FileOutputStream out = new FileOutputStream(file);
while (count < 10) {
out.write(data);
count++;
}
out.close();
System.out.println("沒有落盤");
Thread.sleep(1000000);
}
}
複製代碼
當咱們調用系統調用進行落盤的時候,斷電重啓虛擬機,發現 out.txt
是有數據的。
public class IoOperation1 {
static byte[] data = "1234567890\n".getBytes();
static String path = "/root/testfileio/out.txt";
static int count = 0;
public static void main(String[] args) throws Exception {
File file = new File(path);
final FileOutputStream out = new FileOutputStream(file);
while (count < 10) {
out.write(data);
count++;
}
// 發起了系統調用 fsync,進行數據的落盤
out.getFD().sync();
out.close();
System.out.println("落盤");
Thread.sleep(1000000);
}
}
複製代碼
本文由 張攀欽的博客 www.mflyyou.cn/ 創做。 可自由轉載、引用,但需署名做者且註明文章出處。
如轉載至微信公衆號,請在文末添加做者公衆號二維碼。微信公衆號名稱:Mflyyou