我們接著上篇文章《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫本質(zhì)(上)》的內(nèi)容繼續(xù):

10. JDK NIO 對(duì)普通文件的寫入
注意:下面的例子并不是最佳實(shí)踐,之所以這里引入 HeapByteBuffer 是為了將上篇文章的內(nèi)容和本文銜接起來。事實(shí)上,對(duì)于 IO 的操作一般都會(huì)選擇 DirectByteBuffer ,關(guān)于 DirectByteBuffer 的相關(guān)內(nèi)容筆者會(huì)在后面的文章中詳細(xì)為大家介紹。
FileChannel fileChannel = new RandomAccessFile(new File("file-read-write.txt"), "rw").getChannel();
ByteBuffer heapByteBuffer = ByteBuffer.allocate(4096);
fileChannel.write(heapByteBuffer);
在對(duì)文件進(jìn)行讀寫之前,我們需要首先利用 RandomAccessFile 在內(nèi)核中打開指定的文件 file-read-write.txt ,并獲取到它的文件描述符 fd = 5000。

本例 heapByteBuffer 中存放著需要寫入文件的內(nèi)容,隨后來到 FileChannelImpl 實(shí)現(xiàn)類調(diào)用 IOUtil 觸發(fā)底層系統(tǒng)調(diào)用 write 來寫入文件。
public class FileChannelImpl extends FileChannel {
// 前邊介紹打開的文件描述符 5000
private final FileDescriptor fd;
// NIO中用它來觸發(fā) native read 和 write 的系統(tǒng)調(diào)用
private final FileDispatcher nd;
// 讀寫文件時(shí)加鎖,前邊介紹 FileChannel 的讀寫方法均是線程安全的
private final Object positionLock = new Object();
public int write(ByteBuffer src) throws IOException {
ensureOpen();
if (!writable)
throw new NonWritableChannelException();
synchronized (positionLock) {
//寫入的字節(jié)數(shù)
int n = 0;
try {
......省略......
if (!isOpen())
return 0;
do {
n = IOUtil.write(fd, src, -1, nd);
} while ((n == IOStatus.INTERRUPTED) && isOpen());
// 返回寫入的字節(jié)數(shù)
return IOStatus.normalize(n);
} finally {
......省略......
}
}
}
}
NIO 中的所有 IO 操作全部封裝在 IOUtil 類中,而 NIO 中的 SocketChannel 以及這里介紹的 FileChannel 底層依賴的系統(tǒng)調(diào)用可能不同,這里會(huì)通過 NativeDispatcher 對(duì)具體 Channel 操作實(shí)現(xiàn)分發(fā),調(diào)用具體的系統(tǒng)調(diào)用。對(duì)于 FileChannel 來說 NativeDispatcher 的實(shí)現(xiàn)類為 FileDispatcher。對(duì)于 SocketChannel 來說 NativeDispatcher 的實(shí)現(xiàn)類為 SocketDispatcher。
public class IOUtil {
static int write(FileDescriptor fd, ByteBuffer src, long position,
NativeDispatcher nd)
throws IOException
{
// 標(biāo)記傳遞進(jìn)來的 heapByteBuffer 的 position 位置用于后續(xù)恢復(fù)
int pos = src.position();
// 獲取 heapByteBuffer 的 limit 用于計(jì)算 寫入字節(jié)數(shù)
int lim = src.limit();
assert (pos <= lim);
// 寫入的字節(jié)數(shù)
int rem = (pos <= lim ? lim - pos : 0);
// 創(chuàng)建臨時(shí)的 DirectByteBuffer,用于通過系統(tǒng)調(diào)用 write 寫入數(shù)據(jù)到內(nèi)核
ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);
try {
// 將 heapByteBuffer 中的內(nèi)容拷貝到臨時(shí) DirectByteBuffer 中
bb.put(src);
// DirectByteBuffer 切換為讀模式,用于后續(xù)發(fā)送數(shù)據(jù)
bb.flip();
// 恢復(fù) heapByteBuffer 中的 position
src.position(pos);
int n = writeFromNativeBuffer(fd, bb, position, nd);
if (n > 0) {
// 此時(shí) heapByteBuffer 中的內(nèi)容已經(jīng)發(fā)送完畢,更新它的 postion + n
// 這里表達(dá)的語義是從 heapByteBuffer 中讀取了 n 個(gè)字節(jié)并發(fā)送成功
src.position(pos + n);
}
// 返回發(fā)送成功的字節(jié)數(shù)
return n;
} finally {
// 釋放臨時(shí)創(chuàng)建的 DirectByteBuffer
Util.offerFirstTemporaryDirectBuffer(bb);
}
}
private static int writeFromNativeBuffer(FileDescriptor fd, ByteBuffer bb,
long position, NativeDispatcher nd)
throws IOException
{
int pos = bb.position();
int lim = bb.limit();
assert (pos <= lim);
// 要發(fā)送的字節(jié)數(shù)
int rem = (pos <= lim ? lim - pos : 0);
int written = 0;
if (rem == 0)
return 0;
if (position != -1) {
........省略.......
} else {
written = nd.write(fd, ((DirectBuffer)bb).address() + pos, rem);
}
if (written > 0)
// 發(fā)送完畢之后更新 DirectByteBuffer 的position
bb.position(pos + written);
// 返回寫入的字節(jié)數(shù)
return written;
}
}
在 IOUtil 中首先創(chuàng)建一個(gè)臨時(shí)的 DirectByteBuffer,然后將本例中 HeapByteBuffer 中的數(shù)據(jù)全部拷貝到這個(gè)臨時(shí)的 DirectByteBuffer 中。這個(gè) DirectByteBuffer 就是我們?cè)?IO 系統(tǒng)調(diào)用中經(jīng)常提到的用戶空間緩沖區(qū)。
隨后在 writeFromNativeBuffer 方法中通過 FileDispatcher 觸發(fā) JNI 層的
native 方法執(zhí)行底層系統(tǒng)調(diào)用 write 。
class FileDispatcherImpl extends FileDispatcher {
int write(FileDescriptor fd, long address, int len) throws IOException {
return write0(fd, address, len);
}
static native int write0(FileDescriptor fd, long address, int len)
throws IOException;
}
NIO 中關(guān)于文件 IO 相關(guān)的系統(tǒng)調(diào)用全部封裝在 JNI 層中的 FileDispatcherImpl.c 文件中。里邊定義了各種 IO 相關(guān)的系統(tǒng)調(diào)用的 native 方法。
// FileDispatcherImpl.c 文件
JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_write0(JNIEnv *env, jclass clazz,
jobject fdo, jlong address, jint len)
{
jint fd = fdval(env, fdo);
void *buf = (void *)jlong_to_ptr(address);
// 發(fā)起 write 系統(tǒng)調(diào)用進(jìn)入內(nèi)核
return convertReturnVal(env, write(fd, buf, len), JNI_FALSE);
}
系統(tǒng)調(diào)用 write 在內(nèi)核中的定義如下所示:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
struct fd f = fdget_pos(fd);
......
loff_t pos = file_pos_read(f.file);
ret = vfs_write(f.file, buf, count, &pos);
......
}
現(xiàn)在我們就從用戶空間的 JDK NIO 這一層逐步來到了內(nèi)核空間的邊界處 --- OS 系統(tǒng)調(diào)用 write 這里,馬上就要進(jìn)入內(nèi)核了。

這一次我們來看一下當(dāng)系統(tǒng)調(diào)用 write 發(fā)起之后,用戶進(jìn)程在內(nèi)核態(tài)具體做了哪些事情?
11. 從內(nèi)核角度探秘文件寫入本質(zhì)
現(xiàn)在讓我們?cè)俅芜M(jìn)入內(nèi)核,來看一下內(nèi)核中具體是如何處理文件寫入操作的,這個(gè)過程會(huì)比文件讀取要復(fù)雜很多,大家需要有點(diǎn)耐心~~
再次強(qiáng)調(diào)一下,本文所舉示例中用到的 HeapByteBuffer 只是為了與上篇文章 《一步一圖帶你深入剖析 JDK NIO ByteBuffer 在不同字節(jié)序下的設(shè)計(jì)與實(shí)現(xiàn)》介紹的內(nèi)容做出呼應(yīng),并不是最佳實(shí)踐。筆者會(huì)在后續(xù)的文章中一步一步為大家展開這塊內(nèi)容的最佳實(shí)踐。
11.1 Buffered IO

使用 JDK NIO 中的 HeapByteBuffer 在對(duì)文件進(jìn)行寫入的過程,主要分為如下幾個(gè)核心步驟:
首先會(huì)在用戶空間的 JDK 層將位于 JVM 堆中的 HeapByteBuffer 中的待寫入數(shù)據(jù)拷貝到位于 OS 堆中的 DirectByteBuffer 中。這里發(fā)生第一次拷貝
隨后 NIO 會(huì)在用戶態(tài)通過系統(tǒng)調(diào)用 write 發(fā)起文件寫入的請(qǐng)求,此時(shí)發(fā)生第一次上下文切換。
隨后用戶進(jìn)程進(jìn)入內(nèi)核態(tài),在虛擬文件系統(tǒng)層調(diào)用 vfs_write 觸發(fā)對(duì) page cache 寫入的操作。相關(guān)操作封裝在 generic_perform_write 函數(shù)中。這個(gè)后面筆者會(huì)細(xì)講,這里我們只關(guān)注核心總體流程。
內(nèi)核調(diào)用 iov_iter_copy_from_user_atomic 函數(shù)將用戶空間緩沖區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)拷貝到 page cache 中。發(fā)生第二次拷貝動(dòng)作,這里的操作就是我們常說的 CPU 拷貝。
當(dāng)待寫入數(shù)據(jù)拷貝到 page cache 中時(shí),內(nèi)核會(huì)將對(duì)應(yīng)的文件頁標(biāo)記為臟頁。
臟頁表示內(nèi)存中的數(shù)據(jù)要比磁盤中對(duì)應(yīng)文件數(shù)據(jù)要新。
- 此時(shí)內(nèi)核會(huì)根據(jù)一定的閾值判斷是否要對(duì) page cache 中的臟頁進(jìn)行回寫,如果不需要同步回寫,進(jìn)程直接返回。文件寫入操作完成。這里發(fā)生第二次上下文切換
從這里我們看到在對(duì)文件進(jìn)行寫入時(shí),內(nèi)核只會(huì)將數(shù)據(jù)寫入到 page cache 中。整個(gè)寫入過程就完成了,并不會(huì)寫到磁盤中。
- 臟頁回寫又會(huì)根據(jù)臟頁數(shù)量在內(nèi)存中的占比分為:進(jìn)程同步回寫和內(nèi)核異步回寫。當(dāng)臟頁太多了,進(jìn)程自己都看不下去的時(shí)候,會(huì)同步回寫內(nèi)存中的臟頁,直到回寫完畢才會(huì)返回。在回寫的過程中會(huì)發(fā)生第三次拷貝,通過DMA 將 page cache 中的臟頁寫入到磁盤中。
所謂內(nèi)核異步回寫就是內(nèi)核會(huì)定時(shí)喚醒一個(gè) flusher 線程,定時(shí)將內(nèi)存中的臟頁回寫到磁盤中。這部分的內(nèi)容筆者會(huì)在后續(xù)的章節(jié)中詳細(xì)講解。
在 NIO 使用 HeapByteBuffer 在對(duì)文件進(jìn)行寫入的過程中,一般只會(huì)發(fā)生兩次拷貝動(dòng)作和兩次上下文切換,因?yàn)閮?nèi)核將數(shù)據(jù)拷貝到 page cache 中后,文件寫入過程就結(jié)束了。如果臟頁在內(nèi)存中的占比太高了,達(dá)到了進(jìn)程同步回寫的閾值,那么就會(huì)發(fā)生第三次 DMA 拷貝,將臟頁數(shù)據(jù)回寫到磁盤文件中。
如果進(jìn)程需要同步回寫臟頁數(shù)據(jù)時(shí),在本例中是要發(fā)生三次拷貝動(dòng)作。但一般情況下,在本例中只會(huì)發(fā)生兩次,沒有第三次的 DMA 拷貝。
11.2 Direct IO
在 JDK 10 中我們可以通過如下的方式采用 Direct IO 模式打開文件:
FileChannel fc = FileChannel.open(p, StandardOpenOption.WRITE,
ExtendedOpenOption.DIRECT)

在 Direct IO 模式下的文件寫入操作最明顯的特點(diǎn)就是繞過 page cache 直接通過 DMA 拷貝將用戶空間緩沖區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)寫入到磁盤中。
同樣發(fā)生兩次上下文切換、
在本例中只會(huì)發(fā)生兩次數(shù)據(jù)拷貝,第一次是將 JVM 堆中的 HeapByteBuffer 中的待寫入數(shù)據(jù)拷貝到位于 OS 堆中的 DirectByteBuffer 中。第二次則是 DMA 拷貝,將用戶空間緩沖區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)寫入到磁盤中。
12. Talk is cheap ! show you the code
下面是系統(tǒng)調(diào)用 write 在內(nèi)核中的完整定義:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
// 根據(jù)文件描述符獲取文件對(duì)應(yīng)的 struct file 結(jié)構(gòu)
struct fd f = fdget_pos(fd);
......
// 獲取當(dāng)前文件的寫入位置 offset
loff_t pos = file_pos_read(f.file);
// 進(jìn)入虛擬文件系統(tǒng)層,執(zhí)行具體的文件寫入操作
ret = vfs_write(f.file, buf, count, &pos);
......
}
這里和文件讀取的流程基本一樣,也是通過 vfs_write 進(jìn)入虛擬文件系統(tǒng)層。
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
loff_t *pos)
{
if (file->f_op->write)
return file->f_op->write(file, p, count, pos);
else if (file->f_op->write_iter)
return new_sync_write(file, p, count, pos);
else
return -EINVAL;
}
在虛擬文件系統(tǒng)層,通過 struct file 中定義的函數(shù)指針 file_operations 在具體的文件系統(tǒng)中執(zhí)行相應(yīng)的文件 IO 操作。我們還是以 ext4 文件系統(tǒng)為例。
struct file {
const struct file_operations *f_op;
}
在 ext4 文件系統(tǒng)中 .write_iter 函數(shù)指針指向的是 ext4_file_write_iter 函數(shù)執(zhí)行具體的文件寫入操作。
const struct file_operations ext4_file_operations = {
......省略........
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
......省略.........
}

由于 ext4_file_operations 中只定義了 .write_iter 函數(shù)指針,所以在 __vfs_write 函數(shù)中流程進(jìn)入 else if {......} 分支來到 new_sync_write 函數(shù)中:
static ssize_t new_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
// 將 DirectByteBuffer 以及要寫入的字節(jié)數(shù)封裝進(jìn) iovec 結(jié)構(gòu)體中
struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
// 用來封裝文件 IO 相關(guān)操作的狀態(tài)和進(jìn)度信息:
struct kiocb kiocb;
// 用來封裝用用戶緩存區(qū) DirectByteBuffer 的相關(guān)的信息
struct iov_iter iter;
ssize_t ret;
// 利用文件 struct file 初始化 kiocb 結(jié)構(gòu)體
init_sync_kiocb(&kiocb, filp);
// 設(shè)置文件寫入偏移位置
kiocb.ki_pos = (ppos ? *ppos : 0);
iov_iter_init(&iter, WRITE, &iov, 1, len);
// 調(diào)用 ext4_file_write_iter
ret = call_write_iter(filp, &kiocb, &iter);
BUG_ON(ret == -EIOCBQUEUED);
if (ret > 0 && ppos)
*ppos = kiocb.ki_pos;
return ret;
}
在文件讀取的相關(guān)章節(jié)中,我們介紹了用于封裝傳遞進(jìn)來的用戶空間緩沖區(qū) DirectByteBuffer 相關(guān)信息的 struct iovec 結(jié)構(gòu)體,也介紹了用于封裝文件 IO 相關(guān)操作的狀態(tài)和進(jìn)度信息的 struct kiocb 結(jié)構(gòu)體,這里筆者不在贅述。
不過在這里筆者還是想強(qiáng)調(diào)的一下,內(nèi)核中一般會(huì)使用 struct iov_iter 結(jié)構(gòu)體對(duì) struct iovec 進(jìn)行包裝,iov_iter 中包含多個(gè) iovec。
struct iov_iter {
......省略.....
const struct iovec *iov;
}
這是為了兼容 readv() ,writev() 等系統(tǒng)調(diào)用,它允許用戶使用多個(gè)緩存區(qū)去讀取文件中的數(shù)據(jù)或者從多個(gè)緩沖區(qū)中寫入數(shù)據(jù)到文件中。
JDK NIO Channel 支持的 Scatter 操作底層原理就是 readv 系統(tǒng)調(diào)用。
JDK NIO Channel 支持的 Gather 操作底層原理就是 writev 系統(tǒng)調(diào)用。
FileChannel fileChannel = new RandomAccessFile(new File("file-read-write.txt"), "rw").getChannel();
ByteBuffer heapByteBuffer1 = ByteBuffer.allocate(4096);
ByteBuffer heapByteBuffer2 = ByteBuffer.allocate(4096);
ByteBuffer[] gather = { heapByteBuffer1, heapByteBuffer2 };
fileChannel.write(gather);
最終在 call_write_iter 中觸發(fā) ext4_file_write_iter 的調(diào)用,從虛擬文件系統(tǒng)層進(jìn)入到具體文件系統(tǒng) ext4 中。
static inline ssize_t call_write_iter(struct file *file, struct kiocb *kio,
struct iov_iter *iter)
{
return file->f_op->write_iter(kio, iter);
}
static ssize_t
ext4_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
..........省略..........
ret = __generic_file_write_iter(iocb, from);
return ret;
}
我們看到在文件系統(tǒng) ext4 中調(diào)用的是 __generic_file_write_iter 方法。內(nèi)核針對(duì)文件寫入的所有邏輯都封裝在這里。
ssize_t __generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct file *file = iocb->ki_filp;
struct address_space * mapping = file->f_mapping;
struct inode *inode = mapping->host;
ssize_t written = 0;
ssize_t err;
ssize_t status;
........省略基本校驗(yàn)邏輯和更新文件原數(shù)據(jù)邏輯........
if (iocb->ki_flags & IOCB_DIRECT) {
loff_t pos, endbyte;
// Direct IO
written = generic_file_direct_write(iocb, from);
.......省略......
} else {
// Buffered IO
written = generic_perform_write(file, from, iocb->ki_pos);
if (likely(written > 0))
iocb->ki_pos += written;
}
.......省略......
// 返回寫入文件的字節(jié)數(shù) 或者 錯(cuò)誤
return written ? written : err;
}
這里和我們?cè)诮榻B文件讀取時(shí)候提到的 generic_file_read_iter 函數(shù)中的邏輯是一樣的。都會(huì)處理 Direct IO 和 Buffered IO 的場(chǎng)景。
這里對(duì)于 Direct IO 的處理都是一樣的,在 generic_file_direct_write 中也是會(huì)調(diào)用 address_space 中的 address_space_operations 定義的 .direct_IO 函數(shù)指針來繞過 page cache 直接寫入磁盤。
struct address_space {
const struct address_space_operations *a_ops;
}
written = mapping->a_ops->direct_IO(iocb, from);

在 ext4 文件系統(tǒng)中實(shí)現(xiàn) Direct IO 的函數(shù)是 ext4_direct_IO,這里直接會(huì)調(diào)用到塊設(shè)備驅(qū)動(dòng)層,通過 do_blockdev_direct_IO 直接將用戶空間緩沖區(qū) DirectByteBuffer 中的內(nèi)容寫入磁盤中。do_blockdev_direct_IO 函數(shù)會(huì)等到所有的 Direct IO 寫入到磁盤之后才會(huì)返回。
static const struct address_space_operations ext4_aops = {
.direct_IO = ext4_direct_IO,
};
Direct IO 是由 DMA 直接從用戶空間緩沖區(qū) DirectByteBuffer 中拷貝到磁盤中。
下面我們主要介紹下 Buffered IO 的寫入邏輯 generic_perform_write 方法。
12.1 Buffered IO

ssize_t generic_perform_write(struct file *file,
struct iov_iter *i, loff_t pos)
{
// 獲取 page cache。數(shù)據(jù)將會(huì)被寫入到這里
struct address_space *mapping = file->f_mapping;
// 獲取 page cache 相關(guān)的操作函數(shù)
const struct address_space_operations *a_ops = mapping->a_ops;
long status = 0;
ssize_t written = 0;
unsigned int flags = 0;
do {
// 用于引用要寫入的文件頁
struct page *page;
// 要寫入的文件頁在 page cache 中的 index
unsigned long offset; /* Offset into pagecache page */
unsigned long bytes; /* Bytes to write to page */
size_t copied; /* Bytes copied from user */
offset = (pos & (PAGE_SIZE - 1));
bytes = min_t(unsigned long, PAGE_SIZE - offset,
iov_iter_count(i));
again:
// 檢查用戶空間緩沖區(qū) DirectByteBuffer 地址是否有效
if (unlikely(iov_iter_fault_in_readable(i, bytes))) {
status = -EFAULT;
break;
}
// 從 page cache 中獲取要寫入的文件頁并準(zhǔn)備記錄文件元數(shù)據(jù)日志工作
status = a_ops->write_begin(file, mapping, pos, bytes, flags,
&page, &fsdata);
// 將用戶空間緩沖區(qū) DirectByteBuffer 中的數(shù)據(jù)拷貝到 page cache 中的文件頁中
copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
flush_dcache_page(page);
// 將寫入的文件頁標(biāo)記為臟頁并完成文件元數(shù)據(jù)日志的寫入
status = a_ops->write_end(file, mapping, pos, bytes, copied,
page, fsdata);
// 更新文件 ppos
pos += copied;
written += copied;
// 判斷是否需要回寫臟頁
balance_dirty_pages_ratelimited(mapping);
} while (iov_iter_count(i));
// 返回寫入字節(jié)數(shù)
return written ? written : status;
}
由于本文中筆者是以 ext4 文件系統(tǒng)為例來介紹文件的讀寫流程,本小節(jié)中介紹的文件寫入流程涉及到與文件系統(tǒng)相關(guān)的兩個(gè)操作:write_begin,write_end。這兩個(gè)函數(shù)在不同的文件系統(tǒng)中都有不同的實(shí)現(xiàn),在不同的文件系統(tǒng)中,寫入每一個(gè)文件頁都需要調(diào)用一次 write_begin,write_end 這兩個(gè)方法。
static const struct address_space_operations ext4_aops = {
......省略.......
.write_begin = ext4_write_begin,
.write_end = ext4_write_end,
......省略.......
}
下圖為本文中涉及文件讀寫的所有內(nèi)核數(shù)據(jù)結(jié)構(gòu)圖:

經(jīng)過前邊介紹文件讀取的章節(jié)我們知道在讀取文件的時(shí)候都是先從 page cache 中讀取,如果 page cache 正好緩存了文件頁就直接返回。如果沒有在進(jìn)行磁盤 IO。
文件的寫入過程也是一樣,內(nèi)核會(huì)將用戶緩沖區(qū) DirectByteBuffer 中的待寫數(shù)據(jù)先拷貝到 page cache 中,寫完就直接返回。后續(xù)內(nèi)核會(huì)根據(jù)一定的規(guī)則把這些文件頁回寫到磁盤中。
從這個(gè)過程我們可以看出,內(nèi)核將數(shù)據(jù)先是寫入 page cache 中但是不會(huì)立刻寫入磁盤中,如果突然斷電或者系統(tǒng)崩潰就可能導(dǎo)致文件系統(tǒng)處于不一致的狀態(tài)。
為了解決這種場(chǎng)景,于是 linux 內(nèi)核引入了 ext3 , ext4 等日志文件系統(tǒng)。而日志文件系統(tǒng)比非日志文件系統(tǒng)在磁盤中多了一塊 Journal 區(qū)域,Journal 區(qū)域就是存放管理文件元數(shù)據(jù)和文件數(shù)據(jù)操作日志的磁盤區(qū)域。
文件元數(shù)據(jù)的日志用于恢復(fù)文件系統(tǒng)的一致性。
文件數(shù)據(jù)的日志用于防止系統(tǒng)故障造成的文件內(nèi)容損壞,
ext3 , ext4 等日志文件系統(tǒng)分為三種模式,我們可以在掛載的時(shí)候選擇不同的模式。
日志模式(Journal 模式):這種模式在將數(shù)據(jù)寫入文件系統(tǒng)前,必須等待元數(shù)據(jù)和數(shù)據(jù)的日志已經(jīng)落盤才能發(fā)揮作用。這樣性能比較差,但是最安全。
順序模式(Order 模式): 在 Order 模式不會(huì)記錄數(shù)據(jù)的日志,只會(huì)記錄元數(shù)據(jù)的日志,但是在寫元數(shù)據(jù)的日志前,必須先確保數(shù)據(jù)已經(jīng)落盤。這樣可以減少文件內(nèi)容損壞的機(jī)會(huì),這種模式是對(duì)性能的一種折中,是默認(rèn)模式。
回寫模式(WriteBack 模式):WriteBack 模式 和 Order 模式一樣它們都不會(huì)記錄數(shù)據(jù)的日志,只會(huì)記錄元數(shù)據(jù)的日志,不同的是在 WriteBack 模式下不會(huì)保證數(shù)據(jù)比元數(shù)據(jù)先落盤。這個(gè)性能最好,但是最不安全。
而 write_begin,write_end 正是對(duì)文件系統(tǒng)中相關(guān)日志的操作,在 ext4 文件系統(tǒng)中對(duì)應(yīng)的是 ext4_write_begin,ext4_write_end。下面我們就來看一下在 Buffered IO 模式下對(duì)于 ext4 文件系統(tǒng)中的文件寫入的核心步驟。
12.2 ext4_write_begin
static int ext4_write_begin(struct file *file, struct address_space *mapping,
loff_t pos, unsigned len, unsigned flags,
struct page **pagep, void **fsdata)
{
struct inode *inode = mapping->host;
struct page *page;
pgoff_t index;
...........省略.......
retry_grab:
// 從 page cache 中查找要寫入文件頁
page = grab_cache_page_write_begin(mapping, index, flags);
if (!page)
return -ENOMEM;
unlock_page(page);
retry_journal:
// 相關(guān)日志的準(zhǔn)備工作
handle = ext4_journal_start(inode, EXT4_HT_WRITE_PAGE, needed_blocks);
...........省略.......
在寫入文件數(shù)據(jù)之前,內(nèi)核在 ext4_write_begin 方法中調(diào)用 ext4_journal_start 方法做一些相關(guān)日志的準(zhǔn)備工作。
還有一個(gè)重要的事情是在 grab_cache_page_write_begin 方法中從 page cache 中根據(jù) index 查找要寫入數(shù)據(jù)的文件緩存頁。
struct page *grab_cache_page_write_begin(struct address_space *mapping,
pgoff_t index, unsigned flags)
{
struct page *page;
int fgp_flags = FGP_LOCK|FGP_WRITE|FGP_CREAT;
// 在 page cache 中查找寫入數(shù)據(jù)的緩存頁
page = pagecache_get_page(mapping, index, fgp_flags,
mapping_gfp_mask(mapping));
if (page)
wait_for_stable_page(page);
return page;
}
通過 pagecache_get_page 在 page cache 中查找要寫入數(shù)據(jù)的緩存頁。如果緩存頁不在 page cache 中,內(nèi)核則會(huì)首先會(huì)在物理內(nèi)存中分配一個(gè)內(nèi)存頁,然后將新分配的內(nèi)存頁加入到 page cache 中。
相關(guān)的查找過程筆者已經(jīng)在 《8. page cache 中查找緩存頁》小節(jié)中詳細(xì)介紹過了,這里不在贅述。
12.3 iov_iter_copy_from_user_atomic
這里就是寫入過程的關(guān)鍵所在,圖中描述的 CPU 拷貝是將用戶空間緩存區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)拷貝到內(nèi)核里的 page cache 中,這個(gè)過程就發(fā)生在這里。
size_t iov_iter_copy_from_user_atomic(struct page *page,
struct iov_iter *i, unsigned long offset, size_t bytes)
{
// 將緩存頁臨時(shí)映射到內(nèi)核虛擬地址空間的高端地址上
char *kaddr = kmap_atomic(page),
*p = kaddr + offset;
// 將用戶緩存區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)拷貝到文件緩存頁中
iterate_all_kinds(i, bytes, v,
copyin((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len),
memcpy_from_page((p += v.bv_len) - v.bv_len, v.bv_page,
v.bv_offset, v.bv_len),
memcpy((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len)
)
// 解除內(nèi)核虛擬地址空間與緩存頁之間的臨時(shí)映射,這里映射只是為了拷貝數(shù)據(jù)用
kunmap_atomic(kaddr);
return bytes;
}
但是這里不能直接進(jìn)行拷貝,因?yàn)榇藭r(shí)從 page cache 中取出的緩存頁 page 是物理地址,而在內(nèi)核中是不能夠直接操作物理地址的,只能操作虛擬地址。
那怎么辦呢?所以就需要調(diào)用 kmap_atomic 將緩存頁臨時(shí)映射到內(nèi)核空間的一段虛擬地址上,然后將用戶空間緩存區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)通過這段映射的虛擬地址拷貝到 page cache 中的相應(yīng)緩存頁中。這時(shí)文件的寫入操作就已經(jīng)完成了。
從這里我們看出,內(nèi)核對(duì)于文件的寫入只是將數(shù)據(jù)寫入到 page cache 中就完事了并沒有真正地寫入磁盤。
由于是臨時(shí)映射,所以在拷貝完成之后,調(diào)用 kunmap_atomic 將這段映射再解除掉。
12.4 ext4_write_end
static int ext4_write_end(struct file *file,
struct address_space *mapping,
loff_t pos, unsigned len, unsigned copied,
struct page *page, void *fsdata)
{
handle_t *handle = ext4_journal_current_handle();
struct inode *inode = mapping->host;
......省略.......
// 將寫入的緩存頁在 page cache 中標(biāo)記為臟頁
copied = block_write_end(file, mapping, pos, len, copied, page, fsdata);
......省略.......
// 完成相關(guān)日志的寫入
ret2 = ext4_journal_stop(handle);
......省略.......
}
在這里會(huì)對(duì)文件的寫入流程做一些收尾的工作,比如在 block_write_end 方法中會(huì)調(diào)用 mark_buffer_dirty 將寫入的緩存頁在 page cache 中標(biāo)記為臟頁。后續(xù)內(nèi)核會(huì)根據(jù)一定的規(guī)則將 page cache 中的這些臟頁回寫進(jìn)磁盤中。
具體的標(biāo)記過程筆者已經(jīng)在《7.1 radix_tree 的標(biāo)記》小節(jié)中詳細(xì)介紹過了,這里不在贅述。

另一個(gè)核心的步驟就是調(diào)用 ext4_journal_stop 完成相關(guān)日志的寫入。這里日志也只是會(huì)先寫到緩存里,不會(huì)直接落盤。
12.5 balance_dirty_pages_ratelimited
當(dāng)進(jìn)程將待寫數(shù)據(jù)寫入 page cache 中之后,相應(yīng)的緩存頁就變?yōu)榱伺K頁,我們需要找一個(gè)時(shí)機(jī)將這些臟頁回寫到磁盤中。防止斷電導(dǎo)致數(shù)據(jù)丟失。
本小節(jié)我們主要聚焦于臟頁回寫的主體流程,相應(yīng)細(xì)節(jié)部分以及內(nèi)核對(duì)臟頁的回寫時(shí)機(jī)我們放在下一小節(jié)中在詳細(xì)為大家介紹。
void balance_dirty_pages_ratelimited(struct address_space *mapping)
{
struct inode *inode = mapping->host;
struct backing_dev_info *bdi = inode_to_bdi(inode);
struct bdi_writeback *wb = NULL;
int ratelimit;
......省略......
if (unlikely(current->nr_dirtied >= ratelimit))
balance_dirty_pages(mapping, wb, current->nr_dirtied);
......省略......
}
在 balance_dirty_pages_ratelimited 會(huì)判斷如果臟頁數(shù)量在內(nèi)存中達(dá)到了一定的規(guī)模 ratelimit 就會(huì)觸發(fā) balance_dirty_pages 回寫臟頁邏輯。
static void balance_dirty_pages(struct address_space *mapping,
struct bdi_writeback *wb,
unsigned long pages_dirtied)
{
.......根據(jù)內(nèi)核異步回寫閾值判斷是否需要喚醒 flusher 線程異步回寫臟頁...
if (nr_reclaimable > gdtc->bg_thresh)
wb_start_background_writeback(wb);
}
如果達(dá)到了臟頁回寫的條件,那么內(nèi)核就會(huì)喚醒 flusher 線程去將這些臟頁異步回寫到磁盤中。
void wb_start_background_writeback(struct bdi_writeback *wb)
{
/*
* We just wake up the flusher thread. It will perform background
* writeback as soon as there is no other work to do.
*/
wb_wakeup(wb);
}
13. 內(nèi)核回寫臟頁的觸發(fā)時(shí)機(jī)
經(jīng)過前邊對(duì)文件寫入過程的介紹我們看到,用戶進(jìn)程在對(duì)文件進(jìn)行寫操作的時(shí)候只是將待寫入數(shù)據(jù)從用戶空間的緩沖區(qū) DirectByteBuffer 寫入到內(nèi)核中的 page cache 中就結(jié)束了。后面內(nèi)核會(huì)對(duì)臟頁進(jìn)行延時(shí)寫入到磁盤中。
當(dāng) page cache 中的緩存頁比磁盤中對(duì)應(yīng)的文件頁的數(shù)據(jù)要新時(shí),就稱這些緩存頁為臟頁。
延時(shí)寫入的好處就是進(jìn)程可以多次頻繁的對(duì)文件進(jìn)行寫入但都是寫入到 page cache 中不會(huì)有任何磁盤 IO 發(fā)生。隨后內(nèi)核可以將進(jìn)程的這些多次寫入操作轉(zhuǎn)換為一次磁盤 IO ,將這些寫入的臟頁一次性刷新回磁盤中,這樣就把多次磁盤 IO 轉(zhuǎn)換為一次磁盤 IO 極大地提升文件 IO 的性能。
那么內(nèi)核在什么情況下才會(huì)去觸發(fā) page cache 中的臟頁回寫呢?
內(nèi)核在初始化的時(shí)候,會(huì)創(chuàng)建一個(gè) timer 定時(shí)器去定時(shí)喚醒內(nèi)核 flusher 線程回寫臟頁。
當(dāng)內(nèi)存中臟頁的數(shù)量太多了達(dá)到了一定的比例,就會(huì)主動(dòng)喚醒內(nèi)核中的 flusher 線程去回寫臟頁。
臟頁在內(nèi)存中停留的時(shí)間太久了,等到 flusher 線程下一次被喚醒的時(shí)候就會(huì)回寫這些駐留太久的臟頁。
用戶進(jìn)程可以通過 sync() 回寫內(nèi)存中的所有臟頁和 fsync() 回寫指定文件的所有臟頁,這些是進(jìn)程主動(dòng)發(fā)起臟頁回寫請(qǐng)求。
在內(nèi)存比較緊張的情況下,需要回收物理頁或者將物理頁中的內(nèi)容 swap 到磁盤上時(shí),如果發(fā)現(xiàn)通過頁面置換算法置換出來的頁是臟頁,那么就會(huì)觸發(fā)回寫。
現(xiàn)在我們了解了內(nèi)核回寫臟頁的一個(gè)大概時(shí)機(jī),這里大家可能會(huì)問了:
內(nèi)核通過 timer 定時(shí)喚醒 flush 線程回寫臟頁,那么到底間隔多久喚醒呢?
內(nèi)存中的臟頁數(shù)量太多會(huì)觸發(fā)回寫,那么這里的太多指的具體是多少呢?
臟頁在內(nèi)存中駐留太久也會(huì)觸發(fā)回寫,那么這里的太久指的到底是多久呢?
其實(shí)這三個(gè)問題中涉及到的具體數(shù)值,內(nèi)核都提供了參數(shù)供我們來配置。這些參數(shù)的配置文件存在于 proc/sys/vm 目錄下:

下面筆者就為大家介紹下內(nèi)核回寫臟頁涉及到的這 6 個(gè)參數(shù),并解答上面我們提出的這三個(gè)問題。
13.1 內(nèi)核中的定時(shí)器間隔多久喚醒 flusher 線程
內(nèi)核中通過 dirty_writeback_centisecs 參數(shù)來配置喚醒 flusher 線程的間隔時(shí)間。

該參數(shù)可以通過修改 /proc/sys/vm/dirty_writeback_centisecs 文件來配置參數(shù),我們也可以通過 sysctl 命令或者通過修改 /etc/sysctl.conf 配置文件來對(duì)這些參數(shù)進(jìn)行修改。
這里我們先主要關(guān)注這些內(nèi)核參數(shù)的含義以及源碼實(shí)現(xiàn),文章后面筆者有一個(gè)專門的章節(jié)來介紹這些內(nèi)核參數(shù)各種不同的配置方式。
dirty_writeback_centisecs 內(nèi)核參數(shù)的默認(rèn)值為 500。單位為 0.01 s。也就是說內(nèi)核會(huì)每隔 5s 喚醒一次 flusher 線程來執(zhí)行相關(guān)臟頁的回寫。該參數(shù)在內(nèi)核源碼中對(duì)應(yīng)的變量名為 dirty_writeback_interval。
筆者這里在列舉一個(gè)生活中的例子來解釋下這個(gè) dirty_writeback_interval 的作用。
假設(shè)大家的工作都非常繁忙,于是大家就到家政公司請(qǐng)了專門的保潔阿姨(內(nèi)核 flusher 回寫線程)來幫助我們打掃房間衛(wèi)生(回寫臟頁)。你和保潔阿姨約定每周(dirty_writeback_interval)來你房間(內(nèi)存)打掃一次衛(wèi)生(回寫臟頁),保潔阿姨會(huì)固定每周日按時(shí)來到你房間打掃。記住這個(gè)例子,我們后面還會(huì)用到~~~
13.2 內(nèi)核中如何使用 dirty_writeback_interval 來控制 flusher 喚醒頻率
在磁盤中數(shù)據(jù)是以塊的形式存儲(chǔ)于扇區(qū)中的,前邊在介紹文件讀寫的章節(jié)中,讀寫流程的最后都會(huì)從文件系統(tǒng)層到塊設(shè)備驅(qū)動(dòng)層,由塊設(shè)備驅(qū)動(dòng)程序?qū)?shù)據(jù)寫入對(duì)應(yīng)的磁盤塊中存儲(chǔ)。
內(nèi)存中的文件頁對(duì)應(yīng)于磁盤中的一個(gè)數(shù)據(jù)塊,而這塊磁盤就是我們常說的塊設(shè)備。而每個(gè)塊設(shè)備在內(nèi)核中對(duì)應(yīng)一個(gè) backing_dev_info 結(jié)構(gòu)用于存儲(chǔ)相關(guān)信息。其中最重要的信息是 workqueue_struct *bdi_wq 用于緩存塊設(shè)備上所有的回寫臟頁異步任務(wù)的隊(duì)列。
/* bdi_wq serves all asynchronous writeback tasks */
struct workqueue_struct *bdi_wq;
static int __init default_bdi_init(void)
{
int err;
// 創(chuàng)建 bdi_wq 隊(duì)列
bdi_wq = alloc_workqueue("writeback", WQ_MEM_RECLAIM | WQ_FREEZABLE |
WQ_UNBOUND | WQ_SYSFS, 0);
if (!bdi_wq)
return -ENOMEM;
// 初始化 backing_dev_info
err = bdi_init(&noop_backing_dev_info);
return err;
}
在系統(tǒng)啟動(dòng)的時(shí)候,內(nèi)核會(huì)調(diào)用 default_bdi_init 來創(chuàng)建 bdi_wq 隊(duì)列和初始化 backing_dev_info。
static int bdi_init(struct backing_dev_info *bdi)
{
int ret;
bdi->dev = NULL;
// 初始化 backing_dev_info 相關(guān)信息
kref_init(&bdi->refcnt);
bdi->min_ratio = 0;
bdi->max_ratio = 100;
bdi->max_prop_frac = FPROP_FRAC_BASE;
INIT_LIST_HEAD(&bdi->bdi_list);
INIT_LIST_HEAD(&bdi->wb_list);
init_waitqueue_head(&bdi->wb_waitq);
// 這里會(huì)設(shè)置 flusher 線程的定時(shí)器 timer
ret = cgwb_bdi_init(bdi);
return ret;
}
在 bdi_init 中初始化 backing_dev_info 結(jié)構(gòu)的相關(guān)信息,并在 cgwb_bdi_init 中調(diào)用 wb_init 初始化回寫臟頁任務(wù) bdi_writeback *wb,并創(chuàng)建一個(gè) timer 用于定時(shí)啟動(dòng) flusher 線程。
static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi,
int blkcg_id, gfp_t gfp)
{
......... 初始化 bdi_writeback 結(jié)構(gòu)該結(jié)構(gòu)表示回寫臟頁任務(wù)相關(guān)信息.....
// 創(chuàng)建 timer 定時(shí)執(zhí)行 flusher 線程
INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
......
}
#define __INIT_DELAYED_WORK(_work, _func, _tflags) \
do { \
INIT_WORK(&(_work)->work, (_func)); \
__setup_timer(&(_work)->timer, delayed_work_timer_fn, \
(unsigned long)(_work), \
bdi_writeback 有個(gè)成員變量 struct delayed_work dwork,bdi_writeback 就是把 delayed_work 結(jié)構(gòu)掛到 bdi_wq 隊(duì)列上的。
而 wb_workfn 函數(shù)則是 flusher 線程要執(zhí)行的回寫核心邏輯,全部封裝在 wb_workfn 函數(shù)中。
/*
* Handle writeback of dirty data for the device backed by this bdi. Also
* reschedules periodically and does kupdated style flushing.
*/
void wb_workfn(struct work_struct *work)
{
struct bdi_writeback *wb = container_of(to_delayed_work(work),
struct bdi_writeback, dwork);
long pages_written;
set_worker_desc("flush-%s", bdi_dev_name(wb->bdi));
current->flags |= PF_SWAPWRITE;
.......在循環(huán)中不斷的回寫臟頁..........
// 如果 work-list 中還有回寫臟頁的任務(wù),則立即喚醒flush線程
if (!list_empty(&wb->work_list))
wb_wakeup(wb);
// 如果回寫任務(wù)已經(jīng)被全部執(zhí)行完畢,但是內(nèi)存中還有臟頁,則延時(shí)喚醒
else if (wb_has_dirty_io(wb) && dirty_writeback_interval)
wb_wakeup_delayed(wb);
current->flags &= ~PF_SWAPWRITE;
}
在 wb_workfn 中會(huì)不斷的循環(huán)執(zhí)行 work_list 中的臟頁回寫任務(wù)。當(dāng)這些回寫任務(wù)執(zhí)行完畢之后調(diào)用 wb_wakeup_delayed 延時(shí)喚醒 flusher線程。大家注意到這里的 dirty_writeback_interval 配置項(xiàng)終于出現(xiàn)了,后續(xù)會(huì)根據(jù) dirty_writeback_interval 計(jì)算下次喚醒 flusher 線程的時(shí)機(jī)。
void wb_wakeup_delayed(struct bdi_writeback *wb)
{
unsigned long timeout;
// 使用 dirty_writeback_interval 配置設(shè)置下次喚醒時(shí)間
timeout = msecs_to_jiffies(dirty_writeback_interval * 10);
spin_lock_bh(&wb->work_lock);
if (test_bit(WB_registered, &wb->state))
queue_delayed_work(bdi_wq, &wb->dwork, timeout);
spin_unlock_bh(&wb->work_lock);
}
13.3 臟頁數(shù)量多到什么程度會(huì)主動(dòng)喚醒 flusher 線程
這一節(jié)的內(nèi)容中涉及到四個(gè)內(nèi)核參數(shù)分別是:
drity_background_ratio :當(dāng)臟頁數(shù)量在系統(tǒng)的可用內(nèi)存 available 中占用的比例達(dá)到 drity_background_ratio 的配置值時(shí),內(nèi)核就會(huì)調(diào)用 wakeup_flusher_threads 來喚醒 flusher 線程異步回寫臟頁。默認(rèn)值為:10。表示如果 page cache 中的臟頁數(shù)量達(dá)到系統(tǒng)可用內(nèi)存的 10% 的話,就主動(dòng)喚醒 flusher 線程去回寫臟頁到磁盤。

系統(tǒng)的可用內(nèi)存 = 空閑內(nèi)存 + 可回收內(nèi)存。可以通過 free 命令的 available 項(xiàng)查看。

dirty_background_bytes :如果 page cache 中臟頁占用的內(nèi)存用量絕對(duì)值達(dá)到指定的 dirty_background_bytes。內(nèi)核就會(huì)調(diào)用 wakeup_flusher_threads 來喚醒 flusher 線程異步回寫臟頁。默認(rèn)為:0。

dirty_background_bytes 的優(yōu)先級(jí)大于 drity_background_ratio 的優(yōu)先級(jí)。
dirty_ratio : dirty_background_* 相關(guān)的內(nèi)核配置參數(shù)均是內(nèi)核通過喚醒 flusher 線程來異步回寫臟頁。下面要介紹的 dirty_* 配置參數(shù),均是由用戶進(jìn)程同步回寫臟頁。表示內(nèi)存中的臟頁太多了,用戶進(jìn)程自己都看不下去了,不用等內(nèi)核 flusher 線程喚醒,用戶進(jìn)程自己主動(dòng)去回寫臟頁到磁盤中。當(dāng)臟頁占用系統(tǒng)可用內(nèi)存的比例達(dá)到 dirty_ratio 配置的值時(shí),用戶進(jìn)程同步回寫臟頁。默認(rèn)值為:20 。

dirty_bytes :如果 page cache 中臟頁占用的內(nèi)存用量絕對(duì)值達(dá)到指定的 dirty_bytes。用戶進(jìn)程同步回寫臟頁。默認(rèn)值為:0。
*_bytes 相關(guān)配置參數(shù)的優(yōu)先級(jí)要大于 *_ratio 相關(guān)配置參數(shù)。

我們繼續(xù)使用上小節(jié)中保潔阿姨的例子說明:
之前你們已經(jīng)約定好了,保潔阿姨會(huì)每周日固定(dirty_writeback_centisecs)來到你的房間打掃衛(wèi)生(臟頁),但是你周三回家的時(shí)候,發(fā)現(xiàn)屋子里太臟了,是在是臟到一定程度了(drity_background_ratio ,dirty_background_bytes),你實(shí)在是看不去了,這時(shí)你就不會(huì)等這周日(dirty_writeback_centisecs)保潔阿姨過來才打掃,你會(huì)直接給阿姨打電話讓阿姨周三就來打掃一下(內(nèi)核主動(dòng)喚醒 flusher 線程異步回寫臟頁)。
還有一種更極端的情況就是,你的房間已經(jīng)臟到很夸張的程度了(dirty_ratio ,dirty_byte)連你自己都忍不了了,于是你都不用等保潔阿姨了(內(nèi)核 flusher 回寫線程),你自己就乖乖的開始打掃房間衛(wèi)生了。這就是用戶進(jìn)程同步回寫臟頁。
13.4 內(nèi)核如何主動(dòng)喚醒 flusher 線程
通過 《12.5 balance_dirty_pages_ratelimited》小節(jié)的介紹,我們知道在 generic_perform_write 函數(shù)的最后一步會(huì)調(diào)用 balance_dirty_pages_ratelimited 來判斷是否要觸發(fā)臟頁回寫。
void balance_dirty_pages_ratelimited(struct address_space *mapping)
{
................省略............
if (unlikely(current->nr_dirtied >= ratelimit))
balance_dirty_pages(mapping, wb, current->nr_dirtied);
wb_put(wb);
}
這里會(huì)觸發(fā) balance_dirty_pages 函數(shù)進(jìn)行臟頁回寫。
static void balance_dirty_pages(struct address_space *mapping,
struct bdi_writeback *wb,
unsigned long pages_dirtied)
{
..................省略.............
for (;;) {
// 獲取系統(tǒng)可用內(nèi)存
gdtc->avail = global_dirtyable_memory();
// 根據(jù) *_ratio 或者 *_bytes 相關(guān)內(nèi)核配置計(jì)算臟頁回寫觸發(fā)的閾值
domain_dirty_limits(gdtc);
.............省略..........
}
.............省略..........
在 balance_dirty_pages 中首先通過 global_dirtyable_memory() 獲取系統(tǒng)當(dāng)前可用內(nèi)存。在 domain_dirty_limits 函數(shù)中根據(jù)前邊我們介紹的 *_ratio 或者 *_bytes 相關(guān)內(nèi)核配置計(jì)算臟頁回寫觸發(fā)的閾值。
static void domain_dirty_limits(struct dirty_throttle_control *dtc)
{
// 獲取可用內(nèi)存
const unsigned long available_memory = dtc->avail;
// 封裝觸發(fā)臟頁回寫相關(guān)閾值信息
struct dirty_throttle_control *gdtc = mdtc_gdtc(dtc);
// 這里就是內(nèi)核參數(shù) dirty_bytes 指定的值
unsigned long bytes = vm_dirty_bytes;
// 內(nèi)核參數(shù) dirty_background_bytes 指定的值
unsigned long bg_bytes = dirty_background_bytes;
// 將內(nèi)核參數(shù) dirty_ratio 指定的值轉(zhuǎn)換為以 頁 為單位
unsigned long ratio = (vm_dirty_ratio * PAGE_SIZE) / 100;
// 將內(nèi)核參數(shù) dirty_background_ratio 指定的值轉(zhuǎn)換為以 頁 為單位
unsigned long bg_ratio = (dirty_background_ratio * PAGE_SIZE) / 100;
// 進(jìn)程同步回寫 dirty_* 相關(guān)閾值
unsigned long thresh;
// 內(nèi)核異步回寫 direty_background_* 相關(guān)閾值
unsigned long bg_thresh;
struct task_struct *tsk;
if (gdtc) {
// 系統(tǒng)可用內(nèi)存
unsigned long global_avail = gdtc->avail;
// 這里可以看出 bytes 相關(guān)配置的優(yōu)先級(jí)大于 ratio 相關(guān)配置的優(yōu)先級(jí)
if (bytes)
// 將 bytes 相關(guān)的配置轉(zhuǎn)換為以頁為單位的內(nèi)存占用比例ratio
ratio = min(DIV_ROUND_UP(bytes, global_avail),
PAGE_SIZE);
// 設(shè)置 dirty_backgound_* 相關(guān)閾值
if (bg_bytes)
bg_ratio = min(DIV_ROUND_UP(bg_bytes, global_avail),
PAGE_SIZE);
bytes = bg_bytes = 0;
}
// 這里可以看出 bytes 相關(guān)配置的優(yōu)先級(jí)大于 ratio 相關(guān)配置的優(yōu)先級(jí)
if (bytes)
// 將 bytes 相關(guān)的配置轉(zhuǎn)換為以頁為單位的內(nèi)存占用比例ratio
thresh = DIV_ROUND_UP(bytes, PAGE_SIZE);
else
thresh = (ratio * available_memory) / PAGE_SIZE;
// 設(shè)置 dirty_background_* 相關(guān)閾值
if (bg_bytes)
// 將 dirty_background_bytes 相關(guān)的配置轉(zhuǎn)換為以頁為單位的內(nèi)存占用比例ratio
bg_thresh = DIV_ROUND_UP(bg_bytes, PAGE_SIZE);
else
bg_thresh = (bg_ratio * available_memory) / PAGE_SIZE;
// 保證異步回寫 backgound 的相關(guān)閾值要比同步回寫的閾值要低
if (bg_thresh >= thresh)
bg_thresh = thresh / 2;
dtc->thresh = thresh;
dtc->bg_thresh = bg_thresh;
..........省略..........
}
domain_dirty_limits 函數(shù)會(huì)分別計(jì)算用戶進(jìn)程同步回寫臟頁的相關(guān)閾值 thresh 以及內(nèi)核異步回寫臟頁的相關(guān)閾值 bg_thresh。邏輯比較好懂,筆者將每一步的注釋已經(jīng)為大家標(biāo)注出來了。這里只列出幾個(gè)關(guān)鍵核心點(diǎn):
從源碼中的 if (bytes) {....} else {.....} 分支以及 if (bg_bytes) {....} else {.....} 我們可以看出內(nèi)核配置 *_bytes 相關(guān)的優(yōu)先級(jí)會(huì)高于 *_ratio 相關(guān)配置的優(yōu)先級(jí)。
*_bytes 相關(guān)配置我們只會(huì)指定臟頁占用內(nèi)存的 bytes 閾值,但在內(nèi)核實(shí)現(xiàn)中會(huì)將其轉(zhuǎn)換為 頁 為單位。(每頁 4K 大小)。
內(nèi)核中對(duì)于臟頁回寫閾值的判斷是通過 ratio 比例來進(jìn)行判斷的。
內(nèi)核異步回寫的閾值要小于進(jìn)程同步回寫的閾值,如果超過,那么內(nèi)核異步回寫的閾值將會(huì)被設(shè)置為進(jìn)程通過回寫的一半。
static void balance_dirty_pages(struct address_space *mapping,
struct bdi_writeback *wb,
unsigned long pages_dirtied)
{
..................省略.............
for (;;) {
// 獲取系統(tǒng)可用內(nèi)存
gdtc->avail = global_dirtyable_memory();
// 根據(jù) *_ratio 或者 *_bytes 相關(guān)內(nèi)核配置計(jì)算 臟頁回寫觸發(fā)的閾值
domain_dirty_limits(gdtc);
.............省略..........
}
// 根據(jù)進(jìn)程同步回寫閾值判斷是否需要進(jìn)程直接同步回寫臟頁
if (writeback_in_progress(wb))
return
// 根據(jù)內(nèi)核異步回寫閾值判斷是否需要喚醒flusher異步回寫臟頁
if (nr_reclaimable > gdtc->bg_thresh)
wb_start_background_writeback(wb);
如果是異步回寫,內(nèi)核則喚醒 flusher 線程開始異步回寫臟頁,直到臟頁數(shù)量低于閾值或者全部回寫到磁盤。
void wb_start_background_writeback(struct bdi_writeback *wb)
{
/*
* We just wake up the flusher thread. It will perform background
* writeback as soon as there is no other work to do.
*/
trace_writeback_wake_background(wb);
wb_wakeup(wb);
}
13.5 臟頁到底在內(nèi)存中能駐留多久
內(nèi)核為了避免 page cache 中的臟頁在內(nèi)存中長久的停留,所以會(huì)給臟頁在內(nèi)存中的駐留時(shí)間設(shè)置一定的期限,這個(gè)期限可由前邊提到的 dirty_expire_centisecs 內(nèi)核參數(shù)配置。默認(rèn)為:3000。單位為:0.01 s。

也就是說在默認(rèn)配置下,臟頁在內(nèi)存中的駐留時(shí)間為 30 s。超過 30 s 之后,flusher 線程將會(huì)在下次被喚醒的時(shí)候?qū)⑦@些臟頁回寫到磁盤中。
這些過期的臟頁最終會(huì)在 flusher 線程下一次被喚醒時(shí)候被 flusher 線程回寫到磁盤中。而前邊我們也多次提到過 flusher 線程執(zhí)行邏輯全部封裝在 wb_workfn 函數(shù)中。接下來的調(diào)用鏈為 wb_workfn->wb_do_writeback->wb_writeback。在 wb_writeback 中會(huì)判斷根據(jù) dirty_expire_interval 判斷哪些是過期的臟頁。
/*
* Explicit flushing or periodic writeback of "old" data.
*
* Define "old": the first time one of an inode's pages is dirtied, we mark the
* dirtying-time in the inode's address_space. So this periodic writeback code
* just walks the superblock inode list, writing back any inodes which are
* older than a specific point in time.
*
* Try to run once per dirty_writeback_interval. But if a writeback event
* takes longer than a dirty_writeback_interval interval, then leave a
* one-second gap.
*
* older_than_this takes precedence over nr_to_write. So we'll only write back
* all dirty pages if they are all attached to "old" mappings.
*/
static long wb_writeback(struct bdi_writeback *wb,
struct wb_writeback_work *work)
{
........省略.......
work->older_than_this = &oldest_jif;
for (;;) {
........省略.......
if (work->for_kupdate) {
oldest_jif = jiffies -
msecs_to_jiffies(dirty_expire_interval * 10);
} else if (work->for_background)
oldest_jif = jiffies;
}
........省略.......
}
13.6 臟頁回寫參數(shù)的相關(guān)配置方式
前面的幾個(gè)小節(jié)筆者結(jié)合內(nèi)核源碼實(shí)現(xiàn)為大家介紹了影響內(nèi)核回寫臟頁時(shí)機(jī)的六個(gè)參數(shù)。
內(nèi)核越頻繁的觸發(fā)臟頁回寫,數(shù)據(jù)的安全性就越高,但是同時(shí)系統(tǒng)性能會(huì)消耗很大。所以我們?cè)谌粘9ぷ髦行枰Y(jié)合數(shù)據(jù)的安全性和 IO 性能綜合考慮這六個(gè)內(nèi)核參數(shù)的配置。
本小節(jié)筆者就為大家介紹一下配置這些內(nèi)核參數(shù)的方式,前面的小節(jié)中也提到過,內(nèi)核提供的這些參數(shù)存在于 proc/sys/vm 目錄下。

比如我們直接將要配置的具體數(shù)值寫入對(duì)應(yīng)的配置文件中:
echo "value" > /proc/sys/vm/dirty_background_ratio
我們還可以使用 sysctl 來對(duì)這些內(nèi)核參數(shù)進(jìn)行配置:
sysctl -w variable=value
sysctl 命令中定義的這些變量 variable 全部定義在內(nèi)核 kernel/sysctl.c 源文件中。
其中 .procname 定義的就是 sysctl 命令中指定的配置變量名字。
.data 定義的是內(nèi)核源碼中引用的變量名字。這在前邊我們介紹內(nèi)核代碼的時(shí)候介紹過了。比如配置參數(shù) dirty_writeback_centisecs 在內(nèi)核源碼中的變量名為 dirty_writeback_interval , dirty_ratio 在內(nèi)核中的變量名為 vm_dirty_ratio。
static struct ctl_table vm_table[] = {
........省略........
{
.procname = "dirty_background_ratio",
.data = &dirty_background_ratio,
.maxlen = sizeof(dirty_background_ratio),
.mode = 0644,
.proc_handler = dirty_background_ratio_handler,
.extra1 = SYSCTL_ZERO,
.extra2 = SYSCTL_ONE_HUNDRED,
},
{
.procname = "dirty_background_bytes",
.data = &dirty_background_bytes,
.maxlen = sizeof(dirty_background_bytes),
.mode = 0644,
.proc_handler = dirty_background_bytes_handler,
.extra1 = SYSCTL_LONG_ONE,
},
{
.procname = "dirty_ratio",
.data = &vm_dirty_ratio,
.maxlen = sizeof(vm_dirty_ratio),
.mode = 0644,
.proc_handler = dirty_ratio_handler,
.extra1 = SYSCTL_ZERO,
.extra2 = SYSCTL_ONE_HUNDRED,
},
{
.procname = "dirty_bytes",
.data = &vm_dirty_bytes,
.maxlen = sizeof(vm_dirty_bytes),
.mode = 0644,
.proc_handler = dirty_bytes_handler,
.extra1 = (void *)&dirty_bytes_min,
},
{
.procname = "dirty_writeback_centisecs",
.data = &dirty_writeback_interval,
.maxlen = sizeof(dirty_writeback_interval),
.mode = 0644,
.proc_handler = dirty_writeback_centisecs_handler,
},
{
.procname = "dirty_expire_centisecs",
.data = &dirty_expire_interval,
.maxlen = sizeof(dirty_expire_interval),
.mode = 0644,
.proc_handler = proc_dointvec_minmax,
.extra1 = SYSCTL_ZERO,
}
........省略........
}
而前邊介紹的這兩種配置方式全部是臨時(shí)的,我們可以通過編輯 /etc/sysctl.conf 文件來永久的修改內(nèi)核相關(guān)的配置。
我們也可以在目錄
/etc/sysctl.d/下創(chuàng)建自定義的配置文件。
vi /etc/sysctl.conf
在 /etc/sysctl.conf 文件中直接以 variable = value 的形式添加到文件的末尾。

最后調(diào)用 sysctl -p /etc/sysctl.conf 使 /etc/sysctl.conf 配置文件中新添加的那些配置生效。
總結(jié)
本文筆者帶大家從 Linux 內(nèi)核的角度詳細(xì)解析了 JDK NIO 文件讀寫在 Buffered IO 以及 Direct IO 這兩種模式下的內(nèi)核源碼實(shí)現(xiàn),探秘了文件讀寫的本質(zhì)。并對(duì)比了 Buffered IO 和 Direct IO 的不同之處以及各自的適用場(chǎng)景。
在這個(gè)過程中又詳細(xì)地介紹了與 Buffered IO 密切相關(guān)的文件頁高速緩存 page cache 在內(nèi)核中的實(shí)現(xiàn)以及相關(guān)操作。
最后我們?cè)敿?xì)介紹了影響文件 IO 的兩個(gè)關(guān)鍵步驟:文件預(yù)讀和臟頁回寫的詳細(xì)內(nèi)核源碼實(shí)現(xiàn),以及內(nèi)核中影響臟頁回寫時(shí)機(jī)的 6 個(gè)關(guān)鍵內(nèi)核配置參數(shù)相關(guān)的實(shí)現(xiàn)及應(yīng)用。
dirty_background_bytes
dirty_background_ratio
dirty_bytes
dirty_ratio
dirty_expire_centisecs
dirty_writeback_centisecs
以及關(guān)于內(nèi)核參數(shù)的三種配置方式:
通過直接修改
proc/sys/vm目錄下的相關(guān)參數(shù)配置文件。使用 sysctl 命令來對(duì)相關(guān)參數(shù)進(jìn)行修改。
通過編輯
/etc/sysctl.conf文件來永久的修改內(nèi)核相關(guān)配置。
好了,本文的內(nèi)容到這里就結(jié)束了,能夠看到這里的大家一定是個(gè)狠人兒,但是辛苦的付出總會(huì)有所收獲,恭喜大家現(xiàn)在已經(jīng)徹底打通了 Linux 文件操作相關(guān)知識(shí)的系統(tǒng)脈絡(luò)。感謝大家的耐心觀看,我們下篇文章見~~~