1.背景
在現代作業系統中,記憶體分為使用者空間和核心空間:
- 使用者空間:這是普通應用程式執行的區域。應用程式只能訪問它們自己的記憶體空間,無法直接訪問核心空間的記憶體。
- 核心空間:這是作業系統核心執行的區域。核心可以訪問所有的記憶體,包括使用者空間和核心空間。
在Linux-IO模型這篇文章中,講了檔案的讀read。
寫操作同理,我們是從使用者空間發起系統呼叫,再到核心空間。
核心空間嘛,記憶體。
記憶體和磁碟的速度差別是很明顯的,為了平衡這個速度,核心空間是有快取機制的。
所以啊,大概就是下面這個樣子,注意圖中間的是記憶體Memory。
寫快取(Buffer)和讀快取(Cache)。寫b讀c,諧音記憶的話就是下x班b打d車c。
在作業系統中,寫入檔案和同步檔案資料到磁碟涉及幾個不同的系統呼叫,主要包括 write()
和 fsync()
。
操作 | 型別 | 描述 | 優點 | 缺點 |
---|---|---|---|---|
write() | 延遲寫 | 將資料從使用者空間寫入到核心空間的頁快取。資料此時位於核心快取區中,尚未寫入磁碟。 | 快速返回,效率高,減少磁碟 I/O 頻率 | 資料尚未持久化,系統崩潰時可能丟失 |
fsync() | 刷盤 | 將檔案描述符對應的檔案的所有資料和後設資料從核心快取區同步到磁碟,確保資料持久化。 | 確保資料和後設資料持久化,資料安全性高 | 阻塞操作,效能開銷大,需等待寫入完成 |
fdatasync() | 刷盤 | 將檔案描述符對應的檔案的資料和必要的後設資料(如檔案大小)從核心快取區同步到磁碟,不包括其他後設資料。 | 確保資料和必要後設資料持久化,效能稍優於 fsync() |
阻塞操作,效能開銷較大,需等待寫入完成 |
1.檔案寫入: write
write()
用於將資料從使用者空間寫入到核心空間(核心的頁快取)。
ssize_t write(int fd, const void *buf, size_t count);
引數:
fd
:檔案描述符,表示要寫入的檔案。buf
:要寫入的資料緩衝區。count
:要寫入的資料位元組數。
返回值:
- 成功時,返回實際寫入的位元組數。
- 失敗時,返回 -1,並設定
errno
以指示錯誤。
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open");
return 1;
}
const char *data = "Hello, World!\n";
// 寫入檔案
ssize_t bytes_written = write(fd, data, strlen(data));
if (bytes_written == -1) {
perror("write");
close(fd);
return 1;
}
close(fd);
return 0;
}
2.檔案同步: fsync和fdatasync
2.1 fsync
fsync()
是一個系統呼叫,用於將檔案描述符對應的檔案的所有資料和後設資料(例如檔案大小、修改時間等)從核心快取區同步到磁碟。
int fsync(int fd);
引數:
fd
:檔案描述符,表示要同步的檔案。
返回值:
- 成功時,返回 0。
- 失敗時,返回 -1,並設定
errno
以指示錯誤。
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open");
return 1;
}
const char *data = "Hello, World!\n";
ssize_t bytes_written = write(fd, data, strlen(data));
if (bytes_written == -1) {
perror("write");
close(fd);
return 1;
}
// 同步檔案
if (fsync(fd) == -1) {
perror("fsync");
close(fd);
return 1;
}
close(fd);
return 0;
}
2.2 fdatasync
fdatasync()
類似於 fsync()
,但它只將檔案資料和必要的後設資料(例如檔案大小)同步到磁碟,而不包括檔案的其他後設資料(例如修改時間)。
int fdatasync(int fd);
引數:
fd
:檔案描述符,表示要同步的檔案。
返回值:
- 成功時,返回 0。
- 失敗時,返回 -1,並設定
errno
以指示錯誤。
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open");
return 1;
}
const char *data = "Hello, World!\n";
ssize_t bytes_written = write(fd, data, strlen(data));
if (bytes_written == -1) {
perror("write");
close(fd);
return 1;
}
// 同步檔案
if (fdatasync(fd) == -1) {
perror("fdatasync");
close(fd);
return 1;
}
close(fd);
return 0;
}
3.總結
3.1 只write() 不 fsync()
如果只使用 write()
而不呼叫 fsync()
,資料將會被寫入到核心快取區,但不會立即被寫入到磁碟。
要注意的點是這幾個:
資料可能丟失
- 如果系統在
write()
後發生崩潰或斷電,由於資料只在核心快取區而未寫入到磁碟,這些資料將會丟失。核心快取區中的資料不會在系統崩潰後恢復。
作業系統決定何時寫入磁碟
- 作業系統會根據自己的排程策略在適當的時候將資料從核心快取區寫入到磁碟。
- 這種延遲寫入可以提高系統效能,因為作業系統可以將多個寫操作合併後一起寫入磁碟。
- 但是,這也意味著應用程式無法控制資料何時被持久化,存在一定的不確定性
資料寫入的時機不確定
- 由於核心會根據自己的排程策略和快取策略決定何時將資料寫入磁碟,所以資料的持久化時間是不可預測的。
哎,那麼只用write一定不合適嗎?這就要看我們的場景了,看資料是否重要,比如日誌的話,就可以使用write,安全性要求不高,還能兼具效能。
3.2 只fync()不write()
通常需要先使用 write()
將資料寫入到核心快取區,然後再使用 fsync()
確保資料被持久化到磁碟。
直接用的話,就是將核心快取區的資料同步到磁碟,做持久化,所以也叫作刷盤嘛。
那關鍵點就在於我們核心快取區裡面有沒有資料,沒有的話,刷過去的就是空的。
所以說,我們也可以幾次write再結合一次fsync,自己控制刷盤時機。