Introduction
File systems are the backbone of any operating system, responsible for managing how data is stored and retrieved. Traditionally, developing a file system has been a complex and daunting task, requiring deep knowledge of kernel programming. However, with FUSE (Filesystem in Userspace), this task becomes significantly more accessible and versatile. In this blog post, we will explore what FUSE is, how it works, and why it is a game-changer for Linux users and developers alike. We’ll be developing a simple filesystem that supports creating, reading, writing files and listing files in a directory.
About FUSE
FUSE (Filesystem in USErspace) is a software layer in Linux that allows non-privileged users to create their own file-systems without editing the kernel source code. It is made up of three main components:
fuse.ko
- The FUSE kernel module, which provides the interface for FUSE.libfuse
- A userspace library which provides the necessary API for handling communication with the FUSE kernel module, allowing userspace applications to implement custom filesystem logic.fusermount
- A mount utility.
libfuse
provides two types of APIs: a high-level
synchronous API and a low-level
asynchronous API. Both APIs handle incoming requests from the kernel by using callbacks to pass these requests to the main program. When using the high-level API, callbacks handle file names and paths, and the request is completed when the callback function returns. In contrast, with the low-level API, callbacks work with inodes, and you must explicitly send responses using a separate set of API functions. In this post, we’ll be using the low-level API for building our filesystem. The high-level API will be taken care of in upcoming posts.
Before using the low-level API, we must know about some important structures and macros.
Some important structures, functions and macros
fuse_args
This structure is used to handle command-line arguments passed to a FUSE filesystem.
1 | struct fuse_args { |
FUSE_ARGS_INIT
Initializes a struct fuse_args with argc and argv, andallocated
set to 0.
1 |
fuse_cmdline_opts
A structure used to store command-line options parsed from the arguments. This structure helps manage and configure the FUSE filesystem based on user inputs.
1 | struct fuse_cmdline_opts { |
This structure can be populated using the fuse_parse_cmdline
function.
1 | struct fuse_cmdline_opts opts; |
fuse_session_new
Creates a new low-level session. This function accepts most file-system independent mount options.1
2struct fuse_session *fuse_session_new(struct fuse_args *args,const struct fuse_lowlevel_ops *op,
size_t op_size, void *userdata);fuse_set_signal_handlers
This function installs signal handlers for the signalsSIGHUP
,SIGINT
, andSIGTERM
that will attempt to unmount the file system. If there is already a signal handler installed for any of these signals then it is not replaced. This function returns zero on success and -1 on failure.
1 | int fuse_set_signal_handlers(struct fuse_session *se); |
fuse_lowlevel_ops
This structure represents the low-level filesystem operations1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180struct fuse_lowlevel_ops {
// Called when libfuse establishes communication with the FUSE kernel module.
void (*init) (void *userdata, struct fuse_conn_info *conn);
// Cleans up filesystem, called on filesystem exit.
void (*destroy) (void *userdata);
// Look up a directory entry by name and get its attributes.
void (*lookup) (fuse_req_t req, fuse_ino_t parent, const char *name);
// Can be called to forget about an inode
void (*forget) (fuse_req_t req, fuse_ino_t ino, uint64_t nlookup);
// Called to get file attributes
void (*getattr) (fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi);
// Called to set file attributes
void (*setattr) (fuse_req_t req, fuse_ino_t ino, struct stat *attr,
int to_set, struct fuse_file_info *fi);
// Called to read the target of a symbolic link
void (*readlink) (fuse_req_t req, fuse_ino_t ino);
// Called to create a file node
void (*mknod) (fuse_req_t req, fuse_ino_t parent, const char *name,
mode_t mode, dev_t rdev);
// Called to create a directory
void (*mkdir) (fuse_req_t req, fuse_ino_t parent, const char *name,
mode_t mode);
// Called to remove a file
void (*unlink) (fuse_req_t req, fuse_ino_t parent, const char *name);
// Called to remove a directory
void (*rmdir) (fuse_req_t req, fuse_ino_t parent, const char *name);
// Called to create a symbolic link
void (*symlink) (fuse_req_t req, const char *link, fuse_ino_t parent,
const char *name);
// Called to rename a file or directory
void (*rename) (fuse_req_t req, fuse_ino_t parent, const char *name,
fuse_ino_t newparent, const char *newname,
unsigned int flags);
// Called to create a hard link
void (*link) (fuse_req_t req, fuse_ino_t ino, fuse_ino_t newparent,
const char *newname);
// Called to open a file
void (*open) (fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi);
// Called to read data from a file
void (*read) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info *fi);
// Called to write data to a file
void (*write) (fuse_req_t req, fuse_ino_t ino, const char *buf,
size_t size, off_t off, struct fuse_file_info *fi);
// Called on each close() of the opened file, for flushing cached data
void (*flush) (fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi);
// Called to release an open file (when there are no more references to an open file i.e all file descriptors are closed and all memory mappings are unmapped)
void (*release) (fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi);
// Called to synchronize file contents
void (*fsync) (fuse_req_t req, fuse_ino_t ino, int datasync,
struct fuse_file_info *fi);
// Called to open a directory
void (*opendir) (fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi);
// Called to read directory entries
void (*readdir) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info *fi);
// Called to release an open directory
void (*releasedir) (fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi);
// Called to synchronize directory contents
void (*fsyncdir) (fuse_req_t req, fuse_ino_t ino, int datasync,
struct fuse_file_info *fi);
// Called to get file system statistics
void (*statfs) (fuse_req_t req, fuse_ino_t ino);
// Called to set an extended attribute
void (*setxattr) (fuse_req_t req, fuse_ino_t ino, const char *name,
const char *value, size_t size, int flags);
// Called to get an extended attribute
void (*getxattr) (fuse_req_t req, fuse_ino_t ino, const char *name,
size_t size);
// Called to list extended attribute names
void (*listxattr) (fuse_req_t req, fuse_ino_t ino, size_t size);
// Called to remove an extended attribute
void (*removexattr) (fuse_req_t req, fuse_ino_t ino, const char *name);
// Called to check file-access permissions
void (*access) (fuse_req_t req, fuse_ino_t ino, int mask);
// Called to create and open a file
void (*create) (fuse_req_t req, fuse_ino_t parent, const char *name,
mode_t mode, struct fuse_file_info *fi);
// Called to get a file lock
void (*getlk) (fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi, struct flock *lock);
// Called to set a file lock
void (*setlk) (fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi,
struct flock *lock, int sleep);
// Called to map a block index within file to a block index within device
void (*bmap) (fuse_req_t req, fuse_ino_t ino, size_t blocksize,
uint64_t idx);
// The ioctl handler
void (*ioctl) (fuse_req_t req, fuse_ino_t ino, int cmd,
void *arg, struct fuse_file_info *fi, unsigned flags,
const void *in_buf, size_t in_bufsz, size_t out_bufsz);
void (*ioctl) (fuse_req_t req, fuse_ino_t ino, unsigned int cmd,
void *arg, struct fuse_file_info *fi, unsigned flags,
const void *in_buf, size_t in_bufsz, size_t out_bufsz);
// Called to poll a file for I/O readiness.
void (*poll) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi,
struct fuse_pollhandle *ph);
// Called to write a buffer to a file.
void (*write_buf) (fuse_req_t req, fuse_ino_t ino,
struct fuse_bufvec *bufv, off_t off,
struct fuse_file_info *fi);
// Called to reply to a retrieve operation.
void (*retrieve_reply) (fuse_req_t req, void *cookie, fuse_ino_t ino,
off_t offset, struct fuse_bufvec *bufv);
// Called to forget multiple inodes
void (*forget_multi) (fuse_req_t req, size_t count,
struct fuse_forget_data *forgets);
// Called to acquire, modify or release a file lock
void (*flock) (fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi, int op);
// Called to allocate space to a file
void (*fallocate) (fuse_req_t req, fuse_ino_t ino, int mode,
off_t offset, off_t length, struct fuse_file_info *fi);
// Called to read a directory entry with attributes
void (*readdirplus) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info *fi);
// To copy a range of data from one file to another
void (*copy_file_range) (fuse_req_t req, fuse_ino_t ino_in,
off_t off_in, struct fuse_file_info *fi_in,
fuse_ino_t ino_out, off_t off_out,
struct fuse_file_info *fi_out, size_t len,
int flags);
// The lseek operation, for specifying new file offsets past the current end of the file.
void (*lseek) (fuse_req_t req, fuse_ino_t ino, off_t off, int whence,
struct fuse_file_info *fi);
};fuse_session_loop
Enter a single threaded, blocking event loop. The loop can be terminated through signals if signal handlers have been pre-registered.
1 | int fuse_session_loop(struct fuse_session *se); |
fuse_session_unmount
This function ensures that the file system is unmounted.1
void fuse_session_unmount(struct fuse_session *se);
fuse_reply_*
These types of functions (for example, fuse_reply_entry, fuse_reply_open, etc.) are used to send responses back to the FUSE kernel module from the user space filesystem implementation. Eachfuse_reply_*
type of function corresponds to a specific type of response that can be sent, depending on the operation being performed.
Some concepts related to filesystems
Inode
An inode (index node) is a data structure that stores essential information about a file.Inode Number
The inode number is a unique identifier for a file or directory within a filesystem on Unix-like operating systems. When a file is created, a name and an inode number is assigned to it.Link Count
The UNIX file system contains two entries in every directory:.
and..
. Thus, each directory has a link count of 2+n, where n is the number of subdirectories within that directory.Inode number of the root directory
In our custom file system, the inode number of the root directory will be 1. However, it is 2 in case of linux. A proper explanation for the same can be found here.
Using the low-level API
The low-level API is primarily documented over here. In this post, we’ll be using fuse3. In case you face any trouble with including the headers, install libfuse3-dev using sudo apt install libfuse3-dev
.
We need to create some handlers, according to the items present in the struct fuse_lowlevel_ops
, mentioned above.
Creating a file
: For creating a file, we need to add corresponding functions forlookup
,create
, andgetattr
. In order to create a file usingtouch
, we need to create a function forsetattr
as well.Reading a file
: For reading a file, we need to add corresponding functions forlookup
,open
, andread
Writing to a file
: For writing data to a file, we need to add corresponding functions forlookup
,open
, andwrite
.Listing files in a directory
: For listing files in a directory, the following sequence of events normally occur:getattr
->opendir
->readdir
->lookup
. So, we need to create corresponding handlers for all of them.
Here’s the source code for all of this. I’ve also pushed the code into this GitHub repo. In the next post, we’ll make our file system multithreaded and add a few more functionalities, such as changing file permissions using chmod, and deleting files.
1 | // Do not forget to add the macro FUSE_USE_VERSION |
Compile it using gcc fileName.c -o fileName -lfuse3
References: