近日有人求助,要寫一個UNIX文件系統作為暑假作業。這種事情基本是學操作系統的必須要做的或者是做過的,畢竟文件系統是操作系統課程的一個重要組成部分。要實現這個UNIX文件系統,很多人就扎進了UNIX V6的的系統源碼,以及《萊昂氏UNIX源代碼分析》和《返璞歸真:UNIX技術內幕》這兩本書,很多人出來了,很多人在里面迷失了...最終忘了自己只是要實現一個UNIX文件系統而已。
為何會迷失,因為代碼不是自己寫的,而且年代久遠,編程理念不同了,作者為何那樣寫不一定就能理解,實際上對于任何別人寫的代碼,總是會有一些不易理解的地方,當然,如果作者水平超級高,那么代碼也就相對容易理解。因此,寫代碼永遠比讀代碼要容易!既然是要寫一個文件系統,為何要用現成的UNIX V6代碼呢?如果理解了UNIX文件的布局和結構,自己從零開始不參考任何現有的代碼做一個也不是什么難事,最根本的是UNIX文件系統本身,至于說代碼,僅僅是一個實現與用戶操作的一個接口而已。如果代碼是自己一點一點寫的,那么你肯定能徹底明白每一行的每一個語句的精確含義,至于為何這么寫,你當然及其明了!
本文留下我倉促間幾個小時寫的一個類UNIX文件系統的代碼,不是讓別人看的,是為了自己留檔,因為本文已經說了,看別人的代嗎只能學習經驗,不能理解本質,更何況,你看的還得是超級一流的代碼,而我寫的,則是超級垃圾的代碼。我只想說,理解問題的本質要比代碼重要得多,代碼,碼,并不是很多人想象中的那般重要!本文的實現基于Linux系統,即在Linux系統上編寫一個用戶態程序,實現UNIX文件的IO接口以及操作。
我一向堅持的原則,那就是任何東西的根本性的,本質上的原理以及背后的思想都是及其簡單的,所謂的復雜性都是優化與策略化的擴展帶來的,正如TCP一樣,UNIX的文件系統也不例外!我們必須知道,什么是最根本的,什么是次要的。對于UNIX文件系統,最根本的就是其布局以及其系統調用接口,一個處在最低層,一個在最上層開放給用戶,如下所示:
系統調用接口:open,write,read,close...
文件系統布局:引導快,超級塊,inode區表,數據塊區
所有的在二者中間的部分都是次要的,也就是說那些東西不要也行,比如高速緩沖,高速緩存,VFS層,塊層...因此在我的實現代碼中,并沒有這些東西,我所做到的,僅僅是UNIX文件系統所要求必須做到的最小集,那就是:
面對一個按照UNIX文件系統標準布局的“塊設備”,可以使用open,read,write等接口進行IO操作。
在實現中,我用一個標準的Linux大文件來模擬磁盤塊,這樣塊的操作基本都映射到了Linux標準的write,read等系統調用了。
首先定義必要的結構體:
//超級塊結構
struct filesys {
unsigned int s_size; //總大小
unsigned int s_itsize; //inode表大小
unsigned int s_freeinodesize; //空閑i節點的數量
unsigned int s_nextfreeinode; //下一個空閑i節點
unsigned int s_freeinode[NUM]; //空閑i節點數組
unsigned int s_freeblocksize; //空閑塊的數量
unsigned int s_nextfreeblock; //下一個空閑塊
unsigned int s_freeblock[NUM]; //空閑塊數組
unsigned int s_pad[]; //填充到512字節
};
//磁盤inode結構
struct finode {
int fi_mode; //類型:文件/目錄
int fi_uid_unused; //uid,由于和進程無關聯,僅僅是模擬一個FS,未使用,下同
int fi_gid_unused;
int fi_nlink; //鏈接數,當鏈接數為0,意味著被刪除
long int fi_size; //文件大小
long int fi_addr[BNUM]; //文件塊一級指針,并未實現多級指針
time_t fi_atime_unused; //未實現
time_t fi_mtime_unused;
};
//內存inode結構
struct inode {
struct finode i_finode;
struct inode *i_parent; //所屬的目錄i節點
int i_ino; //i節點號
int i_users; //引用計數
};
//目錄項結構(非Linux內核的目錄項)
struct direct
{
char d_name[MAXLEN]; //文件或者目錄的名字
unsigned short d_ino; //文件或者目錄的i節點號
};
//目錄結構
struct dir
{
struct direct direct[DIRNUM]; //包含的目錄項數組
unsigned short size; //包含的目錄項大小
};
//抽象的文件結構
struct file {
struct inode *f_inode; //文件的i節點
int f_curpos; //文件的當前讀寫指針
};之所以叫做類UNIX文件系統,是因為我并沒有去精確確認當時的UNIX文件系統的超級塊以及inode表的結構,只是大致的模仿其布局,比如超級塊中字段,以及字段的順序可能和標準的UNIX文件系統并不完全一致。但是不管怎么說,當時的UNIX文件系統基本就是這個一個樣子。另外,可以看到file結構體內容及其少,因為本質上,我只是想表示“一個inode節點相對于一個讀寫者來說,就是一個file”,僅此而已。接下來就是具體的實現了,我的方式是自下而上的,這樣做的好處在于便于今后的擴展。那么首先要完成的就是i節點的分配和釋放了,我的實現中,是將文件i節點映射到了內存i節點,這樣或許違背了我的初衷,我不是說過不要那么多“額外”的東西來擾亂視聽的嗎?是的,然而比起那些所謂的額外的優化,我更不喜歡頻繁的調用read和write。反正,只要自己能控制住局面即可。
在實現中,還有一個大事就是內存的分配與釋放,這些也不是本質的,記住,要實現的僅僅是一個UNIX文件系統,其它的能繞開則繞開!顯然malloc,free等也是我們要繞開的,于是我基本都使用預分配空間的東西-全局數組。以下是全局變量:
//內存i節點數組,NUM為該文件系統容納的文件數 struct inode g_usedinode[NUM]; //ROOT的內存i節點 struct inode *g_root; //已經打開文件的數組 struct file* g_opened[OPENNUM]; //超級塊 struct filesys *g_super; //模擬二級文件系統的Linux大文件的文件描述符 int g_fake_disk = -1;
在給出實現代碼之前,要說明的是,在刪除文件的時候,我并沒有實現文件塊區以及i節點的清除操作,眾所周知,那樣很耗時,和很多實現一樣,我只是記錄了一些信息,表示這個文件塊或者inode字段是可以隨時覆蓋的。
//同步i節點,將其寫入“磁盤”
void syncinode(struct inode *inode)
{
int ino = -1, ipos = -1;
ino = inode->i_ino;
//ipos為inode節點表在文件系統塊中的偏移
ipos = IBPOS + ino*sizeof(struct finode);
//從模擬塊的指定偏移位置讀取inode信息
lseek(g_fake_disk, ipos, SEEK_SET);
write(g_fake_disk, (void *)&inode->i_finode, sizeof(struct finode));
}
//同步超級塊信息
int syncsuper(struct filesys *super)
{
int pos = -1, size = -1;
struct dir dir = {0};
pos = BOOTBSIZE;
size = SUPERBSIZE;
lseek(g_fake_disk, pos, SEEK_SET);
write(g_fake_disk, (void *)super, size);
syncinode(g_root);
breadwrite(g_root->i_finode.fi_addr[0], (char *)&dir, sizeof(struct dir), 0, 1);
breadwrite(g_root->i_finode.fi_addr[0], (char *)&dir, sizeof(struct dir), 0, 0);
}
//關鍵的將路徑名轉換為i節點的函數,暫不支持相對路徑
struct inode *namei(char *filepath, char flag, int *match, char *ret_name)
{
int in = 0;
int repeat = 0;
char *name = NULL;
char *path = calloc(1, MAXLEN*10);
char *back = path;
struct inode *root = iget(0);
struct inode *parent = root;
struct dir dir = {0};
strncpy(path, filepath, MAXLEN*10);
if (path[0] != '/')
return NULL;
breadwrite(root->i_finode.fi_addr[0], &dir, sizeof(struct dir), 0, 1);
while((name=strtok(path, "/")) != NULL) {
int i = 0;
repeat = 0;
*match = 0;
path = NULL;
if (ret_name) {
strcpy(ret_name, name);
}
for (; i<dir.size; i++) {
if (!strncmp(name, dir.direct[i].d_name, strlen(name))) {
parent = root;
iput(root);
root = iget(dir.direct[i].d_ino);
root->i_parent = parent;
*match = 1;
if (root->i_finode.fi_mode == MODE_DIR) {
memset(&dir, 0, sizeof(struct dir));
breadwrite(root->i_finode.fi_addr[0], &dir, sizeof(struct dir), 0, 1);
} else {
free(back);
return root;
}
repeat = 1;
}
}
if (repeat == 0) {
break;
}
}
if (*match != 1) {
*match = 0;
}
if (*match == 0) {
if (ret_name) {
strcpy(ret_name, name);
}
}
free(back);
return root;
}
//通過i節點號獲取內存i節點的函數
struct inode *iget(int ino)
{
int ibpos = 0;
int ipos = 0;
int ret = 0;
//傾向于直接從內存i節點獲取
if (g_usedinode[ino].i_users) {
g_usedinode[ino].i_users ++;
return &g_usedinode[ino];
}
if (g_fake_disk < 0) {
return NULL;
}
//實在不行則從模擬磁盤塊讀入
ipos = IBPOS + ino*sizeof(struct finode);
lseek(g_fake_disk, ipos, SEEK_SET);
ret = read(g_fake_disk, &g_usedinode[ino], sizeof(struct finode));
if (ret == -1) {
return NULL;
}
if (g_super->s_freeinode[ino] == 0) {
return NULL;
}
//如果是一個已經被刪除的文件或者從未被分配過的i節點,則初始化其link值以及size值
if (g_usedinode[ino].i_finode.fi_nlink == 0) {
g_usedinode[ino].i_finode.fi_nlink ++;
g_usedinode[ino].i_finode.fi_size = 0;
syncinode(&g_usedinode[ino]);
}
g_usedinode[ino].i_users ++;
g_usedinode[ino].i_ino = ino;
return &g_usedinode[ino];
}
//釋放一個占有的內存i節點
void iput(struct inode *ip)
{
if (ip->i_users > 0)
ip->i_users --;
}
//分配一個未使用的i節點。注意,我并沒有使用超級塊的s_freeinodesize字段,
//因為還會有一個更好更快的分配算法
struct inode* ialloc()
{
int ino = -1, nowpos = -1;
ino = g_super->s_nextfreeinode;
if (ino == -1) {
return NULL;
}
nowpos = ino + 1;
g_super->s_nextfreeinode = -1;
//尋找下一個空閑i節點,正如上述,這個算法并不好
for (; nowpos < NUM; nowpos++) {
if (g_super->s_freeinode[nowpos] == 0) {
g_super->s_nextfreeinode = nowpos;
break;
}
}
g_super->s_freeinode[ino] = 1;
return iget(ino);
}
//試圖刪除一個文件i節點
int itrunc(struct inode *ip)
{
iput(ip);
if (ip->i_users == 0 && g_super) {
syncinode(ip);
g_super->s_freeinode[ip->i_ino] = 0;
g_super->s_nextfreeinode = ip->i_ino;
return 0;
}
return ERR_BUSY;
}
//分配一個未使用的磁盤塊
int balloc()
{
int bno = -1, nowpos = -1;
bno = g_super->s_nextfreeblock;
if (bno == -1) {
return bno;
}
nowpos = bno + 1;
g_super->s_nextfreeblock = -1;
for (; nowpos < NUM; nowpos++) {
if (g_super->s_freeblock[nowpos] == 0) {
g_super->s_nextfreeblock = nowpos;
break;
}
}
g_super->s_freeblock[bno] = 1;
return bno;
}
//讀寫操作
int breadwrite(int bno, char *buf, int size, int offset, int type)
{
int pos = BOOTBSIZE+SUPERBSIZE+g_super->s_itsize + bno*BSIZE;
int rs = -1;
if (offset + size > BSIZE) {
return ERR_EXCEED;
}
lseek(g_fake_disk, pos + offset, SEEK_SET);
rs = type ? read(g_fake_disk, buf, size):write(g_fake_disk, buf, size);
return rs;
}
//IO讀接口
int mfread(int fd, char *buf, int length)
{
struct file *fs = g_opened[fd];
struct inode *inode = fs->f_inode;
int baddr = fs->f_curpos;
int bondary = baddr%BSIZE;
int max_block = (baddr+length)/BSIZE;
int size = 0;
int i = inode->i_finode.fi_addr[baddr/BSIZE+1];
for (; i < max_block+1; i ++,bondary = size%BSIZE) {
size += breadwrite(inode->i_finode.fi_addr[i], buf+size, (length-size)%BSIZE, bondary, 1);
}
return size;
}
//IO寫接口
int mfwrite(int fd, char *buf, int length)
{
struct file *fs = g_opened[fd];
struct inode *inode = fs->f_inode;
int baddr = fs->f_curpos;
int bondary = baddr%BSIZE;
int max_block = (baddr+length)/BSIZE;
int curr_blocks = inode->i_finode.fi_size/BSIZE;
int size = 0;
int sync = 0;
int i = inode->i_finode.fi_addr[baddr/BSIZE+1];
//如果第一次寫,先分配一個塊
if (inode->i_finode.fi_size == 0) {
int nbno = balloc();
if (nbno == -1) {
return -1;
}
inode->i_finode.fi_addr[0] = nbno;
sync = 1;
}
//如果必須擴展,則再分配塊,可以和上面的合并優化
if (max_block > curr_blocks) {
int j = curr_blocks + 1;
for (; j < max_block; j++) {
int nbno = balloc();
if (nbno == -1) {
return -1;
}
inode->i_finode.fi_addr[j] = nbno;
}
sync = 1;
}
for (; i < max_block+1; i ++,bondary = size%BSIZE) {
size += breadwrite(inode->i_finode.fi_addr[i], buf+size, (length-size)%BSIZE, bondary, 0);
}
if (size) {
inode->i_finode.fi_size += size;
sync = 1;
}
if (sync) {
syncinode(inode);
}
return size;
}
//IO的seek接口
int mflseek(int fd, int pos)
{
struct file *fs = g_opened[fd];
fs->f_curpos = pos;
return pos;
}
//IO打開接口
int mfopen(char *path, int mode)
{
struct inode *inode = NULL;
struct file *file = NULL;
int match = 0;
inode = namei(path, 0, &match, NULL);
if (match == 0) {
return ERR_NOEXIST;
}
file = (struct file*)calloc(1, sizeof(struct file));
file->f_inode = inode;
file->f_curpos = 0;
g_opened[g_fd] = file;
g_fd++;
return g_fd-1;
}
//IO關閉接口
void mfclose(int fd)
{
struct inode *inode = NULL;
struct file *file = NULL;
file = g_opened[fd];
inode = file->f_inode;
iput(inode);
free(file);
}
//IO創建接口
int mfcreat(char *path, int mode)
{
int match = 0;
struct dir dir;
struct inode *new = NULL;
char name[MAXLEN] = {0};;
struct inode *inode = namei(path, 0, &match, name);
if (match == 1) {
return ERR_EXIST;
}
breadwrite(inode->i_finode.fi_addr[0], (char *)&dir, sizeof(struct dir), 0, 1);
strcpy(dir.direct[dir.size].d_name, name);
new = ialloc();
if (new == NULL) {
return -1;
}
dir.direct[dir.size].d_ino = new->i_ino;
new->i_finode.fi_mode = mode;
if (mode == MODE_DIR) {
//不允許延遲分配目錄項
int nbno = balloc();
if (nbno == -1) {
return -1;
}
new->i_finode.fi_addr[0] = nbno;
}
new->i_parent = inode;
syncinode(new);
dir.size ++;
breadwrite(inode->i_finode.fi_addr[0], (char *)&dir, sizeof(struct dir), 0, 0);
syncinode(inode);
iput(inode);
syncinode(new);
iput(new);
return ERR_OK;
}
//IO刪除接口
int mfdelete(char *path)
{
int match = 0;
struct dir dir;
struct inode *del = NULL;
struct inode *parent = NULL;
char name[MAXLEN];
int i = 0;
struct inode *inode = namei(path, 0, &match, name);
if (match == 0 || inode->i_ino == 0) {
return ERR_NOEXIST;
}
match = -1;
parent = inode->i_parent;
breadwrite(parent->i_finode.fi_addr[0], (char *)&dir, sizeof(struct dir), 0, 1);
for (; i < dir.size; i++) {
if (!strncmp(name, dir.direct[i].d_name, strlen(name))) {
del = iget(dir.direct[i].d_ino);
iput(del);
if (itrunc(del) == 0) {
memset(dir.direct[i].d_name, 0, strlen(dir.direct[i].d_name));
match = i;
break;
} else {
return ERR_BUSY;
}
}
}
for (i = match; i < dir.size - 1 && match != -1; i++) {
strcpy(dir.direct[i].d_name, dir.direct[i+1].d_name);
}
dir.size--;
breadwrite(parent->i_finode.fi_addr[0], (char *)&dir, sizeof(struct dir), 0, 0);
return ERR_OK;
}
//序列初始化接口,從模擬塊設備初始化內存結構
int initialize(char *fake_disk_path)
{
g_fake_disk = open(fake_disk_path, O_RDWR);
if (g_fake_disk == -1) {
return ERR_NOEXIST;
}
g_super = (struct filesys*)calloc(1, sizeof(struct filesys));
lseek(g_fake_disk, BOOTBSIZE, SEEK_SET);
read(g_fake_disk, g_super, sizeof(struct filesys));
g_super->s_size = 1024*1024;
g_super->s_itsize = INODEBSIZE;
g_super->s_freeinodesize = NUM;
g_super->s_freeblocksize = (g_super->s_size - (BOOTBSIZE+SUPERBSIZE+INODEBSIZE))/BSIZE;
g_root = iget(0);
//第一次的話要分配ROOT
if (g_root == NULL) {
g_root = ialloc();
g_root->i_finode.fi_addr[0] = balloc();
}
return ERR_OK;
}下面是一個測試程序:
int main()
{
int fd = -1,ws = -1;
char buf[16] = {0};
initialize("bigdisk");
mfcreat("/aa", MODE_FILE);
fd = mfopen("/aa", 0);
ws = mfwrite(fd, "abcde", 5);
mfread(fd, buf, 5);
mfcreat("/bb", MODE_DIR);
mfcreat("/bb/cc", MODE_FILE);
fd = mfopen("/bb/cc", 0);
ws = mfwrite(fd, "ABCDEFG", 6);
mfread(fd, buf, 5);
mflseek(0, 4);
ws = mfwrite(0, "ABCDEFG", 6);
mflseek(0, 0);
mfread(0, buf, 10);
mfclose(0);
mfdelete("/aa");
fd = mfopen("/aa", 0);
mfcreat("/aa", MODE_FILE);
fd = mfopen("/aa", 0);
syncsuper(g_super);
}這個文件系統實現得超級簡單,除去了很多額外的非本質的東西,并且也繞開了煩人的內存管理問題!于是,我的這個實現也就顯示了UNIX文件系統的本質。那么再看一下,還有什么東西雖然是額外的,但是卻是必不可少或者起碼說是很有意思的?答案很顯然,那就是空閑塊或者空閑inode的組織以及分配算法,然而這個算法可以單獨抽象出來。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。