基于前文,我們繼續(xù)我們的文件系統(tǒng)的開發(fā)。在《基于Fuse的最簡單的文件系統(tǒng)》一文中我們實現(xiàn)了一個假文件系統(tǒng)。雖然呈現(xiàn)了目錄結(jié)構(gòu),但是不能創(chuàng)建新文件,也不能刪除文件,更加不能對文件進行讀寫。
接下來我們就增加更多的功能,使該文件系統(tǒng)具備創(chuàng)建文件和讀寫的功能。但是,我們本著循序漸進的原則,這里需要注意的是本文件系統(tǒng)還有諸多限制,具體如下:
- 目前的文件系統(tǒng)還是基于內(nèi)存的實現(xiàn),數(shù)據(jù)并不會持久化到存儲介質(zhì)上
- 只支持在根目錄創(chuàng)建和刪除文件
- 文件的大小限制在1024字節(jié)以內(nèi)
- 不支持在根目錄創(chuàng)建子目錄
- 只支持一次性讀寫,不支持指定偏移讀寫
- 最大可以創(chuàng)建256個文件
好吧,限制好多! 增加這么多限制的原因很簡單,就是為了讓降低大家的學(xué)習(xí)門檻。如果一下子實現(xiàn)一個支持所有功能的文件系統(tǒng),估計大家就沒有學(xué)習(xí)的興趣和動力了。所以我們每一個例子會增加一點點功能,逐漸實現(xiàn)一個功能完善的文件系統(tǒng)。至于本系列文章中涉及的技術(shù)細節(jié),大家可以參考本人拙著《文件系統(tǒng)技術(shù)內(nèi)幕》一書。
接下來我們回歸正題。雖然本地文件系統(tǒng)的基本原理是將線性的硬盤空間抽象為樹型層級結(jié)構(gòu),但是底層存儲并不一定要是線性的地址空間。為了實現(xiàn)簡單,本文我們會借助STL的容器實現(xiàn)文件系統(tǒng)的一些基本功能。當(dāng)然我們最終實現(xiàn)的文件系統(tǒng)肯定是基于硬盤的,目前的設(shè)計思路是為了降低學(xué)習(xí)的門檻和坡度,達到循序漸進的目的。
為了支持256個最大為1KB的內(nèi)存文件,我們首先需要分配256KB的內(nèi)存空間(用如下代碼中的data_space表示)。同時,為了管理這些空間,也就是記錄哪些空間已經(jīng)被使用,哪些空間還是可用狀態(tài),我們通過創(chuàng)建了一個位圖類型的變量(data_bitmap)。

另外,為了記錄根目錄中文件名稱與文件數(shù)據(jù)的對應(yīng)關(guān)系,我們創(chuàng)建了一個map類型的變量files。通過這個變量,我們可以根據(jù)文件名稱獲取文件的描述信息,比如文件數(shù)據(jù)的位置和大小等。這里文件的描述信息是通過一個名稱為inode的結(jié)構(gòu)體表示的,這也是模仿的Linux文件系統(tǒng)中的概念。
#define DATA_SPACE_LEN (256)
static map<string, inode*> files;
static vector<char*> data_space;
static bitset<DATA_SPACE_LEN> data_bitmap;
如下是結(jié)構(gòu)體inode的具體定義,這里一共包含4項內(nèi)容,都是最為基本的屬性描述。本文我們主要使用了data_index和size兩個成員變量,分別表示文件數(shù)據(jù)的位置和大小。
struct inode
{
unsigned int inode_id;
unsigned int mode;
unsigned short data_index;
unsigned short size;
};
如下代碼是初始化的代碼,本例中我們主要將內(nèi)存區(qū)域清零,并將位圖清零。這表示目前我們有一個干凈的,沒有任何文件的文件系統(tǒng)。后續(xù)我們在實現(xiàn)基于硬盤的文件系統(tǒng)時可以在這里實現(xiàn)硬盤格式化的工作。需要注意的是,在init函數(shù)中我們調(diào)用了syslog函數(shù),該函數(shù)用于向系統(tǒng)日志中記錄一條日志,方便我們了解程序的運行情況。
/* 初始化的時候被調(diào)用 */
void* MemoryFS::init(struct fuse_conn_info *conn, fuse_config*)
{
syslog(LOG_NOTICE, "init\n");
for (int i = 0; i < DATA_SPACE_LEN; i++) {
char* block = new char[1024];
memset(block, 0, 1024);
data_space.push_back(block);
data_bitmap.reset(i);
}
return 0;
}
然后回到本文的核心內(nèi)容,實現(xiàn)文件的創(chuàng)建和讀寫。文件的創(chuàng)建和讀寫一共涉及4個函數(shù),分別是create、open、write和read。其中create是創(chuàng)建文件時被調(diào)用,open是打開文件時被調(diào)用,write和read分別數(shù)據(jù)寫讀的時候被調(diào)用。接下來我們分別看看代碼實現(xiàn)。
首先要看的自然是創(chuàng)建文件的實現(xiàn),也就是create函數(shù)的實現(xiàn),具體代碼如下所示。本例實現(xiàn)原理是在創(chuàng)建一個文件的時候?qū)⒃撐募鎯υ谝粋€map數(shù)據(jù)結(jié)構(gòu)中,這樣后續(xù)讀寫文件的時候就可以找到該文件。在本例中,我們只是創(chuàng)建一個inode實例,并構(gòu)建文件系統(tǒng)與inode的映射關(guān)系。我們這里實現(xiàn)的非常簡單,相當(dāng)于在根目錄中增加了一個文件。
int MemoryFS::create(const char *name, mode_t mode, struct fuse_file_info *)
{
syslog(LOG_NOTICE, "Create: %s\n", name);
inode *i = new inode();
memset(i, 0, sizeof(inode));
files[name] = i; // 存儲到map中
return 0;
}
打開文件的函數(shù)實現(xiàn)也是非常簡單的,我們只需要查詢一下map,看看要打開的文件是否存在。如果不存在需要返回ENOENT。
int MemoryFS::open(const char *name, struct fuse_file_info *)
{
int status = 0;
auto inode = files.find(name);
if (inode == files.end()) {
status = -ENOENT;
}
syslog(LOG_NOTICE, "Open: %s %d\n", name, status);
return 0;
}
接下來到正題了,也就是文件寫數(shù)據(jù)的功能。首先是寫數(shù)據(jù)的函數(shù)原型,可以看出與Linux的write函數(shù)類似。不同的地方是Linux API第一個參數(shù)是句柄,而本函數(shù)是文件名稱。當(dāng)我們在根目錄中向某個文件寫入數(shù)據(jù)的時候就會觸發(fā)給函數(shù)。該函數(shù)的核心功能是找到前面創(chuàng)建的文件,并且找到一個內(nèi)存空間用于存儲用戶要寫入的數(shù)據(jù)。找到具體的信息后,會將數(shù)據(jù)拷貝到內(nèi)存空間,并更新inode的記錄。
int MemoryFS::write(const char *name, const char *buf, size_t buf_size, off_t offset, struct fuse_file_info *)
{
auto inode = files.find(name); // 查找要寫入數(shù)據(jù)的文件
char* content = nullptr;
short index = 0;
int status = 0;
if ( inode == files.end() ) { // 如果沒有找到文件,返回相應(yīng)的錯誤碼
status = -ENOENT;
goto OUT;
}
if ( inode->second->size ) { // 判斷文件是否已經(jīng)有數(shù)據(jù)
index = inode->second->data_index;
} else {
for (int i = 0; i < DATA_SPACE_LEN; i++) { // 如果沒有數(shù)據(jù),根據(jù)位圖查找一個可用的空間
if (!data_bitmap[i]) {
index = i;
data_bitmap.set(i);
break;
}
}
}
content = data_space[index]; // 獲取空間的地址
memcpy(content, buf, buf_size); // 將數(shù)據(jù)寫入內(nèi)存空間
inode->second->data_index = index; // 更新數(shù)據(jù)的位置
inode->second->size = buf_size; // 更新文件大小
OUT:
syslog(LOG_NOTICE, "Write: %s %s %d\n", name, buf, buf_size);
return buf_size;
}
讀數(shù)據(jù)的實現(xiàn)邏輯與寫數(shù)據(jù)類似,首先我們需要從map中查找到對應(yīng)的文件。如果該文件存在,則會返回文件對應(yīng)的inode實例。前文已述,inode中包含著文件數(shù)據(jù)的位置和大小等信息。根據(jù)inode中保存的信息,我們可以將數(shù)據(jù)拷貝到讀數(shù)據(jù)的緩沖區(qū),這樣在調(diào)用者就可以看到數(shù)據(jù)了。
int MemoryFS::read(const char *name, char *buf, size_t buf_size, off_t offset, struct fuse_file_info *)
{
auto inode = files.find(name); // 查找文件的inode信息
char* content = nullptr;
short index = 0;
short file_size = 0;
int status = 0;
if ( inode == files.end() ) {
status = -ENOENT;
goto OUT;
}
index = inode->second->data_index; // 根據(jù)inode信息確定文件數(shù)據(jù)的位置和大小
file_size = inode->second->size;
content = data_space[index];
memcpy(buf, content, file_size); // 將數(shù)據(jù)拷貝到讀緩沖區(qū)
OUT:
syslog(LOG_NOTICE, "Read: %s C %s B %s %d %d %d\n", name, content, buf, index, file_size, buf_size);
return file_size;
}
雖然上述函數(shù)實現(xiàn)后就可以實現(xiàn)我們的目標(biāo)功能了。但是如果我們用ls命令查看文件,則需要實現(xiàn)目錄遍歷的功能,這部分功能我們在前面文章介紹過。首先是需要實現(xiàn)readdir功能,用于遍歷目錄項。本例中是遍歷map類型的變量。
int MemoryFS::readdir(const char *path, void *buf, fuse_fill_dir_t filler,
off_t, struct fuse_file_info *, enum fuse_readdir_flags)
{
syslog(LOG_NOTICE, "readdir: %s\n", path);
filler(buf, ".", NULL, 0, FUSE_FILL_DIR_PLUS);
filler(buf, "..", NULL, 0, FUSE_FILL_DIR_PLUS);
for (const auto& [key, value] : files) { // 實現(xiàn)目錄項的遍歷
filler(buf, key.c_str() + 1, NULL, 0, FUSE_FILL_DIR_PLUS);
}
return 0;
}
然后是實現(xiàn)getattr,該函數(shù)用于獲取每個文件/目錄的詳細屬性。這里我們也是偷懶了,很多屬性是寫死的。這個函數(shù)的實現(xiàn)并不復(fù)雜,大家可以自行閱讀一下相關(guān)代碼。
int MemoryFS::getattr(const char *path, struct stat *stbuf, struct fuse_file_info *)
{
int res = 0;
memset(stbuf, 0, sizeof(struct stat));
if (path == root_path) {
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
} else if (files.find(path) != files.end()) {
auto inode = files.find(path);
stbuf->st_mode = S_IFREG | 0755; // 我們這里模式是寫死的,
stbuf->st_nlink = 1;
stbuf->st_size = inode->second->size;
syslog(LOG_NOTICE, "getattr file: %s %d\n", path);
} else {
res = -ENOENT;
syslog(LOG_NOTICE, "getattr error: %s\n", path);
}
syslog(LOG_NOTICE, "getattr: %s %d\n", path, res);
return res;
}
至此,我們完成了所有函數(shù)的介紹。相關(guān)代碼已經(jīng)更新到作者的github空間,大家可以自行下載編譯實驗一下。接下來我們將進一步豐富功能,實現(xiàn)一個可以持久化的文件系統(tǒng)。
注: 本文配套的源代碼可以在github的SunnyZhang-IT/fs-from-zero庫中找到。