從Linux內核理解Java中的IO

前言

剛接觸 Java IO 的時候, 一直有一個 困惑:爲何 BufferedInputStreamFileInputStream 快? 隨着對 Linux 瞭解,這個問題也獲得解決。最近也在看 Linux 內核 方面的書,想了解程序在 Linux 上運行的過程,感受收穫仍是不少的。html

基於安全考慮,只有 Linux內核 才能權限去訪問計算機的硬件,Linux內核會提供一些接口(系統調用)讓咱們能夠和硬件交互。不過數據通常都是從硬件內核態 ,再從 Linux內核 複製到 用戶態 進程的內存空間中,這樣進程才能對讀取的數據進行處理。java

image-20200704231239764

本文內容:node

  • Linux 中的虛擬文件系統介紹
  • Page Cache 和 Dirty Page
  • Java api 寫入的數據,何時會被刷新到磁盤中

Linux 中 虛擬文件系統(VFS)

虛擬文件系統(Virtual File System,簡稱VFS)是Linux內核的子系統之一,它爲操做文件(普通文件,socket 等)提供了統一的接口,屏蔽不一樣的硬件差別和操做細節。咱們只需調用 openreadwriteclosefsync 這些系統調用,達到操做文件的目的。linux

咱們實際看到的 linux的目錄,實際就是 VFS 中的路徑,咱們能夠經過將硬盤中的分區掛載到 linux 中的路徑下,訪問虛擬文件系統中的路徑既能夠訪問硬盤中的內容。vim

df -i 能夠看到 VFS 中路徑掛載的分區。api

image-20200704233624173

# 將分區掛載到虛擬文件系統的 /boot 目錄下
mount /dev/sda1 /boot

# 卸載分區
umount /boot
複製代碼

操做系統會將硬盤分紅兩個區域,一個是數據區,用於保存文件的數據;還有一個 Inode 區用於保存文件的元數據(文件建立者,文件建立時間,文件權限,文件大小,塊位置等)。緩存

硬盤的最小存儲單位叫作"扇區"(Sector),每一個扇區儲存512字節(至關於0.5KB)。Linux 內核 從硬盤讀取內容時,不會一個扇區一個扇區讀,而是一次性讀取多個扇區,即一次性讀取一個 塊(Block)。文件的數據內容儲存在 中。安全

基於以上介紹,能夠知道,實際一個文件必須佔有一個 Inode 和 至少一個 blockbash

df -i 能夠查看分區中,inode 的使用狀況和分區對應 Linux 下的文件路徑。微信

查看文件的 Inode 的基本大小(通常 4KB)

image-20200705002110818

當應用程序調用系統調用 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),每一個程序都會有。

image-20200705004254331

爲何 BufferedInputStreamFileInputStream 快?

下面的程序,FileOutputStreamBufferedOutputStream 循環 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
複製代碼

image-20200705122935804

FileInputStream 會調用 10000 次系統調用,進程用戶態到內核態切換了 10000 次,因此代碼執行時間比較長。

BufferedOutputStream 有一個 8192 字節的緩衝區,當調用 BufferedOutputStream.write 會先寫入這個緩衝區,在這個緩衝區滿的時候,會將這個緩衝區的數據發起系統調用,這樣減小了系統調用,因此用時比較少。

Page Cache 和 Dirty Page

文件數據的持久化,也被稱爲 落盤內存 的速度是 硬盤 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,將數據落盤。

image-20200704201130013

Linux 內核進行 Io 調度,來控制數據落盤,時機是:

  1. 當空閒內存低於一個特定的閾值時,內核必須將髒頁寫回磁盤,以便釋放內存。
  2. 當髒頁在內存中駐留時間超過一個特定的閾值時,內核必須將超時的髒頁寫回磁盤吧
  3. 用戶進程調用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

相關文章
相關標籤/搜索