Implementing file system with FUSE
During summer camp at Petnica Science Center you can get a chance to build something and work on your own research project. On computer science course, we usually do, well, computer science (sometimes it is multidisciplinary) so my friend decided to work on building her own file system. This is open source project and can be found here on GitHub. This is proof of concept of how file system work and it is built as stand-alone command-line application which has its own shell and uses RAM as its storage for data. Structure and organization of data is specific to project but it looks pretty much as first implementations of file systems.
You can copy files from your system’s file system to mi-fs but not vice-versa. The main problem is that it was a command-line application so we decided to implement our own kernel module for mi-fs. The problem with that approach is trouble with all kernel related development stuff. You have to be careful to avoid making kernel panic and so on. We found FUSE - File system in user space. You simply implement all operations you want your file system to support and start it as normal desktop app. FUSE kernel module will handle requests from outside world and redirect them to your application to handle requests. Your application responds with numerical return value and FUSE handles that response and then it knows what to do next. It is pretty simple (at least simpler than making your own kernel module and taking care of memory allocation and how to not make damage). I have to say that I am not happy about its documentation Documentation is auto-generated from code and sometimes it was very hard to understand what writer meant.
The whole thing is based of some kind C-style virtual function. You have structure with pointers to function, you implement these functions, set fields in that structure, call FUSE init function with that structure as argument and let FUSE handle all the stuff that follows.
This is an example of our main function:
#define FUSE_USE_VERSION 30
#include "interface.h"
static struct fuse_operations mi_ops;
int main(int argc, char* argv[])
{
mi_ops.getattr = mi_getattr;
mi_ops.mkdir = mi_mkdir;
mi_ops.mknod = mi_mknod;
mi_ops.rmdir = mi_rmdir;
mi_ops.rename = mi_rename;
mi_ops.open = mi_open;
mi_ops.read = mi_read;
mi_ops.write = mi_write;
mi_ops.opendir = mi_opendir;
mi_ops.readdir = mi_readdir;
mi_ops.init = mi_init;
mi_ops.utimens = mi_utimens;
mi_ops.unlink = mi_unlink;
return fuse_main(argc, argv, &mi_ops, NULL);
}
As you can see, we create structure and then we have to assign needed pointers to our functions. Functions are declared in interface.h so we can use them here. I will not explain how to implement every function alone and I will explain that in some other blog post aimed for that. I will just leave one example of implementation of function getattr as well as implementation of functions used in this implementation:
getattr.cpp:
#include "interface.h"
#include "context.h"
#include <sys/stat.h>
int mi_getattr(const char *path, struct stat * fstat)
{
char base[256] = {0};
char name[64] = {0};
mi_split_path(path, base, name);
node* curr = mi_get_destination(path);
if (curr == NULL) return -ENOENT;
mi_get_stat(curr, fstat);
return 0;
}
context.cpp:
...
node* mi_get_destination(const char* path)
{
if (strlen(path) == 1) return mi_get_context()->root;
else
{
char base[256];
char name[64];
mi_split_path(path, base, name);
node* curr = mi_get_destination(base);
if (curr == NULL) return NULL;
else
{
bool found = false;
node* nxt = curr->younger;
while (nxt != NULL && !found)
{
if (!strcmp(nxt->name, name)) {
found = true;
return nxt;
}
nxt = nxt->older_to;
}
if (!found) return NULL;
}
}
return NULL;
}
void mi_split_path(const char* path, char* basename, char* name)
{
int i = 0;
while (path[i] != '\0') i++;
if (i > 1) // ommit on the end of path (e.g. "/path/a/" becomes "/path/a" but "/" stays "/")
{
while (path[i-1] == '/')
{
i--;
}
}
int len=i;
i--;
while (path[i] != '/') i--;
int baselen = i;
int j;
for (j = 0; j<=baselen; j++) basename[j] = path[j];
basename[j] = '\0';
i++;
int l = 0;
while (path[i] != '\0' && path[i] != '/')
{
name[l] = path[i];
i++; l++;
}
name[l] = '\0';
}
void mi_get_stat(node* n, struct stat* fstat)
{
fstat->st_dev = 166;
fstat->st_ino = 0;
fstat->st_mode = S_IRWXU | S_IRWXG | S_IRWXO;
if (n->folder || n == mi_get_context()->root) fstat->st_mode |= S_IFDIR;
else fstat->st_mode |= S_IFREG;
fstat->st_nlink = 1;
fstat->st_uid = 1000;
fstat->st_gid = 1000;
fstat->st_rdev = 0;
fstat->st_size = 0;
fstat->st_blksize = 4096;
fstat->st_blocks = 1;
}
I have omitted some debugging code to make it more readable.
Implementation of these function was easy because we already had structure and implementation of file system behaviour. We only had to make interface to FUSE.
You can see complete implementation here.
NOTE: We haven’t implemented read to work so, in some kind, we have write-only file system (I am not kidding, see for yourself). This is because we couldn’t understand documentation so well and somehow FUSE refuses to work as we think it should. Maybe the problem is with FUSE but I doubt so. If you find where we are wrong, feel free to contact me to email or submit pull request on GitHub.
I bet we are all proud to our child, even if it does not work as we wanted. Thanks to Marina Ivanović who made file system structure and implemented file handling in memory and to Nikola Bebić who helped me to make interface to FUSE.