日志
本文來聊聊文件系統(tǒng)中的日志系統(tǒng),來看一個簡單的日志系統(tǒng)是如何實現(xiàn)的。本文是接著前面的 xv6 系列,用到的一些前導(dǎo)知識不再說明,沒看的可以先看一下。
文件系統(tǒng)設(shè)計中通常要考慮錯誤恢復(fù),這是因為文件系統(tǒng)會涉及對磁盤的多次寫操作,如果在寫的過程中系統(tǒng)崩潰了,就會使得磁盤上的文件系統(tǒng)處于不一致的錯誤狀態(tài)。
日志就是設(shè)計來解決因為系統(tǒng)崩潰導(dǎo)致的錯誤問題,本文就 來講解怎么實現(xiàn)一個簡單的日志系統(tǒng)。在 的日志系統(tǒng)中,文件操作方面的系統(tǒng)調(diào)用并不會直接對磁盤進(jìn)行寫操作,而是把對磁盤寫操作描述包裝成一個日志寫在磁盤中,當(dāng)該系統(tǒng)調(diào)用執(zhí)行完成之后,再提交一個記錄到磁盤上。
為什么日志可以解決文件系統(tǒng)操作中出現(xiàn)的崩潰呢?如果崩潰發(fā)生在提交之前,那么磁盤上的日志文件就不會被標(biāo)記為已完成,恢復(fù)系統(tǒng)的代碼就會忽視它,磁盤的狀態(tài)就好像寫操作從未進(jìn)行一樣。如果是在提交之后崩潰的,恢復(fù)程序會重演所有的寫操作。在任何一種情況下,日志文件都使得磁盤操作對于系統(tǒng)崩潰來說是原子操作:在恢復(fù)之后,要么所有的寫操作都完成了,要么一個寫操作都沒有完成。
上面的理論大都來自 文檔,我們能了解到,最為重要的是實現(xiàn)寫操作的原子性,那么怎樣實現(xiàn)呢? 在磁盤上分配了一片日志區(qū),假如現(xiàn)在內(nèi)存中有一個緩存塊準(zhǔn)備同步到磁盤區(qū)域 A, 并不立即將該緩存塊的數(shù)據(jù)寫到磁盤區(qū)域 A,而是先寫到磁盤的日志區(qū)(提交)。如果沒有問題則將日志區(qū)的數(shù)據(jù)寫到相應(yīng)的磁盤區(qū)域 A。如果有問題,在提交之前發(fā)生了崩潰,則恢復(fù)代碼忽略日志信息,區(qū)域 A 根本就沒進(jìn)行過寫操作,當(dāng)然就能夠保證數(shù)據(jù)的一致性。如果在提交之后發(fā)生了崩潰,則恢復(fù)代碼將日志區(qū)的數(shù)據(jù)重新寫到磁盤區(qū)域 A,也保證了數(shù)據(jù)的一致性。
日志區(qū)也需要相應(yīng)的數(shù)據(jù)結(jié)構(gòu)來組織管理,相關(guān)的結(jié)構(gòu)定義如下:
結(jié)構(gòu)定義超級塊struct superblock {
uint size; // Size of file system image (blocks) 文件系統(tǒng)大小,也就是一共多少塊
uint nblocks; // Number of data blocks 數(shù)據(jù)塊數(shù)量
uint ninodes; // Number of inodes. //i結(jié)點數(shù)量
uint nlog; // Number of log blocks //日志塊數(shù)量
uint logstart; // Block number of first log block //第一個日志塊塊號
uint inodestart; // Block number of first inode block //第一個i結(jié)點所在塊號
uint bmapstart; // Block number of first free map block //第一個位圖塊塊號
};
文件系統(tǒng)的超級塊,超級塊中記錄了文件系統(tǒng)的元信息,比如上述 的超級塊記錄了數(shù)據(jù)塊、i 結(jié)點、日志塊的數(shù)量和第一塊的塊號。
文件系統(tǒng)的總體布局如下:
日志區(qū)位于文件系統(tǒng)的末尾,分為日志頭(位于第一個日志塊)和日志數(shù)據(jù)塊。
日志頭#define MAXOPBLOCKS 10 // max # of blocks any FS op writes#define LOGSIZE (MAXOPBLOCKS*3) // max data blocks in on-disk logstruct logheader { //日志頭部
int n;
int block[LOGSIZE];
};
日志頭用來記錄每次日志的大小和位置關(guān)系信息。 來記錄每次日志使用的空間大小,日志空間的總大小記錄在超級塊中(大小的單位是塊),同時 也規(guī)定每次日志使用的塊數(shù)也不能超過 。
是一個 型數(shù)組,元素個數(shù)最多為 ,用來記錄位置關(guān)系。寫入磁盤是先寫入日志區(qū),再寫到磁盤的其他區(qū)域。這個日志區(qū)的磁盤塊和其他區(qū)域的磁盤之間需要有一個映射關(guān)系,這個關(guān)系就記錄在 數(shù)組中。舉個例子: 表示日志塊 記錄的數(shù)據(jù)應(yīng)放在 號磁盤塊中。
struct log {
struct spinlock lock;
int start; //日志區(qū)第一塊塊號
int size; //日志區(qū)大小
int outstanding; // 有多少文件系統(tǒng)調(diào)用正在執(zhí)行
int committing; // 正在提交
int dev; //設(shè)備,即主盤還是從盤,文件系統(tǒng)在從盤
struct logheader lh; //日志頭
};
struct log log;
這個結(jié)構(gòu)體只存在于內(nèi)存,用來記錄當(dāng)前的日志信息。這個日志信息也是一個公共資源要避免競爭條件所以配了一把鎖。 三個屬性值從超級塊中讀取。其他的信息見注釋,具體含義后面慢慢講解。下面直接來看日志的函數(shù)實現(xiàn):
函數(shù)實現(xiàn)void readsb(int dev, struct superblock *sb) //讀超級塊
{
struct buf *bp;
bp = bread(dev, 1); //讀取超級塊數(shù)據(jù)到緩存塊
memmove(sb, bp-》data, sizeof(*sb)); //移動數(shù)據(jù)
brelse(bp); //釋放緩存塊
}
這個函數(shù)用來讀取超級塊的內(nèi)容,超級塊在第一塊,第零塊是引導(dǎo)塊。調(diào)用 將數(shù)據(jù)從磁盤讀取到緩存塊中,然后將緩存塊中超級塊的數(shù)據(jù)復(fù)制一份到內(nèi)存中定義的超級塊數(shù)據(jù)結(jié)構(gòu)中去,最后再釋放緩存塊的鎖,因為 調(diào)用 獲取了鎖,使用完該緩存塊就該釋放,詳見磁盤那篇文章
void initlog(int dev)
{
if (sizeof(struct logheader) 》= BSIZE)
panic(“initlog: too big logheader”);
struct superblock sb; //定義局部變量超級塊sb
initlock(&log.lock, “l(fā)og”); //初始化日志的鎖
readsb(dev, &sb); //讀取超級塊
/*根據(jù)超級塊的信息設(shè)置日志的一些信息*/
log.start = sb.logstart; //第一個日志塊塊號
log.size = sb.nlog; //日志塊塊數(shù)
log.dev = dev; //日志所在設(shè)備
recover_from_log(); //從日志中恢復(fù)
}
這個函數(shù)來初始化日志的信息,前面應(yīng)該都很好理解,超級塊中記錄的有一些元數(shù)據(jù),讀取超級塊來初始化一些日志信息,比如日志的大小位置。最后一點不太好理解的地方便是 故名思意,從日志中恢復(fù),每次啟動調(diào)用初始化函數(shù)它都會執(zhí)行這個函數(shù)來保證文件系統(tǒng)的一致性,關(guān)于這個函數(shù)我們后面再詳述。
static void install_trans(void)
{
int tail;
for (tail = 0; tail 《 log.lh.n; tail++) {
struct buf *lbuf = bread(log.dev, log.start+tail+1); // read log block 讀取日志塊
struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dst 讀取日志塊中數(shù)據(jù)本身應(yīng)在的磁盤塊
memmove(dbuf-》data, lbuf-》data, BSIZE); // copy block to dst 將數(shù)據(jù)復(fù)制到目的地
bwrite(dbuf); // write dst to disk 同步緩存塊到磁盤
brelse(lbuf); //釋放 lbuf
brelse(dbuf); //釋放 dbuf
}
}
就干了一件事:將磁盤中的日志塊數(shù)據(jù)復(fù)制到應(yīng)在的磁盤塊中去,前面文章曾說過針對一些列的磁盤操作,都是先在對應(yīng)的緩存塊中操作再同步到相應(yīng)的磁盤塊中去。所以先讀取兩部分的數(shù)據(jù)到內(nèi)存中的緩存塊(不一定真的從磁盤中讀出來,要視磁盤數(shù)據(jù)在內(nèi)存中是否有緩存),在內(nèi)存中把數(shù)據(jù)復(fù)制過去,再同步到磁盤塊中去,最后釋放掉緩存塊。
典型的日志使用方式如下:
begin_op();
。..。..。..。
bp = bread(。..);
bp-》data[。..] = 。..;
log_write(bp);
。..。..。..。
end_op();
和 是一對兒,配套使用,表明一個文件系統(tǒng)調(diào)用的開始和結(jié)束。通常文件系統(tǒng)調(diào)用就是讀寫磁盤上的數(shù)據(jù),所以同樣的先調(diào)用 讀取數(shù)據(jù),然后修改,但是同步寫到磁盤上不是直接調(diào)用 而是使用 來替代。為什么這么操作,我們按照上面的順序一個一個來看:
void begin_op(void)
{
acquire(&log.lock);
while(1){
if(log.committing){ //如果日志正在提交,休眠
sleep(&log, &log.lock);
} else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS 》 LOGSIZE){
// this op might exhaust log space; wait for commit. 如果此次文件系統(tǒng)調(diào)用涉及的塊數(shù)超過日志塊數(shù)上限,休眠
sleep(&log, &log.lock);
} else {
log.outstanding += 1; //文件系統(tǒng)調(diào)用加1
release(&log.lock); //釋放鎖
break; //退出循環(huán)
}
}
}
表明一個文件系統(tǒng)調(diào)用開始,它將一直等待直到日志處于未提交狀態(tài),直到有足夠的日志空間保存當(dāng)前所有調(diào)用的寫入。這個足夠的空間是保守估計的, 假設(shè)每個系統(tǒng)調(diào)用可能寫入 個塊, 表示正在執(zhí)行的系統(tǒng)調(diào)用個數(shù), 就表示加上自身這個系統(tǒng)調(diào)用,這個數(shù)乘以 就表示當(dāng)前并發(fā)的系統(tǒng)調(diào)用可能寫入的塊數(shù), 表示當(dāng)前的日志空間已經(jīng)使用的塊數(shù),它們兩者之和如果小于日志空間,則可以繼續(xù)下一步,否則等待。
若能繼續(xù)下一步,表示日志空間的空閑區(qū)域足夠容納當(dāng)前系統(tǒng)調(diào)用的寫入操作,則執(zhí)行該文件系統(tǒng)調(diào)用,將 數(shù)量加 ,表示當(dāng)前正執(zhí)行的系統(tǒng)調(diào)用個數(shù)增加 個。
void log_write(struct buf *b)
{
int i;
if (log.lh.n 》= LOGSIZE || log.lh.n 》= log.size - 1) //當(dāng)前已使用的日志空間不能大于規(guī)定的大小
panic(“too big a transaction”);
if (log.outstanding 《 1) //如果當(dāng)前正執(zhí)行的系統(tǒng)調(diào)用小于1
panic(“l(fā)og_write outside of trans”);
acquire(&log.lock);
for (i = 0; i 《 log.lh.n; i++) {
if (log.lh.block[i] == b-》blockno) // log absorbtion
break;
}
log.lh.block[i] = b-》blockno;
if (i == log.lh.n)
log.lh.n++; //日志空間使用量加1
b-》flags |= B_DIRTY; // prevent eviction 設(shè)置臟位,避免緩存塊直接釋放掉了
release(&log.lock);
}
就是 一個替代品, 直接設(shè)置緩存塊的臟位然后請求磁盤同步到磁盤上去。而 只是設(shè)置緩存塊的臟位并未立即進(jìn)行磁盤請求,而是后面提交的時候統(tǒng)一同步寫到磁盤。
同一個塊在單個事務(wù)中多次寫入的時候,會先在 數(shù)組中查找是否記錄了當(dāng)前緩存塊,如果記錄了,就使用當(dāng)前的日志塊,如果沒有記錄,分配一個日志塊, 數(shù)組更新信息。這樣操作即使一個塊在單個事務(wù)中多次寫入,也只會占用一個日志塊,節(jié)省了日志空間,這種優(yōu)化操作就叫做吸收。
如果調(diào)用了 之后調(diào)用 釋放緩存塊,這時候日志還沒有提交,則可能會出現(xiàn)緩存塊引用為 0,但數(shù)據(jù)臟的情況,具體例子可參考 函數(shù)。在這兒就回答了磁盤 一文遺留的一個問題,在 函數(shù)分配緩存塊的時候一定要尋找引用為 0 且臟位沒有設(shè)置的緩存塊。因為就算緩存塊的引用為 0,只要數(shù)據(jù)臟,則代表該緩存塊仍在使用當(dāng)中。
void end_op(void)
{
int do_commit = 0;
acquire(&log.lock); //取鎖
log.outstanding -= 1; //文件系統(tǒng)調(diào)用減1
if(log.committing) //如果正在提交,panic
panic(“l(fā)og.committing”);
if(log.outstanding == 0){ //如果正在執(zhí)行的文件系統(tǒng)調(diào)用為0,則可以提交了
do_commit = 1;
log.committing = 1;
} else {
// begin_op() may be waiting for log space,
// and decrementing log.outstanding has decreased
// the amount of reserved space.
wakeup(&log); //喚醒因日志空間不夠而休眠的進(jìn)程
}
release(&log.lock);
if(do_commit){ //如果可以提交
// call commit w/o holding locks, since not allowed
// to sleep with locks.
commit(); //提交
acquire(&log.lock); //取鎖
log.committing = 0; //提交完之后設(shè)為沒有處于提交狀態(tài)
wakeup(&log); //日志空間已重置,喚醒因正在提交和空間不夠而休眠的進(jìn)程
release(&log.lock); //釋放鎖
}
}
基本上是 相反的操作,它表示系統(tǒng)調(diào)用結(jié)束,將 減 1。如果 減為 0,表示當(dāng)前沒有文件系統(tǒng)調(diào)用在進(jìn)行,則可以提交事務(wù)了:設(shè)置 和 t 屬性為 1,具體提交操作在后面進(jìn)行。
如果 不為 0,則喚醒休眠在 上的進(jìn)程。前面 會因為日志空間可能不夠用而休眠,在這兒喚醒。可能有朋友疑惑,在這兒喚醒有什么用, 減 1 但是日志空間已經(jīng)被占用了,似乎在這兒喚醒無用。這里要注意 中的計算空間的式子:,這是一個很保守的估計,當(dāng)前系統(tǒng)調(diào)用完成之后 的值會變大, 的值會減 1,因此這個式子的總和完全可能變小,所以在這兒喚醒是有作用的。
執(zhí)行提交的過程主要就是調(diào)用 函數(shù),提交之后修改日志提交狀態(tài)為 0 表示并未處于提交狀態(tài),這時候日志空間也已經(jīng)清空有足夠的日志空間可以使用,所以喚醒休眠在 上的進(jìn)程。
接下來看具體的日志提交:
static void commit()
{
if (log.lh.n 》 0) {
write_log(); // Write modified blocks from cache to log
write_head(); // Write header to disk -- the real commit
install_trans(); // Now install writes to home locations
log.lh.n = 0;
write_head(); // Erase the transaction from the log
}
}
static void write_log(void) //將緩存塊寫到到日志區(qū)
{
int tail;
for (tail = 0; tail 《 log.lh.n; tail++) {
struct buf *to = bread(log.dev, log.start+tail+1); // log block
struct buf *from = bread(log.dev, log.lh.block[tail]); // cache block
memmove(to-》data, from-》data, BSIZE);
bwrite(to); // write the log
brelse(from);
brelse(to);
}
}
static void write_head(void) //將日志頭寫到日志區(qū)第一塊
{
struct buf *buf = bread(log.dev, log.start); //讀取日志頭
struct logheader *hb = (struct logheader *) (buf-》data); //類型轉(zhuǎn)換
int i;
hb-》n = log.lh.n; //日志記錄大小
for (i = 0; i 《 log.lh.n; i++) {
hb-》block[i] = log.lh.block[i]; //位置信息
}
bwrite(buf); //將日志頭同步到磁盤
brelse(buf);
}
static void read_head(void) //讀取日志頭信息
{
struct buf *buf = bread(log.dev, log.start); //日志頭在日志區(qū)第一塊
struct logheader *lh = (struct logheader *) (buf-》data); //地址類型轉(zhuǎn)換
int i;
log.lh.n = lh-》n; //當(dāng)前日志塊數(shù)
for (i = 0; i 《 log.lh.n; i++) {
log.lh.block[i] = lh-》block[i]; //當(dāng)前日志位置信息
}
brelse(buf);
}
這幾個函數(shù)應(yīng)該很好理解了,看注釋應(yīng)該都能明白就不一一解釋了,在這兒主要說一些提交的具體過程:
首先判斷日志頭中的 是否大于 0,大于 0 表示有日志要提交,否則日志為空,不用提交也無可提交。
如果有日志要提交,則先根據(jù)內(nèi)存中的日志頭中的 數(shù)組記錄的信息,將內(nèi)存中的緩存塊寫到日志區(qū)。
然后將內(nèi)存中的日志頭同步到磁盤的日志頭中去。這一步代表提交點,完成這一步表示已提交,反之則沒有提交。
經(jīng)過提交點之后,再根據(jù)內(nèi)存中的日志頭中的 數(shù)組記錄的信息,將日志區(qū)的數(shù)據(jù)復(fù)制到磁盤的其他區(qū)域。
之后將內(nèi)存中的日志頭的 設(shè)為 0,再同步日志頭到磁盤。表示已完整的完成一次事務(wù)操作,清除日志空間,為下一次事務(wù)做準(zhǔn)備。
static void recover_from_log(void)
{
read_head(); //讀取日志頭
install_trans(); // if committed, copy from log to disk
log.lh.n = 0;
write_head(); // clear the log
}
,從日志中恢復(fù),可以看出這個函數(shù)與 很相似,只不過 需要從磁盤將日志頭讀出,而 的時候日志頭本身就在內(nèi)存當(dāng)中不用讀取,其他部分一模一樣不再解釋。
這里也解釋了為什么這個日志是一個 ,可以看出如果能從日志中恢復(fù),它是將提交所做的事情重新做了一遍。
在這兒再來看看為什么 能夠進(jìn)行錯誤恢復(fù),使得磁盤中的數(shù)據(jù)保持一致性呢?如果在提交之前發(fā)生了崩潰,則磁盤上的日志不會被標(biāo)記為已完成,也就是日志頭中的 為 0。因此在進(jìn)行恢復(fù)操作執(zhí)行 函數(shù)時, 讀取日志頭的時候發(fā)現(xiàn) n 為 0,則執(zhí)行 的時候根本就不會進(jìn)入 循環(huán)進(jìn)行實際的操作。也即如果在提交之前發(fā)生崩潰,對磁盤所有的操作都發(fā)生日志區(qū),恢復(fù)代碼直接忽略該日志,不會將日志中的數(shù)據(jù)同步到磁盤的其他區(qū)域,也就保證了磁盤中文件系統(tǒng)的一致性。
如果崩潰發(fā)生在提交之后,則磁盤中的日志頭 n 不為 0,恢復(fù)代碼將根據(jù) 數(shù)組記錄的信息,循環(huán) n 次把所有使用的日志塊同步到磁盤的其他區(qū)域。對磁盤所有的寫入操作先是寫入了日志區(qū),恢復(fù)的時候又從日志區(qū)同步到磁盤相應(yīng)的其他區(qū)域,這也就保證了磁盤中數(shù)據(jù)的一致性。
所以因為日志的存在,對磁盤所有的寫入操作都先是寫到日志區(qū),再同步到磁盤的其他區(qū)域。使得對磁盤的寫入操作是一種原子操作,要么寫入操作全部完成,要么好像根本就沒有進(jìn)行寫入操作一樣(實際上日志區(qū)是有寫入操作的),因此這種原子寫入操作保證了磁盤文件系統(tǒng)的一致性。
好啦,關(guān)于 的文件系統(tǒng)的日志層就聊到這里,有什么錯誤還請批評指正,也歡迎大家來同我討論交流學(xué)習(xí)進(jìn)步。
責(zé)任編輯:haq
-
代碼
+關(guān)注
關(guān)注
30文章
4828瀏覽量
69055 -
日志
+關(guān)注
關(guān)注
0文章
139瀏覽量
10679
原文標(biāo)題:如何實現(xiàn)一個簡單的日志系統(tǒng)
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
防止根文件系統(tǒng)破壞,OverlayRootfs 讓你的設(shè)備更安全
![防止根<b class='flag-5'>文件系統(tǒng)</b>破壞,OverlayRootfs 讓你的設(shè)備更安全](https://file.elecfans.com/web2/M00/10/E9/pYYBAGEfInyAb9dQAABH4pzjonI981.jpg)
Jtti:Linux中虛擬文件系統(tǒng)和容器化的關(guān)系
Linux根文件系統(tǒng)的掛載過程
如何構(gòu)建Linux根文件系統(tǒng)
小型文件系統(tǒng)如何選擇?FatFs和LittleFs優(yōu)缺點比較
![小型<b class='flag-5'>文件系統(tǒng)</b>如何選擇?FatFs和LittleFs優(yōu)缺點比較](https://file1.elecfans.com//web2/M00/09/86/wKgaomb5DGSAF-VfAAC7k0BnFXQ574.jpg)
想提高開發(fā)效率,不要忘記文件系統(tǒng)
![想提高開發(fā)效率,不要忘記<b class='flag-5'>文件系統(tǒng)</b>](https://file1.elecfans.com/web2/M00/06/60/wKgZombkyvaAUaWlABZH07LDusM007.png)
如何修改buildroot和debian文件系統(tǒng)
![如何修改buildroot和debian<b class='flag-5'>文件系統(tǒng)</b>](https://file1.elecfans.com/web2/M00/FD/B9/wKgZomaeKpCAeS1mAAJr5bnMFl8719.png)
linux--sysfs文件系統(tǒng)
![linux--sysfs<b class='flag-5'>文件系統(tǒng)</b>](https://file1.elecfans.com/web2/M00/F9/A4/wKgZomaLXwqAbGxzAAAQ6QVA7SI028.png)
使用wear_level api來安裝fat文件系統(tǒng),如何格式化FAT文件系統(tǒng)?
通用FAT文件系統(tǒng)模塊
![通用FAT<b class='flag-5'>文件系統(tǒng)</b>模塊](https://file1.elecfans.com/web2/M00/D2/5A/wKgaomYjKCSAABUtAAAZSvPHgJE423.png)
【嵌入式SD NAND】基于FATFS/Littlefs文件系統(tǒng)的日志框架實現(xiàn)
![【嵌入式SD NAND】基于FATFS/Littlefs<b class='flag-5'>文件系統(tǒng)</b>的<b class='flag-5'>日志</b>框架<b class='flag-5'>實現(xiàn)</b>](https://file1.elecfans.com//web2/M00/C4/65/wKgZomXyza2APA-TACTREhVCSEw691.png)
【嵌入式SD NAND】基于FATFS/Littlefs文件系統(tǒng)的日志框架實現(xiàn)
【嵌入式SD NAND】基于FATFS/Littlefs文件系統(tǒng)的日志框架實現(xiàn)
![【嵌入式SD NAND】基于FATFS/Littlefs<b class='flag-5'>文件系統(tǒng)</b>的<b class='flag-5'>日志</b>框架<b class='flag-5'>實現(xiàn)</b>](https://file1.elecfans.com/web2/M00/C5/4F/wKgaomXyzQWAOuZPAADeOXPjtqE725.png)
評論