Callbacks and struct fuse_operations

A FUSE filesystem is a program that listens on a socket for file operations to perform, and performs them. The FUSE library (libfuse) provides the communication with the socket, and passes the requests on to your code. It accomplishes this by using a "callback" mechanism.

The callbacks are a set of functions you write to implement the file operations, and a struct fuse_operations containing pointers to them.

In the case of BBFS, the callback struct is named bb_oper. There are a total of 34 file operations defined in bbfs.c with pointers in bb_oper. The initialization uses a syntax that not everyone is familiar with; looking at a part of the initialization of the struct we see

struct fuse_operations bb_oper = {
  .getattr = bb_getattr,
  .readlink = bb_readlink,
  .open = bb_open,
  .read = bb_read
};
(this isn't the complete struct, of course — just enough to get a sense of what's going on)

This indicates that bb_oper.getattr points to bb_getattr(), bb_oper.readlink points to bb_readlink(), bb_oper.open points to bb_open(), and bb_oper.read points to bb_read(). Each of these functions is my re-implementation of the corresponding filesystem function: when a user program calls read(), my bb_read() function ends up getting called. In general, what all of my reimplementations do is to log some information about the call, and then call the original system implementation of the operation on the underlyng filesystem.

Let's take a look at two of these in particular: bb_open() (my re-implementation of open(), and bb_read() (my re-implementation of read().

Here's bb_open():

int bb_open(const char *path, struct fuse_file_info *fi)
{
    int retstat = 0;
    int fd;
    char fpath[PATH_MAX];

    bb_fullpath(fpath, path);
    
    log_msg("bb_open(fpath\"%s\", fi=0x%08x)\n",
	    fpath,  (int) fi);
    
    fd = open(fpath, fi->flags);
    if (fd < 0)
	retstat = bb_error("bb_open open");
    
    fi->fh = fd;
    log_fi(fi);
    
    return retstat;
}

When the function is called, it is passed two parameters: a file path (which is relative to the root of the mounted file system), and a pointer to a struct fuse_file_info which is used to maintain information about the file.

bb_open() starts by translating the relative path it was given to a full path in the underlying filesystem using my bb_fullpath() function. It then logs the full path, and the address of the fi pointer. It passes the call on down to the underlying fileystem, and sees if it was successful. If it was, it stores away the file descriptor returned by open() (so I'll be able to use it later), and returns 0. If it failed, it returns -errno. About the return value:

  1. 0 should be returned on success. This is the normal behavior for most of the calls in the libraries; exceptions are documented.
  2. A negative return value denotes failure. If I return a value of -i, a -1 will be returned to the caller and errno is set to i. My bb_error() function looks up errno as set by the system open() call, logs the error, and returns -errno to this function so I can pass it to the user.

Notice that FUSE performs some translations. The open() system call is documented as returning a file descriptor (behavior I'm depending on), not 0 — so when my return is passed to the original caller, FUSE recognizes that I sent a 0 and returns an appropriate file descriptor (not necessarily the same one I got from my call to open()!). Meanwhile, I've got the underlying file open, and I've got its file descriptor in fi. Future calls to my code will include this pointer, so I'll be able to get the file descriptor and work with it. So... the user program has an open file in the mounted filesystem, and a file descriptor that it is keeping track of. Whenever that program tries to do anything with that file descriptor, the operation is intercepted by the kernel and sent to the bbfs program. Within my program, I also have a file open in the underlying directory, and a file descriptor. When the operation is sent to my program, I'll log it and then perform the same operation on my file.

To make this concrete, let's take a look at bb_read():

int bb_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi)
{
    int retstat = 0;
    
    log_msg("bb_read(path=\"%s\", buf=0x%08x, size=%d, offset=%lld, fi=0x%08x)\n",
	    path,  (int) buf, size,  offset,  (int) fi);
    
    retstat = pread(fi->fh, buf, size, offset);
    if (retstat < 0)
	retstat = bb_error("bb_read read");
    
    return retstat;
}

This function allows us to read data from some specified offset from the beginning of a file (so it corresponds more directly to the pread() function than to read()).

The main thing to point out about this function is that I use my file descriptor, which I put in fi when I opened the file, to read it. Also, if I get a non-error return from pread(), I pass this value up to the caller. In this case FUSE doesn't perform any translations, it just returns the value I gave it. To return an error, I use the same technique as in bb_open().


Next: Parsing the Command Line and Initializing FUSE


Last modified: Thu Jun 12 18:12:31 MDT 2014