本節主要學習內核中 pagecache 頁緩存、mmap 內存映射、Java 文件系統 BIO、NIO、內存緩存區的作用。
1. pagecache
本節主要驗證 Java 程序寫入文件時,pagecache 的使用、io、nio、內存中緩存區的作用。
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);
}
}
這個程序提供了三個方法:
- testBasicFileIO() 方法是直接通過 FileOutputStream 往 out.txt 文件中寫數據,每次寫入都會調用系統調用
- testBufferedFileIO() 方法是直接通過 BufferedOutputStream 往 out.txt 文件中寫數據,它在 jvm 堆內存上開闢了 8kb 的緩存區,當緩存區滿了再調用系統調用
- testRandomAccessFileWrite() 方法是創建一個 RandomAccessFile 隨機訪問文件,有直接寫入數據到文件,也有通過 FileChannel 調用 map 方法獲取 MappedByteBuffer,然後讀取數據
修改內核:
# 後臺髒頁緩存寫入磁盤的比例
vm.dirty_background_ratio = 90
vm.dirty_background_bytes = 1048576
# 前臺頁緩存髒的比例,超過這個比例,程序會先暫停,內核先把 pagecache 寫入磁盤
vm.dirty_ratio = 90
vm.dirty_bytes = 1048576
vm.dirty_writeback_centisecs = 5000
vm.dirty_expire_centisecs = 30000
驗證下 Java 程序調用內核的系統調用:
# 編輯一個 shell 文件 mysh
[root@ecs-02 testfileio]# cat mysh
rm -fr *out*
javac OSFileIO.java
strace -ff -o out java OSFileIO $1
# 執行腳本
[root@ecs-02 testfileio]# ./mysh 0
# 執行命令觀察文件和頁緩存加載的情況變化
[root@ecs-02 testfileio]# ll
總用量 11780
-rwxr-xr-x 1 root root 68 1月 19 20:07 mysh
-rwxr-xr-x 1 root root 87 1月 21 16:06 mysh2
-rw-r--r-- 1 root root 4084 1月 21 17:48 OSFileIO.class
-rw-r--r-- 1 root root 4766 1月 19 20:05 OSFileIO.java
-rw-r--r-- 1 root root 16930 1月 21 18:47 out.883730
-rw-r--r-- 1 root root 149446 1月 21 18:47 out.883731
-rw-r--r-- 1 root root 552261 1月 21 18:47 out.883732
-rw-r--r-- 1 root root 935 1月 21 18:47 out.883733
-rw-r--r-- 1 root root 1126 1月 21 18:47 out.883734
-rw-r--r-- 1 root root 1914 1月 21 18:47 out.883735
-rw-r--r-- 1 root root 135995 1月 21 18:47 out.883736
-rw-r--r-- 1 root root 229724 1月 21 18:47 out.883737
-rw-r--r-- 1 root root 17420 1月 21 18:47 out.883738
-rw-r--r-- 1 root root 1049 1月 21 18:47 out.883739
-rw-r--r-- 1 root root 10855959 1月 21 18:47 out.883740
-rw-r--r-- 1 root root 10058 1月 21 18:47 out.883741
-rw-r--r-- 1 root root 2437 1月 21 18:47 out.883772
-rw-r--r-- 1 root root 1195 1月 21 18:47 out.883891
-rw-r--r-- 1 root root 1200 1月 21 00:43 SocketClient.class
-rw-r--r-- 1 root root 822 1月 21 00:42 SocketClient.java
-rw-r--r-- 1 root root 2236 1月 21 00:27 SocketIO.class
-rw-r--r-- 1 root root 1204 1月 21 00:22 SocketIO.java
-rw-r--r-- 1 root root 3573 1月 21 22:36 SocketIOPropertites.class
-rw-r--r-- 1 root root 3623 1月 21 22:36 SocketIOPropertites.java
-rw-r--r-- 1 root root 2890 1月 21 22:05 SocketNIO.java
[root@ecs-02 testfileio]# pcstat out.txt
+---------+----------------+------------+-----------+---------+
| Name | Size (bytes) | Pages | Cached | Percent |
|---------+----------------+------------+-----------+---------|
| out.txt | 8882790 | 2169 | 2169 | 100.000 |
+---------+----------------+------------+-----------+---------+
編寫一個 shell 腳本,執行一個 Java 進程,並且使用 strace 命令追蹤這個進程調用系統調用的信息,並且輸出信息到以 out 開頭的文件。
pcstat 是一個查看文件的 pagecache 的使用情況的工具。
可以看到這個 out.txt 文件在不斷的變大,pcstat 展示出它的 Pages 數不斷變大,以及它的 page 頁和 cache 頁的佔比情況。
隔一段時間再看這個 out.txt 文件的 pagecache 情況:
[root@ecs-02 testfileio]# pcstat out.txt
+---------+----------------+------------+-----------+---------+
| Name | Size (bytes) | Pages | Cached | Percent |
|---------+----------------+------------+-----------+---------|
| out.txt | 165076610 | 40302 | 28760 | 071.361 |
+---------+----------------+------------+-----------+---------+
發現它的 Pages 和 Cached 都在不斷的增加,Percent 比例開始下降了。這是因爲這個文件不斷的增長,它在內存中的 page 和 cache 不斷變大,但是 cache 的增長速率很慢。
關閉 Java 程序,將 out.txt 文件改名爲 ooxx.txt。
再次啓動 mysh 文件:
[root@ecs-02 testfileio]# ./mysh 1
在另一個窗口查看 out.txt 和 ooxx.txt 文件的變化和它們的 pagecacache 信息:
[root@ecs-02 testfileio]# ll -h ; pcstat out.txt ; pcstat ooxx.txt
總用量 1.3G
-rwxr-xr-x 1 root root 68 1月 19 20:07 mysh
-rwxr-xr-x 1 root root 87 1月 21 16:06 mysh2
-rw-r--r-- 1 root root 292M 1月 22 19:04 ooxx.txt
-rw-r--r-- 1 root root 4.0K 1月 22 19:14 OSFileIO.class
-rw-r--r-- 1 root root 4.7K 1月 19 20:05 OSFileIO.java
-rw-r--r-- 1 root root 17K 1月 22 19:14 out.886277
-rw-r--r-- 1 root root 5.4M 1月 22 19:14 out.886278
-rw-r--r-- 1 root root 3.2K 1月 22 19:14 out.886279
-rw-r--r-- 1 root root 905 1月 22 19:14 out.886280
-rw-r--r-- 1 root root 1.1K 1月 22 19:14 out.886281
-rw-r--r-- 1 root root 1001 1月 22 19:14 out.886282
-rw-r--r-- 1 root root 23K 1月 22 19:14 out.886283
-rw-r--r-- 1 root root 65K 1月 22 19:14 out.886284
-rw-r--r-- 1 root root 6.7K 1月 22 19:14 out.886285
-rw-r--r-- 1 root root 1019 1月 22 19:14 out.886286
-rw-r--r-- 1 root root 34K 1月 22 19:14 out.886287
-rw-r--r-- 1 root root 980 1月 22 19:14 out.886288
-rw-r--r-- 1 root root 676M 1月 22 19:14 out.txt
-rw-r--r-- 1 root root 1.2K 1月 21 00:43 SocketClient.class
-rw-r--r-- 1 root root 822 1月 21 00:42 SocketClient.java
-rw-r--r-- 1 root root 2.2K 1月 21 00:27 SocketIO.class
-rw-r--r-- 1 root root 1.2K 1月 21 00:22 SocketIO.java
-rw-r--r-- 1 root root 3.5K 1月 21 22:36 SocketIOPropertites.class
-rw-r--r-- 1 root root 3.6K 1月 21 22:36 SocketIOPropertites.java
-rw-r--r-- 1 root root 2.9K 1月 21 22:05 SocketNIO.java
+---------+----------------+------------+-----------+---------+
| Name | Size (bytes) | Pages | Cached | Percent |
|---------+----------------+------------+-----------+---------|
| out.txt | 708227072 | 172907 | 172907 | 100.000 |
+---------+----------------+------------+-----------+---------+
+----------+----------------+------------+-----------+---------+
| Name | Size (bytes) | Pages | Cached | Percent |
|----------+----------------+------------+-----------+---------|
| ooxx.txt | 306016090 | 74711 | 30925 | 041.393 |
+----------+----------------+------------+-----------+---------+
過上 10 秒再看下:
[root@ecs-02 testfileio]# ll -h ; pcstat out.txt ; pcstat ooxx.txt
總用量 2.4G
-rwxr-xr-x 1 root root 68 1月 19 20:07 mysh
-rwxr-xr-x 1 root root 87 1月 21 16:06 mysh2
-rw-r--r-- 1 root root 292M 1月 22 19:04 ooxx.txt
-rw-r--r-- 1 root root 4.0K 1月 22 19:14 OSFileIO.class
-rw-r--r-- 1 root root 4.7K 1月 19 20:05 OSFileIO.java
-rw-r--r-- 1 root root 17K 1月 22 19:14 out.886277
-rw-r--r-- 1 root root 8.7M 1月 22 19:14 out.886278
-rw-r--r-- 1 root root 4.1K 1月 22 19:14 out.886279
-rw-r--r-- 1 root root 905 1月 22 19:14 out.886280
-rw-r--r-- 1 root root 1.1K 1月 22 19:14 out.886281
-rw-r--r-- 1 root root 1001 1月 22 19:14 out.886282
-rw-r--r-- 1 root root 23K 1月 22 19:14 out.886283
-rw-r--r-- 1 root root 65K 1月 22 19:14 out.886284
-rw-r--r-- 1 root root 6.7K 1月 22 19:14 out.886285
-rw-r--r-- 1 root root 1019 1月 22 19:14 out.886286
-rw-r--r-- 1 root root 53K 1月 22 19:14 out.886287
-rw-r--r-- 1 root root 980 1月 22 19:14 out.886288
-rw-r--r-- 1 root root 1.1G 1月 22 19:14 out.txt
-rw-r--r-- 1 root root 1.2K 1月 21 00:43 SocketClient.class
-rw-r--r-- 1 root root 822 1月 21 00:42 SocketClient.java
-rw-r--r-- 1 root root 2.2K 1月 21 00:27 SocketIO.class
-rw-r--r-- 1 root root 1.2K 1月 21 00:22 SocketIO.java
-rw-r--r-- 1 root root 3.5K 1月 21 22:36 SocketIOPropertites.class
-rw-r--r-- 1 root root 3.6K 1月 21 22:36 SocketIOPropertites.java
-rw-r--r-- 1 root root 2.9K 1月 21 22:05 SocketNIO.java
+---------+----------------+------------+-----------+---------+
| Name | Size (bytes) | Pages | Cached | Percent |
|---------+----------------+------------+-----------+---------|
| out.txt | 1152053248 | 281263 | 236038 | 083.921 |
+---------+----------------+------------+-----------+---------+
+----------+----------------+------------+-----------+---------+
| Name | Size (bytes) | Pages | Cached | Percent |
|----------+----------------+------------+-----------+---------|
| ooxx.txt | 306016090 | 74711 | 0 | 000.000 |
+----------+----------------+------------+-----------+---------+
能夠得出幾個結論:
- Java 的 testBufferedFileIO() 方法寫入文件的速度快,因爲它在 jvm 的堆上使用 8KB 的緩衝區,相比於 testBasicFileIO() 方法減少了寫入磁盤的系統調用,速度更快
- out.txt文件不斷變大的過程中,它的 pagecache 會把內存中已經打開的 ooxx.txt 文件佔用的 pagecache 都給擠掉。
還有一個點,內存中的 pagecache 當被程序創建、修改之後,就會變成 dirty,那麼觸發系統的 page diry 配置參數限制時,會先把 page 寫入磁盤,對應的 dirty 就會被抹掉,然後把這個 pagecache 淘汰掉,如果這個 pagecache 還被引用就會把放入到 swap 分區。
2. mmap
再次執行 mysh 文件,使用 testRandomAccessFileWrite() 方法:
# 傳入參數 2,調用程序的 testRandomAccessFileWrite() 方法
[root@ecs-02 testfileio]# ./mysh 2
write------------
開啓另一個 ssh 窗口,查看 out.txt 文件信息:
[root@ecs-02 testfileio]# cat out.txt
hello mashibing
hello seanzhou
此時程序在阻塞,我們輸入回車
# 傳入參數 2,調用程序的 testRandomAccessFileWrite() 方法
[root@ecs-02 testfileio]# ./mysh 2
write------------
seek---------
注意輸入回車之後,程序會執行隨機讀寫 seek(4) 方法,它會執行到它內部維護這個,它會跳轉到這個文件在內存中 pagecache 指定位置,然後從這個地址上寫入數據。
並且程序開始執行獲取 FileChannel,以及執行它的 map 方法返回 MappedByteBuffer map,在這個 map 上執行 map.put("@@@".getBytes()) 方法。
再查看 out.txt 文件信息:
[root@ecs-02 testfileio]# cat out.txt
hellooxxshibing
hello seanzhou
可以發現文件的第一行 hell後邊 4 個字符被修改了,變成了 ooxx。
Java 程序中的 FileChannel 的 map() 方法返回的是這個文件對應的內核中內存 pagecache 的直接映射,它不依賴與 java 進程中堆區以及裏邊的 jvm 堆內區。
對於它的操作不需要經過 java 進程堆、jvm 堆區,不需要用戶態與內核態的切換。
Mmap 不是系統調用,但是數據會到達內核的 pagecache,曾經我們是需要 out.write() 這樣的系統調用,才能讓程序的 data 進入內核的 pagecache,曾經必須有用戶態內核態切換,mmap 的內存映射,依然是內核的 pagecache 體系所約束的!!!
換言之,丟數據。你可以去github上找一些 其他C程序員寫的jni擴展庫,使用linux內核的Direct IO,直接IO是忽略linux的pagecache 是把pagecache 交給了程序自己開闢一個字節數組當作pagecache,動用代碼邏輯來維護一致性/dirty。。。一系列複雜問題
<img src="http://picture.kaisesai.com/image-20210122192532089.png" alt="image-20210122192532089" />
3. java 文件系統 io、nio、內存中緩存區作用
Java 1.4 中的 ByteBuffer 是一個字節緩存區,有三個屬性:position、limit、capacity
<img src="http://picture.kaisesai.com/image-20210122194835056.png" alt="image-20210122194835056" style="zoom:50%;" />
它的操作方法:
- put() 添加數據
- flip() 反轉,用於讀寫交替
- get() 獲取字節數據
- compact() 壓縮
- clear() 清理