MQH Blog

Writing Less Simple, Yet Stupid Filesystem Using FUSE in C

Introduction

user

Mohammed Q. Hussain

A Programmer.


Featured

FUSE Linux Filesystem C

Writing Less Simple, Yet Stupid Filesystem Using FUSE in C

Posted by Mohammed Q. Hussain on .

In the previous tutorial we learned how to write Simple, Stupid Filesystem (SSFS) by using FUSE, we have covered the basics of FUSE to write a really simple filesystem which is able to list root directory’s files, return the attributes of available files and read the content of a file. In the current tutorial we are going to extend what we have discussed and create a Less Simple, Yet Stupid Filesystem (LSYSFS) which is able to create new directories, create new files and write some content in the files.

Please note that some parts of this tutorial is already covered in SSFS tutorial, therefore, I recommend you to read it first. Also, please make sure that FUSE is installed on your Linux machine and you are ready to go. I used version 2.9.7 of FUSE in this tutorial, which also runs SSFS with no issues.

What “Less Simple, Yet Stupid Filesystem (LSYSFS)” will be able to do?

We have mentioned previously that LSYSFS will be able to do the following:

  1. Create new directories by implementing the function do_mkdir for the event mkdir and introducing a simple array to maintain the list of directories created by the user.
  2. Create new files by implementing the function do_mknod for the event mknod and introducing a simple array to maintain the list of files created by the user.
  3. Write to files by implementing the function do_write for the event write and introducing a simple array to maintain the contents of the files.

As SSFS, LSYSFS is an in-memory filesystem. That means all the information of the filesystem (e.g. created directories and files) will reside in the memory and will not be stored persistently on the disk, once the filesystem is unmounted, all of its contents will go.

For the sake of simplicity, the new files and directories can be only created in the root directory of the filesystem “/”, we will call this limitation “root-level-only objects” to ease our discussions later on. An object is either a directory or a file. However, the concept will be the same if you want to make it possible to create files and directories in a sub-directory, but more advanced data structure should be used instead.

Including Headers

We need to start our C file with the following code to include needed headers:

#define FUSE_USE_VERSION 30

#include <fuse.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

Initializing Data Structures

Before working with FUSE API, we are going to work on our data structures and their auxiliary functions. The goal of these data structure is to keep the list of directories and files which are created by the user, also, the content of these files will be maintained by one of these data structures. Because we aim to create a simple filesystem here, our data structures will be simple arrays.

char dir_list[ 256 ][ 256 ];
int curr_dir_idx = -1;

char files_list[ 256 ][ 256 ];
int curr_file_idx = -1;

char files_content[ 256 ][ 256 ];
int curr_file_content_idx = -1;

The first array is “dir_list” which maintains the names of directories that have been created by the user, the second array “files_list” maintains the names of files that have been created by the user and the third array “files_content” maintains the contents of the files. As you can see all of them are arrays of strings, each array can store 256 strings and each string has the maximum length of 256 bytes. That means, we can only create 256 directories and 256 files in LSYSFS. Furthermore, the length of filename, directory name or file content can be only 256 character.

The variables “curr_dir_idx”, “curr_file_idx” and “curr_file_content_idx” indicate the current index of each array. Initially, there is no entries in any of these arrays, so we start with -1 for each index. Now, let’s define some functions for these arrays to make our life eaiser when we start dealing with FUSE.

void add_dir( const char *dir_name )
{
	curr_dir_idx++;
	strcpy( dir_list[ curr_dir_idx ], dir_name );
}

The first function is “add_dir” which adds a new directory to the list of directories. It takes the name of the directory that we wish to create. As you can see, we first increment the current index of directories then we copy the name of the new directory to the new position of the array. So, when the first directory is created, the value of “curr_dir_idx” which is -1 will be 0 and the name of the first directory will be copied to the position 0 (which is, as you now, the first position of the array in C) of the array dir_list. Let’s move to the second function.

int is_dir( const char *path )
{
	path++; // Eliminating "/" in the path
	
	for ( int curr_idx = 0; curr_idx <= curr_dir_idx; curr_idx++ )
		if ( strcmp( path, dir_list[ curr_idx ] ) == 0 )
			return 1;
	
	return 0;
}

Given a path, the function “is_dir” is going to check if this path is a directory available in the filesystem by trying to find the name of directory in the directories list “dir_list”, if there is a directory with such name, “is_dir” returns 1, otherwise it returns 0.

Now, to understand the line “path++” we need to discuss the point of root-level-only objects further. When the user needs to deal with an object (a file or directory) in the filesystem, the full path of this object will be sent to our filesystem by FUSE. For example, when the user wishes to write some content to the file “bar” which resides in the directory “foo”, FUSE sends a write request to our filesystem with the full path “/foo/bar” of the file.

LSYSFS supports root-level-only objects, the means there will be no files or directories inside a sub-directory as in the example of “/foo/bar”, that means any path recieved by LSYSFS will be in the following format “/[Object Name]”, which indicates that the object resides in the root directory of the filesystem. And as you have seen in the function “add_dir” (and as you will see later in the function “add_file”), the names of the objects will be stored in the data structures instead of their paths, therefore, we need to get rid of the first slash in the received path which indicates that the object resides in the root directory. That’s exactly what the line “path++” does. After this line, we will have only the name of the object, and we can easily try to find it by using a simple sequential search on the array “dir_list” and that’s implemented in the for loop block of “is_dir” function.

Well, that’s it with the functions that relate to directories list. Let’s now move to the data structures of both files list and files contents.

void add_file( const char *filename )
{
	curr_file_idx++;
	strcpy( files_list[ curr_file_idx ], filename );
	
	curr_file_content_idx++;
	strcpy( files_content[ curr_file_content_idx ], "" );
}

Same as “add_dir”, the function “add_file” adds a new file in the filesystem by adding a new entry in the array “files_list” with the name of the new file. It also initializes the content of this file to be an empty string and stores this content in the array “files_content” as we have mentioned before.

You can see here that the files and their contents are related to each other by the file index which should be the same in both arrays “files_list” and “files_content”, that is, if a file in index x of the array “files_list”, its content should be in the same index x in the array “files_content”.

For example, let’s assume that the first file named “foo” is created in the filesystem by “add_file” function, its file index will be 0 in “files_list” array which stores its name “foo”. Also, its initial content which is an empty string will be in the same file index (0) in contents array “file_content”, so to refer to the file “foo” in our filesystem, we use its file index which is 0, in the way we can obtain both its name and its content. This method reminds us a little bit with inode. Let’s move to the next function.

int is_file( const char *path )
{
	path++; // Eliminating "/" in the path
	
	for ( int curr_idx = 0; curr_idx <= curr_file_idx; curr_idx++ )
		if ( strcmp( path, files_list[ curr_idx ] ) == 0 )
			return 1;
	
	return 0;
}

Same as “is_dir”, this function takes a path and see if there is a file with the same name exists in the filesystem. It returns 1 if the file exists, otherwise 0 will be returned. No further explanation required for this function since it resembles “is_dir” with the only difference that it works with “files_list” array instead of “dir_list” array.

int get_file_index( const char *path )
{
	path++; // Eliminating "/" in the path
	
	for ( int curr_idx = 0; curr_idx <= curr_file_idx; curr_idx++ )
		if ( strcmp( path, files_list[ curr_idx ] ) == 0 )
			return curr_idx;
	
	return -1;
}

Given a path, the function “get_file_index” returns the file index that is valid to be used with the both arrays “files_list” and “files_content”. Same as “is_file”, it uses sequential search on the array “files_list” to find if there is some file which has the given name. If such file is found in the array its index in the array will be returned. If no such file, -1 will be returned.

void write_to_file( const char *path, const char *new_content )
{
	int file_idx = get_file_index( path );
	
	if ( file_idx == -1 ) // No such file
		return;
		
	strcpy( files_content[ file_idx ], new_content ); 
}

The final function of data structures manipulation is “write_to_file”, it takes the path of the file that the user wishes to write some content on and the content, then it writes this content on the file.

As a first step, “write_to_file” tries to obtain the file index by using the predefined function “get_file_index”. If no such file, it does nothing. Otherwise, it is going to use the file index to copy the new content to the contents array “files_content”.

Well, we have declared our data structures, and written the functions that help us in manipulating these data structure. We are ready now to implement LSYSFS with FUSE.

Implementing Less Simple, Yet Stupid Filesystem

To write a LSYSFS, we need to implement functions for the following FUSE events: getattr, readdir, read, mkdir, mknod and write. The first three has have covered with details previously in SSFS tutorial. Therefore, we are going to pass through them with less details.

Implementing “do_getattr”

The function of “getattr” event is the one which provides the information about a specific object for the operating system. It’s the one that tells the operating system the type of the object in question if it is a file, directory or something else. Also, it tells about the owner, group and permission bits of the object. This function is a vital one that without it we can’t have a functional filesystem. Let’s start implementing it with the name “do_getattr”.

static int do_getattr( const char *path, struct stat *st )
{

The first parameter is the path of the object (file or directory) that the operating system is asking about, while the second parameter is the structure will be filled with object’s information. Let’s continue.

	st->st_uid = getuid();
	st->st_gid = getgid();
	st->st_atime = time( NULL );
	st->st_mtime = time( NULL );

Here we fill the following information about the object in question: its owner, its group, last access time and last modification time. Due to the simplicity, we are filling information that are not real in these fields. Of course, in real filesystem, this kind of information will be stored in some data structure and should be accurate.

	if ( strcmp( path, "/" ) == 0 || is_dir( path ) == 1 )
	{
		st->st_mode = S_IFDIR | 0755;
		st->st_nlink = 2; // Why "two" hardlinks instead of "one"? The answer is here: http://unix.stackexchange.com/a/101536
	}

After filling the basic information, we are checking if the object in question is a directory or not as you can see in the if statement. If the object in question is the root of the filesystem, we consider it as directory, otherwise we try to check our data structure if there is such directory in the filesystem or not by using the function “is_dir” which we have defined above. In case that the object in question is a directory, we fill “st_mode” field with the type “S_IFDIR” which means directory, also, the permission bits will be filled in the same field. Then, the number of hardlinks will be filled in the filed “st_nlink”.

	else if ( is_file( path ) == 1 )
	{
		st->st_mode = S_IFREG | 0644;
		st->st_nlink = 1;
		st->st_size = 1024;
	}

If the object in question isn’t a directory, we check our data structures to see if there is a file with such name. If the file is found, the type “S_IFREG” which means regular file will be filled in “st_mode” field, the permission bits, the number of hard links and finally the size of the file which will always be 1024 bytes for the sake of simplicity.

	else
	{
		return -ENOENT;
	}

Finally, if we didn’t find the object in question in our directories list and files list, that means there is no such object in the filesystem. Therefore, we need to tell the operating system that there is no such file/directory by returning -ENOENT. It is crucial to tell the operating system that a file/directory doesn’t exist to be able to create new files and directories.

	
	return 0;
}

Finally, when everything is fine, we return 0 as a success sign. The full code of “do_getattr” is the following:

static int do_getattr( const char *path, struct stat *st )
{
	st->st_uid = getuid(); // The owner of the file/directory is the user who mounted the filesystem
	st->st_gid = getgid(); // The group of the file/directory is the same as the group of the user who mounted the filesystem
	st->st_atime = time( NULL ); // The last "a"ccess of the file/directory is right now
	st->st_mtime = time( NULL ); // The last "m"odification of the file/directory is right now
	
	if ( strcmp( path, "/" ) == 0 || is_dir( path ) == 1 )
	{
		st->st_mode = S_IFDIR | 0755;
		st->st_nlink = 2; // Why "two" hardlinks instead of "one"? The answer is here: http://unix.stackexchange.com/a/101536
	}
	else if ( is_file( path ) == 1 )
	{
		st->st_mode = S_IFREG | 0644;
		st->st_nlink = 1;
		st->st_size = 1024;
	}
	else
	{
		return -ENOENT;
	}
	
	return 0;
}

More about the parameters, return values and errors of getattr is here.

Implementing “do_readdir”

The function of the event readdir provides the operating system with the list of files/directories that reside in a given directory. Our implementation of this function is going to traverse through the two arrays “dir_list” and “files_list” and return their content, and due to LSYSFS’s root-level-only objects limitation, this is going to happen only when the operating system asks about the files/directories inside the root directory. Let’s start implementing “do_readdir”.

static int do_readdir( const char *path, void *buffer, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi )
{
	filler( buffer, ".", NULL, 0 ); // Current Directory
	filler( buffer, "..", NULL, 0 ); // Parent Directory

We mentioned earlier that the parameter “buffer” is where we fill the list of available files/directories that reside in the directory of question. Also the parameter “filler” is a function provided by FUSE that we can use to fill the “buffer”. You can see in the beginning of the function we filled the buffer with the two directories “.” and “..” as we have done with SSFS. Let’s continue.

	if ( strcmp( path, "/" ) == 0 )
	{
		for ( int curr_idx = 0; curr_idx <= curr_dir_idx; curr_idx++ )
			filler( buffer, dir_list[ curr_idx ], NULL, 0 );
	
		for ( int curr_idx = 0; curr_idx <= curr_file_idx; curr_idx++ )
			filler( buffer, files_list[ curr_idx ], NULL, 0 );
	}

Now, we check if the directory in question is the root directory of LSYSFS, we start to traverse through the array “dir_list” and fill its content in the buffer, then we do the same with “files_list” array.

	
	return 0;
}

Finally, we return 0 to indicate that everything is fine. The full code of “do_readdir” is the following:

static int do_readdir( const char *path, void *buffer, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi )
{
	filler( buffer, ".", NULL, 0 ); // Current Directory
	filler( buffer, "..", NULL, 0 ); // Parent Directory
	
	if ( strcmp( path, "/" ) == 0 ) // If the user is trying to show the files/directories of the root directory show the following
	{
		for ( int curr_idx = 0; curr_idx <= curr_dir_idx; curr_idx++ )
			filler( buffer, dir_list[ curr_idx ], NULL, 0 );
	
		for ( int curr_idx = 0; curr_idx <= curr_file_idx; curr_idx++ )
			filler( buffer, files_list[ curr_idx ], NULL, 0 );
	}
	
	return 0;
}

Implementing “do_read”

The function of the event read is called when the operating system wants to read the content of a specific file. In this function we are going to depend on the array “files_content” to get the content of a file.

static int do_read( const char *path, char *buffer, size_t size, off_t offset, struct fuse_file_info *fi )
{
	int file_idx = get_file_index( path );
	
	if ( file_idx == -1 )
		return -1;

As we mentioned, the file index is needed to know in which index the required content is reside in “files_content”. To get the index we use the function “get_file_index” which we have already defined above. When the file doesn’t exists -1 will be returned.

	char *content = files_content[ file_idx ];
	
	memcpy( buffer, content + offset, size );
		
	return strlen( content ) - offset;
}

After locating the file index of the file in question, we get its content from “files_content” array, then copy the content to the “buffer” which is provided as a parameter. Finally, the number of bytes that have been read will be returned. The details of size and offset parameters can be found in the previous tutorial. The full code of “do_read” is the following:

static int do_read( const char *path, char *buffer, size_t size, off_t offset, struct fuse_file_info *fi )
{
	int file_idx = get_file_index( path );
	
	if ( file_idx == -1 )
		return -1;
	
	char *content = files_content[ file_idx ];
	
	memcpy( buffer, content + offset, size );
		
	return strlen( content ) - offset;
}

More about the parameters, return values and errors of read is here.

Implementing “do_mkdir”

Now, we start with the first event that has not been used previously in SSFS, it is mkdir which will be called when the user wishes to create a new directory. The function of mkdir event takes two parameters, the first one, as usual, is the path of the directory that the user wants to create. The second parameter is “mode” which specifies permission bits. The implementation of “do_mkdir” is too simple thanks to the predefined functions of LSYSFS’s data structures.

static int do_mkdir( const char *path, mode_t mode )
{
	path++;
	add_dir( path );
	
	return 0;
}

That’s it! We first eliminate the slash of the root directory path for the reason the we explained above, then, we simply call the function “add_dir” with the name of the new directory. “add_dir” is going to do the job by adding the name of the new directory in directories data structure “dir_list”. Finally, we return 0 to indicate that everything is fine.

More about the parameters, return values and errors of mkdir is here.

Implementing “do_mknod”

The function of mknod event is called when the user wishes to create a new file. The new file can be a regular file, device file or named pipe file. However, we are interested on regular files.

static int do_mknod( const char *path, mode_t mode, dev_t rdev )
{
	path++;
	add_file( path );
	
	return 0;
}

As in “do_mkdir”, the function “do_mknod” is also too simple and works the same way. It takes the following parameters: “path” which is the path of the new file, “mode” which specifices the permission bits and the type of the new file (regular, device or named pipe) and “rdev” which should be specified if the new file is a device file.

In the same manner of “do_mkdir”, the slash of the root directory will be eliminated from the path, and the function “add_file” will be used to add the new file to “files_list” array and initialize the content of the new file. Finally, the function returns 0 to indicate that everything is fine.

More about the parameters, return values and errors of mknod is here.

Implementing “do_write”

The last function that we are going to implement is “do_write” which is the function of write event. This function will be called when the user wishes to write content the a specific file.

static int do_write( const char *path, const char *buffer, size_t size, off_t offset, struct fuse_file_info *info )
{
	write_to_file( path, buffer );
	
	return size;
}

The parameter “buffer” contains the content that the user whishes to write to the file in “path”. Both “size” and “offset” are same as those on the function of “read” event, because LSYSFS is too simple, and the maximum size of the file is only 256 bytes we are not going to deal with these two parameters.

The function that we have defined above “write_to_file” will be used to copy the content of “buffer” to “files_content” data structure. The number of written bytes will be returned.

More about the parameters, return values and errors of write is here.

Filling “fuse_operations” & Telling FUSE About It

Now we fill “fuse_operations” structure and call the main function of FUSE which is going to run our filesystem:

static struct fuse_operations operations = {
    .getattr	= do_getattr,
    .readdir	= do_readdir,
    .read		= do_read,
    .mkdir		= do_mkdir,
    .mknod		= do_mknod,
    .write		= do_write,
};

int main( int argc, char *argv[] )
{
	return fuse_main( argc, argv, &operations, NULL );
}

Well, we have got Less Simple, Yet Stupid Filesystem! :-)

Compiling & Mounting The Filesystem

You can use GCC to compile LSYSFS as the following:

gcc lsysfs.c -o lsysfs `pkg-config fuse --cflags --libs`

The “pkg-config” part is going to provide the compiler the proper arguments to include “fuse” library. To mount the filesystem after compilation:

./lsysfs -f [mount point]

Using the option “-f” will let you see the debug messages which are printed using “printf”.

Source Code

You can find the source code of SSFS in my GitHub account here: https://github.com/MaaSTaaR/LSYSFS.

Demo

user

Mohammed Q. Hussain

http://www.maastaar.net

A Programmer.