代碼在github上。
這次實驗是要對文件系統修改,使其支持更大的文件以及符號鏈接,實驗本身並不是很復雜。但文件系統可以說是XV6中最復雜的部分,整個文件系統包括了七層:文件描述符,路徑名,目錄,inode,日志,緩沖區,磁盤。
文件描述符類似於Linux,將文件、管道、設備、套接字等都抽象為文件描述符,從而可以使用read
和write
系統調用對其進行讀寫。XV6的read
和write
是使用if-else來對描述符類型進行判斷,選擇對應的底層函數;而在Linux中,則是使用函數指針直接指向對應的底層函數,避免進行多次判斷。
路徑名則提供了根據路徑名從目錄系統中查找文件的功能。在路徑查找過程中需要避免可能會出現的死鎖,例如路徑名中包含..
。
目錄層類似於文件,目錄文件的內部會保存該目錄的目錄項struct dirent
,其中包含了文件名和對應的inode號。在XV中目錄查找是使用遍歷目錄項數組來依次比較,時間復雜度為O(n);而在NTFS、ZFS等文件系統中,會使用磁盤平衡樹來組織目錄項,使目錄查找的復雜度降低為O(lgn)。
inode層為文件在磁盤上的組織,在磁盤中會有一塊區域用於保存inode
信息,包括文件類型、大小、鏈接數以及文件每個塊對應的磁盤塊號。通過路徑從目錄系統中查找到對應的inode
號,之后就可以從磁盤上讀取對應的inode
信息,之后就可以根據偏移量查找對應的磁盤塊號,最后對其進行讀寫。
日志層提供了事務以及故障恢復的功能,當有多個磁盤操作必須原子完成時就要用到事務(如刪除文件時要從目錄中刪除文件,刪除文件對應的inode,對空閑塊bitmap進行修改等)。日志先將操作寫到磁盤的日志區上,寫入完成后再寫入commit
,最后再將所有操作真正寫到磁盤上去。當在寫入commit
之前發生故障,就不需要進行操作,因為事務沒有被提交;當在寫入commit
之后發生故障,就將日志區的日志全部重寫一遍,保證事務被正確提交。
緩沖區則提供了磁盤塊緩存,同時保證一個磁盤塊在緩沖區中只有一個,使得同一時間只能有一個線程對同一個塊進行操作,避免讀到的數據不一致。
Large files (moderate)
這一個實驗是要使XV6支持更大的文件。原始XV6中的文件塊號dinode.addr
是使用一個大小為12的直接塊表以及一個大小為256的一級塊表,即文件最大為12+256
塊。可以通過將一個直接塊表中的項替換為一個二級塊表來使系統支持大小為11+256+256*256
個塊的文件。
首先修改對應的宏以及inode
定義。
#define NDIRECT 11
#define NINDIRECT (BSIZE / sizeof(uint))
#define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT)
struct dinode {
...
uint addrs[NDIRECT+2]; // Data block addresses
};
struct inode {
...
uint addrs[NDIRECT+2]; // Data block addresses
};
之后修改bmap
函數,使其支持二級塊表,其實就是重復一次塊表的查詢過程。
static uint
bmap(struct inode *ip, uint bn)
{
...
bn -= NINDIRECT;
if(bn < NINDIRECT * NINDIRECT){
// double indirect
int idx = bn / NINDIRECT;
int off = bn % NINDIRECT;
if((addr = ip->addrs[NDIRECT + 1]) == 0)
ip->addrs[NDIRECT + 1] = addr = balloc(ip->dev);
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[idx]) == 0){
a[idx] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[off]) == 0){
a[off] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
panic("bmap: out of range");
}
最后修改itrunc
函數使其能夠釋放二級塊表對應的塊,主要就是注意一下brelse
的調用就行了,仿照一級塊表的處理就行了。
void
itrunc(struct inode *ip)
{
...
if(ip->addrs[NDIRECT + 1]){
bp = bread(ip->dev, ip->addrs[NDIRECT + 1]);
a = (uint*)bp->data;
struct buf *bpd;
uint* b;
for(j = 0; j < NINDIRECT; j++){
if(a[j]){
bpd = bread(ip->dev, a[j]);
b = (uint*)bpd->data;
for(int k = 0; k < NINDIRECT; k++){
if(b[k])
bfree(ip->dev, b[k]);
}
brelse(bpd);
bfree(ip->dev, a[j]);
}
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT + 1]);
ip->addrs[NDIRECT + 1] = 0;
}
ip->size = 0;
iupdate(ip);
}
Symbolic links (moderate)
這一個實驗是要實現符號鏈接,符號鏈接就是在文件中保存指向文件的路徑名,在打開文件的時候根據保存的路徑名再去查找實際文件。與符號鏈接相反的就是硬鏈接,硬鏈接是將文件的inode
號指向目標文件的inode
,並將引用計數加一。
symlink
的系統調用實現起來也很簡單,就是創建一個inode
,設置類型為T_SYMLINK
,然后向這個inode
中寫入目標文件的路徑就行了。
uint64
sys_symlink(void)
{
char target[MAXPATH];
memset(target, 0, sizeof(target));
char path[MAXPATH];
if(argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0){
return -1;
}
struct inode *ip;
begin_op();
if((ip = create(path, T_SYMLINK, 0, 0)) == 0){
end_op();
return -1;
}
if(writei(ip, 0, (uint64)target, 0, MAXPATH) != MAXPATH){
// panic("symlink write failed");
return -1;
}
iunlockput(ip);
end_op();
return 0;
}
最后在sys_open
中添加對符號鏈接的處理就行了,當模式不是O_NOFOLLOW
的時候就對符號鏈接進行循環處理,直到找到真正的文件,如果循環超過了一定的次數(10),就說明可能發生了循環鏈接,就返回-1。這里主要就是要注意namei
函數不會對ip
上鎖,需要使用ilock
來上鎖,而create
則會上鎖。
uint64
sys_open(void)
{
...
if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
...
}
if(ip->type == T_SYMLINK){
if(!(omode & O_NOFOLLOW)){
int cycle = 0;
char target[MAXPATH];
while(ip->type == T_SYMLINK){
if(cycle == 10){
iunlockput(ip);
end_op();
return -1; // max cycle
}
cycle++;
memset(target, 0, sizeof(target));
readi(ip, 0, (uint64)target, 0, MAXPATH);
iunlockput(ip);
if((ip = namei(target)) == 0){
end_op();
return -1; // target not exist
}
ilock(ip);
}
}
}
if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
...
}