分布式文件系统-实验二:Basic File Server
作者 伊可 | 发布于 2015-09-30
Frangipani 分布式系统 实验Paxos算法 锁服务 文件系统

Lock Server

1、实验介绍

      这次实验主要实现下面的功能 create/mknod/lookup/readdir/ setattr/write/read. 这里包含文件的创建,读写,设置属性,读取目录内容,通过文件名查找文件的inum等. 整个yfs分布式文件系统的架构如下:

      其中客户端是yfs,负责实现文件逻辑:例如读写操作等.而服务端extent server 负责存储文件数据, 所以extent server的作用就像一个磁盘. 尽管图中有多个yfs运行在不同的主机上,但是它们"看到"的文件系统 都是一样的.

2、实现extent server

      在extent_server.h中有extent server的定义. 它的主要是负责存储文件数据,作用类似硬盘。每个文件都有一个id, 文件内容和对应的属性三个部分. id类似unix-like系统中的i-node 号 用来唯一标示一个文件. 所以在extent server 中建立起文件的存储数据结构如下:

    pthread_mutex_t mutex;//多个yfs客户端可以互斥的访问file_map的锁 struct extent { std::string data;//存储文件的数据 extent_protocol::attr attr;//存储文件的属性,它包含文件修改时间mtime,文件状态改变时间ctime,文件访问时间atime和文件大小size. }; std::map file_map;//存储文件的数据结构,每一个文件都是其中的一个对,其中key是文件id,value是extent类型的,包含文件内容和属性

      extent server 基本的操作: get: 获取文件内容 put: 将新的文件内容写回extent server. getaddr: 获取文件的属性 remove: 删除一个文件 在建立好文件的存储结构file_map后我们需要实现上面的四个方法.

      get的实现如下:

    int extent_server::get(extent_protocol::extentid_t id,std::string <buf) { // You fill this in for Lab 2. ScopedLock _l(<mutex); if (file_map.find(id) != file_map.end()) { file_map[id].attr.atime = time(NULL); buf = file_map[id].data; return extent_protocol::OK; } return extent_protocol::NOENT; }

      其中get获取id对应的文件内容,存放到buf中. 实现很简单,只需要在file_map中查询id对应的文件内容即可,但是需要注意修改文件属性中(访问时间)的atime. put的实现如下:

    int extent_server::put(extent_protocol::extentid_t id, std::string buf, int <) { // You fill this in for Lab 2. ScopedLock _l(<mutex); extent_protocol::attr attr; attr.atime = attr.mtime = attr.ctime = time(NULL); if (file_map.find(id) != file_map.end()) attr.atime = file_map[id].attr.atime; attr.size = buf.size(); file_map[id].data = buf; file_map[id].attr = attr; return extent_protocol::OK; }

      put将buf中的数据写入到id对应的文件中. id可能对应一个新文件, 也可能是一个已经存在的文件.无论是哪种情形都只要将id对应的文件的 内容更新为buf中的内容即可.主要需要修改文件属性ctime和mtime.如果这是一个新文件还需要atime.

      getattr的实现如下:

    int extent_server::getattr(extent_protocol::extentid_t id, extent_protocol::attr <a) { ScopedLock _l(<mutex); if (file_map.find(id) != file_map.end()) { a = file_map[id].attr; return extent_protocol::OK; } return extent_protocol::NOENT; }

      getattr查找id对应的文件,读取该文件的属性存放到a中.

      remove的实现如下:

    int extent_server::remove(extent_protocol::extentid_t id, int <) { // You fill this in for Lab 2. std::map&extent_protocol::extentid_t, extent>::iterator iter; iter = file_map.find(id); if (iter != file_map.end()) { file_map.erase(iter); return extent_protocol::OK; } else { return extent_protocol::NOENT; } }

      remove删除id对应的文件,另外实验要求系统启动后需要有一个名为root的空的根目录文件.所以在extent_server的构造函数中需要为extent_server创建这样一个root目录文件.

      简单的调用put即可.实现如下:

    extent_server::extent_server() { int ret; pthread_mutex_init(<mutex, NULL); //root i-number is 1 put(1, "", ret); }

3、create/mknod,lookup, readdir操作

      在实现文件的存储extent server后,接下来考虑create/mknod, lookup和readdir的实现. 当用户程序操作yfs_client(即前面提到的yfs客户端)管理(served)的文件或者目录(例如测试时产生的yfs1)时, FUSE在内核中的代码会将文件操作请求发送到yfs_client, yfs_client通过RPC与extent_server 交互. 我们需要修改fuse.cc中的代码来调用yfs_client中实现的文件逻辑. 在给出具体的实现前给出一些约定:

      文件id是64位的unsigned long long类型. 其中高32位为0. 低32位表示真实的id. 如果文件id的第31位为0那么这个 id对应的文件是一个目录.为1表示id对应一个文件. 文件目录的格式是一系列键值对.name是文件名,inum是文件id,但是为了方便实现.name前后各加一个"/", inum后加一个"/".所以一个目录项的实际是"/"+name+"/"+inum+"/"字符串

      create/mknod的实现: fuse.cc中fuseserver_createhelper函数是文件创建的核心函数. 具体的代码实现不列出. 该函数调用yfs_clent提供的create方法. yfs_clent中create的实现如下:

    int yfs_client::create(inum parent, const char *name, inum <inum) { int r = OK; std::string dir_data; std::string file_name; if (ec->get(parent, dir_data) != extent_protocol::OK) { r = IOERR; goto release; } file_name = "/" + std::string(name) + "/"; if (dir_data.find(file_name) != std::string::npos) { return EXIST; } inum = random_inum(true); if (ec->put(inum, std::string()) != extent_protocol::OK) { r = IOERR; goto release; } dir_data.append(file_name + filename(inum) + "/"); if (ec->put(parent, dir_data) != extent_protocol::OK) { r = IOERR; } release: return r; }

      create函数在目录parent创建一个名为name的文件空文件. 并且为这个文件生成一个唯一id存储在inum中. 首先读取parent的文件内容.查看文件name是否存在. 如果存在返回EXIST. 否则随机生成一个inum,并调用put 方法创建一个空文件. 然后将文件名和inum写入parent目录文件中.

      lookup的实现: fuse.cc中fuseserver_lookup函数负责将查询请求发送到yfs_client. 也需要我们自己实现, 具体的代码实现不列出. 该函数调用yfs_clent提供的lookup方法. yfs_clent中lookup的实现如下.

    int yfs_client::lookup(inum parent, const char *name, inum <inum, bool *found) { int r = OK; size_t pos, end; std::string dir_data; std::string file_name; std::string ino; if (ec->get(parent, dir_data) != extent_protocol::OK) { r = IOERR; goto release; } file_name = "/" + std::string(name) + "/"; pos = dir_data.find(file_name); if (pos != std::string::npos) { *found = true; pos += file_name.size(); end = dir_data.find_first_of("/", pos); if(end != std::string::npos) { ino = dir_data.substr(pos, end-pos); inum = n2i(ino.c_str()); } else { r = IOERR; goto release; } } else { r = IOERR; } release: return r; }

      lookup在目录parent查找名为name的文件,如果找到将found设置为true,inum设置为name对应的文件id. 首先读入目录文件parent的内容到dir_data. 然后在其中查找文件名name,注意name前后添加了"/",这是因为前面 我们约定了目录项的格式:"/"+name+"/"+inum+"/", 找到name后,可以根据这三个"/"之间的距离,提取出name和inum.

      readdir的实现: fuse.cc中fuseserver_readdir函数负责将readdir请求发送到yfs_client. 也需要我们自己实现,具体的代码实现不列出. 该函数调用yfs_clent提供的readdir方法. yfs_clent中readdir的实现如下:

    int yfs_client::readdir(inum inum, std::list&dirent> <dirents) { int r = OK; std::string dir_data; std::string inum_str; size_t pos, name_end, name_len, inum_end, inum_len; if (ec->get(inum, dir_data) != extent_protocol::OK) { r = IOERR; goto release; } pos = 0; while(pos != dir_data.size()) { dirent entry; pos = dir_data.find("/", pos); if(pos == std::string::npos) break; name_end = dir_data.find_first_of("/", pos + 1); name_len = name_end - pos - 1; entry.name = dir_data.substr(pos + 1, name_len); inum_end = dir_data.find_first_of("/", name_end + 1); inum_len = inum_end - name_end - 1; inum_str = dir_data.substr(name_end + 1, inum_len); entry.inum = n2i(inum_str.c_str()); dirents.push_back(entry); pos = inum_end + 1; } release: return r; }

      readdir读取目录文件inum, 一次提取出的键值对加入到dirents. 首先读取文件inum的内容到dir_data中. 根据目录的格式"/"+name+"/"+inum+"/". 逐个提取name和inum,最后加入到dirents中. dirents是一个dirent类型的list. dirent在yfs_client.h中定义如下

    struct dirent { std::string name; yfs_client::inum inum; };

      所以提取出的name和inum可以组成一个dirent结构,然后加入到dirents中.完成第一部分后使用如下命令进行测试

  • ./start.sh
  • /test-lab-2-a.pl ./yfs1
  • ./stop.sh
  • 如果第二条命令结果是Passed all tests!.则实现正确.即使设置RPC_LOSSY为5,也应该是Passed all tests!

    4、实现setattr, write个read

          这部分实现setattr, write个read. 分别是设置文件的属性,读和写文件. 完成这部分后可以实现这样的效果: 一个yfs_client写的数据可以被另外一个yfs_client读取.

          setattr的实现: fuse.cc中fuseserver_setattr函数函数负责将用户setattr请求发送到yfs_client. 也需要我们自己实现, 具体的代码实现不列出. 该函数调用yfs_clent提供的setattr方法. yfs_clent中setattr的实现如下:

      int yfs_client::setattr(inum inum, struct stat *attr) { int r = OK; size_t size = attr->st_size; std::string buf; if (ec->get(inum, buf) != extent_protocol::OK) { r = IOERR; goto release; } buf.resize(size, '\0'); if (ec->put(inum, buf) != extent_protocol::OK) { r = IOERR; } release: return r; }

          当前setattr只实现了设置文件大小的功能. 首先读取文件inum的内容, 然后将文件内容调整为size大小. 如果size比文件原来的长度大,那么多出的部分填充'\0'. 最后调用put写回文件内容,put会根据写回的内容长度修改文件对应的长度属性.

           read的实现 fuse.cc中fuseserver_read函数负责将用户read请求发送到yfs_client. 也需要我们自己实现, 具体的代码实现不列出. 该函数调用yfs_clent提供的read方法. yfs_clent中read的实现如下:

      int yfs_client::read(inum inum, off_t off, size_t size, std::string <buf) { int r = OK; std::string file_data; size_t read_size; if (ec->get(inum, file_data) != extent_protocol::OK) { r = IOERR; goto release; } if (off >= file_data.size()) buf = std::string(); read_size = size; if(off + size > file_data.size()) read_size = file_data.size() - off; buf = file_data.substr(off, read_size); release: return r; }

          read函数从文件inum的偏移off处读取size大小的字节到buf中. 如果off比文件长度大则读取0个字节. 如果off+size 超出了文件大小. 则读到文件尾即可. 该函数首先调用get读取文件inum的内容到 file_data中.然后根据off和size的大小,从file_data中取出合适的字节数到buf.

          write的实现: fuse.cc中fuseserver_write函数负责将用户write请求发送到yfs_client. 也需要我们自己实现,具体的代码实现不列出. 该函数调用yfs_clent提供的write方法. yfs_clent中write的实现如下:

      int yfs_client::write(inum inum, off_t off, size_t size, const char *buf) { int r = OK; std::string file_data; if (ec->get(inum, file_data) != extent_protocol::OK) { r = IOERR; goto release; } if (size + off > file_data.size()) file_data.resize(size + off, '\0'); for (int i = 0; i & size; i++) file_data[off + i] = buf[i]; if (ec->put(inum, file_data) != extent_protocol::OK) { r = IOERR; } release: return r; }

          write函数将buf中size个字节写入到文件inum的偏移off处. 这里需要考虑几种情形

  • 1、off比文件长度大, 那么需要将文件增大,并且使用'\0'填充,然后再从off处写入.
  • 2、off+size比文件长度大, 那么文件的大小将会根据写入的数据大小也会相应的调整.

           write首先读取文件inum的内容到缓冲区file_data. 接下来调整file_data的大小为最终的大小, 即off+size, 然后往file_data中写入数据. 最后调用put方法写回file_data中的数据.