6.824:Lab2-3 FileSystem
Lab2-3 File Server
Introduction(Lab2)
在这个实验中,将使用FUSE的接口完成一个文件系统
- CREATE/MKNOD, LOOKUP, 和 READDIR
- SETTATTR,WRITE,READ
这些操作是完成一个文件系统的最根本的几个操作之一。使用下图的架构。
我们将提供YFS和extent服务器模块的基本框架,如图[1]所示。
图[1] 整体系统结构
YFS模块时实现文件系统基本核心逻辑的模块,这一模块最后被编译为一个yfs_client支持本地挂载的文件系统。这一模块的代码框架由两部分组成:
- FUSE 接口,在这个fuse.cc中,这个代码将FUSE操作从内核模块变为了YFS客户端调用。实验已经提供给了FUSE内核的注册操作,需要做的是,修改fuse.cc使其能够正确地调用yfs_client提供的成员方法,再通过FUSE接口返回。
- YFS 客户端,在yfs_client.{cc,h}。YFS clients使用extent和lock服务实现了分布式文件系统的主要逻辑。例如当创建一个新的文件时,你的yfs_client就要在目录数据中增加一个目录项(item)在extent服务器中,这一模块是调用方,即为客户端,而不是想文件服务器,活着时锁服务器一样的服务器。(fuse->yfs_client->extent client-> extent server),如图[2]所示。 如图[2] 整体客户端结构,exe代表操作系统上运行的可执行文件,比如touch,ls等
extent服务器存储着文件系统所有的数据,像是在二进制文件中的硬盘的作用。在随后的实验中,你将在多个主机上运行YFSclient,他们都使用一个公共的extent服务,这也就意味着所有的主机使用着和共享着相同的数据。extent服务器代码框架包含有两个方面:
- Extent客户端 在extent_client.{cc.h}封装了与extent服务器通行的RPC。
- Extent服务器 在extent_server.{cc.h}和extent_smain,extent服务器管理了一个简单的K-V存储,它使用extent_protocol::extentid_t作为Keys,V包含两个方面,使用string作为存储的数据类型,使用一个结构体包换这个数据的属性(attr)。服务器包含四个方法:put(k,v), v_s get(k), v_a getattr(k), remove(k).
Getting Started
首先给电脑的操作系统安装FUSE库(详见实验概述)
框架代码仅仅实现了GETATTR和STATFS这两个接口,所以刚开始挂载的文件系统是不能够正常使用的。实验将完善文件系统中更多的FUSE的操作
- 第一部分:应当完成CREATE/MKNOD,LOOKUP,和READDIR。代码应当通过test-lab-2-a.pl这一脚本。这个脚本主要的测试项是创建空文件,在目录中查询文件名,列出目录中的文件内容。
- 第二部分:需要实现SETATTR,READ和WRITE,你的代码应当通过test-lab-2-b.pl这一脚本。这个脚本的主要测试读,写追加文件。还测试文件是否能够在一个客户端创建,在另一个客户端读取。 确保你在Linux操作的系统的用户在FUSE的用户组中,这样才能够操作FUSE格式的文件系统。
PART1:CREATE/MKNOD, LOOKUP, and READDIR
your job
在第一部分的工作是实现一个extent服务(存储服务),然后实现CREATE NKNOD,LOOKUP和READDIR 的FUSE文件操作。必须使用extent服务器来存储文件系统的内容,这样才能够完成part2中共享数据以及后续的实验。当然可以将extent的存储放在内存中(我就只使用了c++ 的map),只是一旦关闭extent服务器,所有的数据就都丢失了。
在一些系统中,FUSE使用MKNOD创建文件,在另一些系统中,使用CREATE。这两个接口有一些小区别,但是为了方便,我们提供了一个createhelper()接口,MKNOD或者CREATE接口都wrap了这个接口。实验所要做的工作就是实现createhelper()方法。
Step One
- 实现extent服务器:需要再extent_server.cc中实现这个服务。一共有四个操作put(k,v), get(k), getattr(k)和remove(k)。put和getRPC被用作更新和获取数据(extent的content)。getattrRPC获取数据的“属性”,属性包含了数据最后修改时间(mtime),最后改变属性的时间(ctime)和最后获取数据的时间(atime)。时间以秒作为单位从1970年起计算(unix时间戳)。attribute有点像unix系统的i-node。你可以使用转本的attribue存数,不用像inode一样将元数据和数据都存储在里边。当调用put()时,ctime和mtime一样改变。调用get()时,atime会改变。
-
文件/目录 ID
YFS和FUSE都需要一个能够辨识唯一的标识符,如果UNIX文件系统中的inode标号。FUSE使用32-bit作为标识符,你的YFS应当使用64-bit的数字作为标识符,其中高32位为0,低32位作为文件/目录的ID,在这个系统里,标识符在yfs_client.h成为inum。
当创建一个新的文件(使用fuseserver_createhelper)或者文件(fuseserver_mkdir),你不必分配一个inum,简单点地做法就是随机取一个数组,只要不重复就可以但要考虑当文件和目录不断增加时,冲突的概率。(我刚开始想学unix做bitmap,后来做到客户端了。。。所以这里偷了个懒,就使用随机数生成,使用map count作为判断是否冲突。)
int extent_server::GetInodeNum(int is_file, extent_protocol::extentid_t& rino){ extent_lock ex_lc(&pmutex_file_block_map_); extent_protocol::extentid_t ino; struct timeval tv; gettimeofday(&tv,NULL); srand(tv.tv_usec); do{ ino = rand() % 0x80000000LLU; if (ino == 1) continue; if(is_file){ ino |= 0x80000000LLU; } }while(file_block_.count(ino) > 0); rino = ino; return extent_protocol::OK; }
YFS需要通过inum来区分哪一个是文件数据,哪一个是目录数据。这里使用31bit作为inum号进行分配。这里使用
yfs_client::isfile
来判断,如果第31位为0,为目录,1为文件。bool yfs_client::isfile(inum inum) { if(inum & 0x80000000) return true; return false; }
-
目录格式:
下一个任务就是定义目录的存储结构格式。一个目录数据应当包含目录中名称和这对应inum的映射。当然存储的时候,需要把目录数据转换成一个string存储在extent服务器中。LOOKUP和READDIR读取目录内容,CREATE/MKNOD修改目录内容。以下是我对目录的定义:
/* *dir_block_:map<FILE_NAME, INUM> *dir_item_num_ :目录项个数 *StringToDirForm 将extent的数据 转换成目录结构数据 *DirFormToString 将目录结构数据 转化成extent数据 */ struct DirForm{ std::map<std::string, uint64_t> dir_block_; int dir_item_num_; void StringToDirForm(const std::string&); void DirFormToString(std::string&); }; void DirForm::StringToDirForm(const std::string& data){ const char* start = data.data(); std::size_t pos = 0; dir_item_num_ = *(int *) (start + pos); printf("dir_item_num_ = %d\n", dir_item_num_); pos += sizeof(int); for(int i = 0; i < dir_item_num_ && pos < data.size(); i++) { std::string item_name; item_name.assign((char *) (start + pos), MAX_ITEM_NAME_LEN); pos += MAX_ITEM_NAME_LEN; uint64_t item_num = *(uint64_t *)(start + pos); pos += sizeof(uint64_t); dir_block_.insert(std::make_pair(item_name, item_num)); } } void DirForm::DirFormToString(std::string& data){ data.clear(); dir_item_num_ = dir_block_.size(); char a32[4]; memcpy(a32, &dir_item_num_, 4); data.append(a32, sizeof(dir_item_num_)); typeof(dir_block_.begin()) mit = dir_block_.begin(); for(;mit != dir_block_.end(); mit++){ std::string tmp_str = mit->first; tmp_str.resize(MAX_ITEM_NAME_LEN); data.append(tmp_str.c_str(), MAX_ITEM_NAME_LEN); char a64[8]; memcpy(a64, &(mit->second), 8); data.append(a64, sizeof(mit->second)); } }
这里就不需要考虑文件所有者,模式或者是权限等。
-
FUSE:
当一个程序(像ls或者文本编辑器)在你的yfs_client中操作一个文件或者是目录时,在内核中的代码通过FUSE传递给yfs_client。实验已在fuse.cc中实现了这一接口,比如create,read,write等等操作。你应当在fuse.cc中修改相关的接口,fuse接口中的create调用yfs_client中的create。这个基本的操作可以参考已经实现的gettattr函数。
相关的一些方法可以在fuse_lowlevel.h中进行详细的理解,比如FUSE使用fuse_reply把成功的结果返回给内核,通过fuse_reply_err报告一个错误。
在通过READDIR读取目录信息的时候,使用了一些trick,我们已经实现了dirbuf_add,reply_buf_limited等方法,你所要做的就是修改FUSE中的READDIR让系统获得这个目录下的目录项。
尽管当你创建新的文件或是目录时,可以随意的使用inum作为标识符,但是FUSE是将0x00000001作为根目录(root)的。因此你应当确保0x00000001这个不会作为普通文件或者目录的inum,因为它已被占用
-
YFS code:
文件系统的主要逻辑应当在yfs_client中完成,因为大部分的操作都是fuse调用yfs_client。fuse只是作为一个handle。在yfs_client中,使用get(inum)从extent服务器获取数据。在目录文件中,要能够将string解析成文件目录结构。
-
tips
如果一个文件已经存在(filename在相同的目录下重复),CREATE操作就应当返回EEXIST给FUSE。
//创建文件或目录
yfs_client::status
fuseserver_createhelper(fuse_ino_t parent, const char *name,
mode_t mode, struct fuse_entry_param *e)
{
// In yfs, timeouts are always set to 0.0, and generations are always set to 0
e->attr_timeout = 0.0;
e->entry_timeout = 0.0;
e->generation = 0;
// You fill this in for Lab 2
uint64_t inum;
int ret = yfs->create(parent, name, inum);
if (ret != yfs_client::OK) {
return ret;
}
e->ino = inum;
getattr(inum, e->attr);
printf("fuseserver_createhelper create item succ!\n");
return yfs_client::OK;
}
yfs_client::status yfs_client::create(uint64_t parent, const char *name, uint64_t& ret_ino){
yfs_client::status ret;
std::string data;
std::string str_name = name;
str_name.resize(MAX_ITEM_NAME_LEN);
printf("parent id: %lu\n", parent);
printf("file name is %s\n", str_name.c_str());
YfsLock yl(lc, parent);
ret = getdata(parent, data);
if (ret != yfs_client::OK){
return yfs_client::NOENT;
}
DirForm dir_info;
dir_info.StringToDirForm(data);
if (dir_info.dir_block_.count(str_name) > 0) {
return yfs_client::EXIST;
}
ret_ino = GetAvailInum(true);
printf("fuseserver_createhelper ino:%lu\n", ret_ino);
if (ret_ino == 0) {
return yfs_client::NOENT;
}
dir_info.dir_block_[str_name] = ret_ino;
dir_info.DirFormToString(data);
ret = putdata(parent, data);
if (ret != yfs_client::OK) {
RestoreInum(ret_ino, true);
return ret;
}
data.clear();
ret = putdata(ret_ino, data);
if (ret != yfs_client::OK){
RestoreInum(ret_ino, true);
return ret;
}
yfs_client::fileinfo info;
ret = getfile(ret_ino, info);
if (ret != yfs_client::OK){
RestoreInum(ret_ino, true);
return ret;
}
return yfs_client::OK;
}
//LOOKUP操作
void
fuseserver_lookup(fuse_req_t req, fuse_ino_t parent, const char *name)
{
struct fuse_entry_param e;
// In yfs, timeouts are always set to 0.0, and generations are always set to 0
e.attr_timeout = 0.0;
e.entry_timeout = 0.0;
e.generation = 0;
bool found = false;
// You fill this in for Lab 2
yfs_client::status ret = yfs->lookup(parent, name, found, e.ino);
if (ret != yfs_client::OK)
goto lookup_end;
if(found){
getattr(e.ino, e.attr);
}
lookup_end:
if (found)
fuse_reply_entry(req, &e);
else
fuse_reply_err(req, ENOENT);
}
yfs_client::status yfs_client::lookup(uint64_t parent, const char *name, bool& found, uint64_t& imun){
DirForm dirinfo;
std::string data;
std::string str_name = name;
str_name.resize(MAX_ITEM_NAME_LEN);
printf("input name=%s\n", str_name.c_str());
std::map<std::string, uint64_t>::iterator mit;
YfsLock yl(lc, parent);
if(getdata(parent, data) != yfs_client::OK){
return yfs_client::NOENT;
}
dirinfo.StringToDirForm(data);
printf("fuseserver_lookup dirinfo.dir_block_ size is %lu\n", dirinfo.dir_block_.size());
mit = dirinfo.dir_block_.begin();
/*
for(;mit != dirinfo.dir_block_.end(); mit++) {
printf("name:%s,in:%llu\n",mit->first.c_str(), mit->second);
}
*/
if (dirinfo.dir_block_.count(str_name) > 0){
imun = dirinfo.dir_block_[str_name];
found = true;
}
return yfs_client::OK;
}
\\READDIR
void
fuseserver_readdir(fuse_req_t req, fuse_ino_t ino, size_t size,
off_t off, struct fuse_file_info *fi)
{
yfs_client::inum inum = ino; // req->in.h.nodeid;
struct dirbuf b;
printf("fuseserver_readdir\n");
if(!yfs->isdir(inum)){
fuse_reply_err(req, ENOTDIR);
return;
}
memset(&b, 0, sizeof(b));
// You fill this in for Lab 2
DirForm dirinfo;
yfs_client::status ret = yfs->readdir(dirinfo, ino);
if (ret != yfs_client::OK){
printf("readdir:yfs->readdir return %d", ret);
}
typeof(dirinfo.dir_block_.begin()) mit = dirinfo.dir_block_.begin();
for(;mit != dirinfo.dir_block_.end(); mit++){
dirbuf_add(&b, mit->first.c_str(), mit->second);
}
reply_buf_limited(req, b.p, b.size, off, size);
free(b.p);
}
yfs_client::status yfs_client::readdir(DirForm& dirinfo, uint64_t inum){
;
std::string data;
YfsLock yl(lc, inum);
yfs_client::status ret = getdata(inum, data);
if (ret != yfs_client::OK){
return ret;
}
dirinfo.StringToDirForm(data);
return yfs_client::OK;
}
PART2 SETTATTR,READ,WRITE(LAB2)
Part 2 Your Jobs
在Part中需要实现SETATTR,WRITE,READ等FUSE操作,只要通过了test-lab-2-b.pl Lab2就算是完成了。test-lab-2-b.pl测试了读、写、追加文件,在一个客户端写,查看另一个客户端是否可读。
Part2:Detailed Guidance
-
实现SETATTR
操作系统通过FUSE的SETATTR操作告诉你的文件系统设置文件属性。在to_set参数中设置哪一个需要被设置。目前只有一个文件属性需要注意就是文件大小的属性。设置FUSE_SET_ATTR_SIZE,操作系统也许会通过覆盖写,创建一个已存在的文件。那么就不会通调用FUSE的CREATE,而是SETATTR改变文件size属性。
-
实现READ\WRITE
READ(fuseserver_read)读一个文件,需要一个读取文件大小的size和从哪里开始的offset作为参数。当size小于文件size时,当读取数据超出了文件范围时,能读多少返回多少可读的数据。在linux系统中使用man 查看read的详细信息。
对于WRITE(fuseserver_write)来说,需要一个写入数据大小的size和从哪里开始的offset作为、数据这三个参数。当offset值大于文件原本size的时候,大于的部分全部填充‘\0’。
yfs_client::status yfs_client::read(uint64_t ino, size_t size, off_t off, std::string &buf){
std::string data;
YfsLock yl(lc, ino);
yfs_client::status ret = getdata(ino, data);
if (ret != yfs_client::OK) {
return yfs_client::NOENT;
}
if ((uint64_t)off >= data.size()) {
buf = "";
}else if ((off + size) > data.size()){
buf = data.substr(off);
}else{
buf = data.substr(off, size);
}
return yfs_client::OK;
}
yfs_client::status yfs_client::write(uint64_t ino, size_t size, off_t off, const char* buf){
std::string data;
std::string write_data;
YfsLock yl(lc, ino);
yfs_client::status ret = getdata(ino, data);
if (ret != yfs_client::OK) {
return ret;
}
if ((uint64_t)off > data.size()) {
data.resize(off);
}
write_data = data.substr(0, off);
if ((off + size) > data.size()){
write_data.append(buf, size);
} else {
std::string tmp_str(buf, size);
write_data += tmp_str;
write_data += data.substr(off + size);
}
ret = putdata(ino, write_data);
if (ret != yfs_client::OK){
return ret;
}
return ret;
}
yfs_client::status yfs_client::setattr(uint64_t ino, struct stat *attr){
std::string data;
YfsLock yl(lc, ino);
yfs_client::status ret = getdata(ino, data);
if (ret != yfs_client::OK) {
return ret;
}
data.resize(attr->st_size);
ret = putdata(ino, data);
if (ret != yfs_client::OK) {
return ret;
}
return ret;
}
Lab3:MKDIR,UNLINK,and Locking
Introduction
在这个实验中,
- 在FUSE中增加MKDIR和UNLINK操作
- 在yfs_client中加入锁操作,以确保在同一个目录下,不同用户之间操作不会有冲突
Part1:MKDIR,UNLINK
Your Job
这个任务主要是在FUSE中实现MKDIR和UNLINK,确保在MKDIR创建是,标志文件与目录的inum的那个bit位为0。对于MKDIR,你不用在创建每个目录的时候再创建“.”,“..”因为Linux内核对于YFS是透明的。UNLINK只需要删除对应inum的extent即可,不需要实现UNIX系统那样的文件应用计数。
如果全部通过test-lab-3-a.pl脚本测试,那么就算是完成了part1的内容了。这个测试脚本包含了创建目录,创建删除文件,检查目录extent的mtime和ctime属性。要注意的是测试脚本会检查时间属性的正确性。创建一个文件会同时改变父文件的mtime和ctime。
fuseserver_mkdir(fuse_req_t req, fuse_ino_t parent, const char *name,
mode_t mode)
{
struct fuse_entry_param e;
// In yfs, timeouts are always set to 0.0, and generations are always set to 0
e.attr_timeout = 0.0;
e.entry_timeout = 0.0;
e.generation = 0;
// Suppress compiler warning of unused e.
//(void) e;
// You fill this in for Lab 3
#if 1
yfs_client::status ret = yfs->mkdir(parent, name, e.ino);
if (ret == yfs_client::EXIST) {
fuse_reply_err(req, EEXIST);
return;
}
if (ret != yfs_client::OK) {
fuse_reply_err(req, ENOENT);
return;
}
ret = getattr(e.ino, e.attr);
if (ret != yfs_client::OK) {
fuse_reply_err(req, ENOENT);
return;
}
fuse_reply_entry(req, &e);
#else
fuse_reply_err(req, ENOSYS);
#endif
}
yfs_client::status yfs_client::mkdir(uint64_t parent, const char *name, uint64_t& ret_ino) {
yfs_client::status ret;
std::string data;
std::string str_name = name;
str_name.resize(MAX_ITEM_NAME_LEN);
printf("parent id: %lu\n", parent);
printf("file name is %s\n", str_name.c_str());
YfsLock yl(lc, parent);
ret = getdata(parent, data);
if (ret != yfs_client::OK){
return yfs_client::NOENT;
}
DirForm dir_info;
dir_info.StringToDirForm(data);
if (dir_info.dir_block_.count(str_name) > 0) {
return yfs_client::EXIST;
}
ret_ino = GetAvailInum(false);
printf("fuseserver_createhelper ino:%lu\n", ret_ino);
if (ret_ino == 0) {
return yfs_client::NOENT;
}
dir_info.dir_block_[str_name] = ret_ino;
dir_info.DirFormToString(data);
ret = putdata(parent, data);
if (ret != yfs_client::OK) {
RestoreInum(ret_ino, false);
return ret;
}
data.clear();
ret = putdata(ret_ino, data);
if (ret != yfs_client::OK){
RestoreInum(ret_ino, false);
return ret;
}
yfs_client::fileinfo info;
ret = getfile(ret_ino, info);
if (ret != yfs_client::OK){
RestoreInum(ret_ino, false);
return ret;
}
return yfs_client::OK;
}
void
fuseserver_unlink(fuse_req_t req, fuse_ino_t parent, const char *name)
{
// You fill this in for Lab 3
// Success: fuse_reply_err(req, 0);
// Not found: fuse_reply_err(req, ENOENT);
yfs_client::status ret = yfs->unlink(parent, name);
if (ret != yfs_client::OK) {
fuse_reply_err(req, ENOENT);
return;
}
fuse_reply_err(req, 0);
return;
fuse_reply_err(req, ENOSYS);
}
yfs_client::status yfs_client::unlink(uint64_t parent, const char *name){
yfs_client::status ret;
std::string data;
std::string str_name = name;
str_name.resize(MAX_ITEM_NAME_LEN);
YfsLock yl(lc, parent);
ret = getdata(parent, data);
if (ret != yfs_client::OK){
return yfs_client::NOENT;
}
DirForm dir_info;
dir_info.StringToDirForm(data);
std::map<std::string, uint64_t>::iterator del_mit = dir_info.dir_block_.find(str_name);
//printf("unlink str-name:%s\n, parent is %llu\n", str_name.c_str(), parent);
if (del_mit == dir_info.dir_block_.end()) {
return yfs_client::NOENT;
}
uint64_t ino = del_mit->second;
//printf("yfs_client::unlink ino:%ullX\n", ino);
if (!isfile(ino)) {
return yfs_client::NOENT;
}
/*
int ret_restore = RestoreInum(true, ino);
if (ret_restore != 0) {
return yfs_client::NOENT;
}
*/
dir_info.dir_block_.erase(del_mit);
dir_info.DirFormToString(data);
ret = putdata(parent, data);
if (ret != yfs_client::OK) {
return ret;
}
YfsLock yl2(lc, ino);
if (ec->remove(ino) != extent_protocol::OK) {
return yfs_client::NOENT;
}
return yfs_client::OK;
}
Part 2:Locking
下一步就要考虑当有多个yfs_client客户端运行的时候,保证文件操作的原子性了。现在如果yfs_client 的create方法会调用extent服务器中父目录的一些内容,做一些改变,并存储一些新的东西给extent服务器。在两个客户端同时运行的时候,会出点两个客户端同时获取一个extent服务器的旧数据的信息,每一个会分别插入一个新的内容在这个目录文件中,然后写入extent服务器中。那么最终这个extent中的数据是最后一个人新加的东西。当然正确的结果是这两个客户添加的东西都要加上,这是一个潜在的竞争。在并发CREATE UNLIN,并发 MKDIR UNLINK,并发WRITE中都会存在。
所以,可以要使用锁服务来消除竞争。比如yfs_client应当在CREATE的时候请求一个锁,当修改完毕后,在把这个锁释放掉。如果存在并发操作,这个锁会将并发的两个操作分开,保证原子性。左右yfs_client必须在相同的一个锁服务器上请求。逻辑如图[3]所示:
图[3]
Your Job
主要任务就时在yfs_client操作的时候加上锁来确保并发的正确性。test-lab-3-b和test-lab-3-c用作测试。如果你在添加锁之前见进行了测试,那么会在并发创建(concurrent create)。如果在添加了锁之后还有这个错误,那就是有bug了。
Detailed Guidance
-
关于锁
最极端的例子就是,整个文件系统中只是用一个锁,这样所有的操作都不会是并行的。还有一个比较极端的例子就是在一个目录下使用一个锁,当然这也不是一个好的方法。单一个全局变量的锁就保证了所有的并发,但是一个细粒度的锁就会增加开销,而且容易发生死锁,因为可能在某些操作时,会需要持有多个锁。
应当给每一个锁一个inum。这样在对文件或者目录做操作的时候就会比较容易,不用再做什么转换。对那个inum操作,申请哪个inum的锁,使用完后直接释放就好。当然在发生了error,也要及时的释放锁。
在yfs_client中使用lab1中实现的lock服务就好,就像使用extent_client一样方便。
-
注意以下事件
这是首次挂载两个YFS,如果在以前的实验中没有注意的话你可能在分配inum时会出现一些问题,即分配了两个相同的inum。有一种避免的方法就是使用随机数生成inum,可以通过pid生成。(我的代码使用的是时间毫秒级,秒级的不行)。
这次实验也是首次使用“\0”写入文件,如果使用std::string(char*)构造函数创建会失败,因为这个构造函数还把“\0”作为string’的结尾。如果使用这种构造函数进行写读写数据的初始化,会造成问题。使用std::string(bug, size)这一个函数代替。
22 May 2018 by John Brown