i-node
xv6文件系統是使用inode來管理文件,先上一張圖來解釋一個文件的inode是怎么管理它的磁盤塊的:
xv6文件系統里定義了2個版本的inode。一個是硬盤上面存儲的版本struct dinode,在fs.h里定義;另一個是內存里存儲的版本struct inode,起到緩存的作用,在file.h里定義。兩個結構都有成員ref和nlink。nlink是inode在文件系統里面的鏈接數,如果nlink為0,說明inode對應的文件已經被刪除;ref是此inode被進程引用的次數,如果ref為0,就說明沒有進程使用這個inode了,這時操作系統就不用繼續在內存里緩存它,可以將它驅逐到硬盤。操作系統可以使用iupdate()更新硬盤里面的inode,用iget()增加一次ref,iput()減少一次ref。
內存里的inode有一個睡眠鎖,讀寫inode的時候需要使用ilock()和iunlock()加鎖。使用readi()和writei()可以讀寫inode管理的磁盤塊,使用namei()可以根據路徑搜索inode,使用nameiparent()可以搜索指定文件的父目錄的inode。
Large files
這里我們主要關心的是指向數據塊的指針。原來的xv6系統里,有12個direct pointer和1個single indirect pointer,一共能容納268個硬盤塊,現在要求改成:前11個指針是direct pointer,第12個指針改成single indirect pointer,第13個指針改成double indirect pointer,這樣就擴大文件容量到65803塊。
首先修改inode的相關定義:
在fs.h里
#define NDIRECT 11
#define SDIRECT_PTR 11
#define DDIRECT_PTR 12
#define NINDIRECT (BSIZE / sizeof(uint))
#define NDBINDIRECT ((BSIZE / sizeof(uint))*(BSIZE / sizeof(uint)))
#define MAXFILE (NDIRECT + NINDIRECT + NDBINDIRECT)
//#define MAXFILE 2000
// On-disk inode structure
struct dinode {
short type; // File type
short major; // Major device number (T_DEVICE only)
short minor; // Minor device number (T_DEVICE only)
short nlink; // Number of links to inode in file system
uint size; // Size of file (bytes)
uint addrs[NDIRECT+2]; // Data block addresses
};
把NDIRECT改為11;增加一個宏NDBINDIRECT,用來表示double indirect pointer的容量。為了保持inode大小不變,addrs數組的定義要改成NDIRECT+2。務必記得file.h里面的struct inode也需要修改。設置兩個宏SDIRECT_PTR和DDIRECT_PTR,指示兩個indirect pointer。另外還要修改MAXFILE,操作系統不會為inode安排多於MAXFILE的硬盤塊數。
接下來要修改bmap()函數。bmap()負責的是:Return the disk block address of the nth block in inode ip.所以bmap會檢查參數bn位於哪個區間,如果位於direct pointer之內就直接訪問,如果沒有對應數據塊的話就調用balloc()分配一個新的硬盤塊。如果超出了direct的范圍,就把bn減去NDIRECT的偏移量,先訪問addrs[11]指向的一級指針塊,使用一級指針塊里面的指針訪問數據塊。
同理可以實現二級指針塊:
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a, *a1;
struct buf *bp;
struct buf *bp1st, *bp2nd;
//printf("%d init bn\n",bn);
if(bn < NDIRECT){
if((addr = ip->addrs[bn]) == 0)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
}
bn -= NDIRECT;
if(bn < NINDIRECT){
// Load indirect block, allocating if necessary.
if((addr = ip->addrs[NDIRECT]) == 0)
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[bn]) == 0){
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
bn -= NINDIRECT;
if(bn<NDBINDIRECT)
{
if((addr = ip->addrs[DDIRECT_PTR]) == 0)//先分配二階指針的一階節點
ip->addrs[DDIRECT_PTR] = addr = balloc(ip->dev);
bp1st = bread(ip->dev, addr);//讀裝滿指針的一階block
a = (uint*)bp1st->data;
uint block1st = bn/NINDIRECT;//訪問哪個二階block?
if((addr = a[block1st]) == 0)
{
a[block1st] = addr = balloc(ip->dev);
log_write(bp1st);
}
brelse(bp1st);//可以釋放一級指針的讀寫了
bp2nd = bread(ip->dev, addr);//讀裝滿指針的二階block
a1 = (uint*)bp2nd->data;
uint block2nd = bn%NINDIRECT;//訪問二階block的哪個指針
if((addr = a1[block2nd]) == 0)
{
a1[block2nd] = addr = balloc(ip->dev);
log_write(bp2nd);
}
brelse(bp2nd);
return addr;
}
panic("bmap: out of range");
}
如果bn超出了double indirect pointer的范圍,就再把bn減去NINDIRECT的偏移量,然后訪問addrs[12]指向的一級指針。每個一級指針指向256個二級指針,所以使用bn/NINDIRECT找到待訪問的一級指針block1st,通過它訪問二級指針塊bp2nd。使用bn模NINDIRECT找到指向數據塊的二級指針block2nd。
注意函數調用bread()之后的處理方法:bread()讀取硬盤塊,把硬盤塊緩存在內存里一個struct buf類型的結構體里面,返回一個指向它的指針。我們需要的數據在此結構體的data成員中。這個data是一個1024字節的uchar類型的數組,而我們的指針都是4字節的。因此需要把data轉換成uint*類型,這樣a[block1st]做的指針運算才是以4字節為步長的。
另外注意一下log_write的細節。log_write()上面的注釋給出了使用xv6的日志系統的方法,這幾個系統調用不能亂用:
log_write() replaces bwrite(); a typical use is:
bp = bread(...)
modify bp->data[]
log_write(bp)
brelse(bp)
然后修改itrunc(),釋放所有數據塊。itrunc()里面也給我們舉了釋放single indirect pointer的例子。直接模仿:
void
itrunc(struct inode *ip)
{
int i, j;
struct buf *bp, *bp1, *bp2;
uint *a, *a1;
for(i = 0; i < NDIRECT; i++){
if(ip->addrs[i]){
bfree(ip->dev, ip->addrs[i]);
ip->addrs[i] = 0;
}
}
if(ip->addrs[NDIRECT]){
bp = bread(ip->dev, ip->addrs[NDIRECT]);
a = (uint*)bp->data;
for(j = 0; j < NINDIRECT; j++){
if(a[j])
bfree(ip->dev, a[j]);
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT]);
ip->addrs[NDIRECT] = 0;
}
//釋放所有二階指針
if(ip->addrs[DDIRECT_PTR])
{
bp1 = bread(ip->dev, ip->addrs[DDIRECT_PTR]); //訪問一階指針塊
a=(uint*)bp1->data;
for(j=0;j<NINDIRECT;j++)
{
if(a[j])
{
//如果發現了二級指針塊
bp2 = bread(ip->dev, a[j]); //訪問它
a1=(uint*)bp2->data;
for(int k=0;k<NINDIRECT;k++)
{
if(a1[k])//如果這個位置有數據塊則釋放
bfree(ip->dev,a1[k]);
}
brelse(bp2);
bfree(ip->dev,a[j]);//釋放這個二級指針塊
}
//釋放完了二階指針塊,
}
brelse(bp1); //釋放一級指針塊
bfree(ip->dev, ip->addrs[DDIRECT_PTR]);
ip->addrs[DDIRECT_PTR] = 0;
}
ip->size = 0;
iupdate(ip);
}
Symbolic links
我們需要實現一個符號鏈接的功能。符號鏈接是用專門的特殊文件類型“符號鏈接文件”來實現的,文件中只有一個表明鏈接對象路徑的字符串。一旦建立了符號鏈接,刪除操作刪除的是符號鏈接文件,其它所有操作都訪問符號鏈接引用的文件。
根據提示來逐步做:
1.首先在user.h和syscall.h等地方加入系統調用,在kernel里加入sys_symlink(),加入T_SYMLINK等定義。
2.實現symlink()調用。
先看一下硬鏈接link()做了什么:link接受兩個參數,把待鏈接的文件路徑放在old里,把新的鏈接路徑放在new里。link()使用namei查詢old的inode,返回指針ip,增加ip的nlink數;使用nameiparent()查詢new的父目錄的inode,然后使用dirlink()把new加入它的父目錄之中。
相比link(),symlink()少了增加原來節點的nlink數,多了創建新的inode,寫入target字符串的操作。因此再結合create()的操作來實現symlink()。
uint64 sys_symlink(void)
{
char name[DIRSIZ], target[MAXPATH], path[MAXPATH];
struct inode *dp, *ip, *sym;
//ip指向target dp指向path的父目錄
if(argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0)
return -1;
begin_op();
if((ip = namei(target)) != 0) // 提示要求target指定的文件是可以不存在的
{
ilock(ip);
if(ip->type == T_DIR) // 根據提示,不需要處理target是一個目錄文件的情況
{
iunlockput(ip);
end_op();
return -1;
}
iunlockput(ip);
}
if((dp = nameiparent(path, name)) == 0) //獲取path的父目錄
{
end_op();
return -1;
}
ilock(dp);
if((sym = dirlookup(dp, name, 0)) != 0) //如果有同名文件,則生成失敗
{
iunlockput(dp);
end_op();
return -1;
}
if((sym = ialloc(dp->dev, T_SYMLINK)) == 0) //仿照create生成符號鏈接類型的inode
panic("create: ialloc");
ilock(sym);
sym->nlink = 1;
iupdate(sym);
if(dirlink(dp, name,sym->inum) < 0) //把符號鏈接文件加入到path的父目錄中
panic("create: dirlink");
iupdate(dp);
//把target字符串寫入符號鏈接文件里面
if(writei(sym, 0, (uint64)&target, 0, strlen(target)) != strlen(target))
panic("symlink: writei");
iupdate(sym);//注意調用iupdate把inode信息寫入磁盤
iunlockput(dp);
iunlockput(sym);
end_op();
return 0;
}
這里需要注意的就是ilock()問題,使用inode記得上鎖,用完inode一定要記得釋放睡眠鎖和減少ref值。
3.修改open()系統調用,識別T_SYMLINK類型;加入O_NOFOLLOW,如果使用O_NOFOLLOW則打開符號鏈接文件本身;保證文件系 統是一個有向無環圖,不能出現環形鏈接。
首先要理解open()的作用。為了理解open(),我們首先要了解Linux的三級文件活動目錄。首先,每個進程的PCB的user結構中有自己的FDT,FDT是一個名為u_ofile的數組,這個數組記錄了該進程打開的文件,數組下標就是所謂的文件描述符。整個內核有一張struct file類型的系統文件表SFT。整個核心還有一張活動inode表,用作硬盤里inode的緩沖。
三張表的關系如下圖:
我們再來看xv6。在file.c中有一個ftable結構體,里面的file數組存儲了所有打開的文件。open調用的filealloc()其實就是在這個file數組里找一個ref數為0的項,返回指向這個項的指針;struct proc里有一個struct file*類型的ofile數組,fdalloc()在ofile數組里找到一個值為0的項,把這項填入一個指針,把下標返回,這樣就得到了文件描述符。
不難發現,在xv6里ftable對應SFT、ofile對應FDT、前面的buffer cache對應活動inode表。所以open()“打開一個文件”,就是在SFT(ftable.file)里找到一個空的struct file數組項,向這項里寫入需要的數據,再把這項的指針存入該進程的ofile數組,返回這個指針在ofile里的下標,把這個下標作為文件描述符返回。
理解了open()的作用就可以寫代碼了。我們的目的就是通過符號鏈接找到目標文件的inode:
uint64
sys_open(void)
{
char path[MAXPATH];
char sympath[MAXPATH];
int fd, omode;
struct file *f;
struct inode *ip; //稍后要傳遞給f->ip
struct inode* symip=0; //這里要設置一個symip用來防止鎖的混亂
int n;
if((n = argstr(0, path, MAXPATH)) < 0 || argint(1, &omode) < 0)
return -1;
begin_op();
if(omode & O_CREATE){
ip = create(path, T_FILE, 0, 0);
if(ip == 0){
end_op();
return -1;
}
} else {
//printf("here2\n");
if((ip = namei(path)) == 0){
end_op();
return -1;
}
ilock(ip);
if(ip->type == T_DIR && omode != O_RDONLY){
iunlockput(ip);
end_op();
return -1;
}
}
//判斷一下是不是符號鏈接;如果加了O_NOFOLLOW,則按照普通文件處理
if(!(omode & O_NOFOLLOW) && ip->type == T_SYMLINK)
{
int i=0;
//循環向下搜索,搜索到非符號鏈接文件為止;或者搜索深度達到10為止
while(ip->type==T_SYMLINK)
{
//printf("here\n");
if(readi(ip,0,(uint64)&sympath,0,MAXPATH)==-1) //讀取文件里的字符串,放在sympath里
{
iunlockput(ip);
end_op();
return -1;
}
iunlockput(ip); //讀取的ip可以釋放了,記得ref減去1
if((symip = namei(sympath)) == 0) //根據sympath搜索相應inode,如果搜索失敗則退出
{
end_op();
return -1;
}
i++;
if(i==10)
{
end_op();
return -1;
}
ip=symip;
ilock(ip); //給剛剛獲取的inode加鎖
}
}
if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
iunlockput(ip);
end_op();
return -1;
}
//printf("here3\n");
if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
if(f)
fileclose(f);
iunlockput(ip);
end_op();
return -1;
}
if(ip->type == T_DEVICE){
f->type = FD_DEVICE;
f->major = ip->major;
} else {
f->type = FD_INODE;
f->off = 0;
}
f->ip = ip;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
if((omode & O_TRUNC) && ip->type == T_FILE){
itrunc(ip);
}
iunlock(ip);
end_op();
return fd;
}
