Tutorial de FUSE

Por Ezequiel Aguerre - <ezeaguerre ARROBA gmail.com>

¿Qué es FUSE?

FUSE significa Filesystem in USErspace. Como todos sabemos el kernel de Linux tiene un componente muy importante que es el VFS, este se encarga de administrar todos los sistemas de archivos montados que tengamos. Por ej., ejecutamos el siguiente comando:

mount -t ext3 /dev/hdb2 /mnt/unidad
el VFS se encarga de decirle al módulo que maneja el sistema ext3 que monte esa unidad, entonces ahora el VFS se encarga de presentarle al usuario la nueva estructura del sistema de archivos, ¿cómo hace esto? cada acceso que se realiza sobre /mnt/unidad lo redirige al módulo controlador del sistema ext3. FUSE funciona exactamente de la misma manera, hay un módulo para el kernel que se registra como un manejador de un sistema de archivos cualquiera. Entonces cuando queremos acceder a una unidad FUSE montada el VFS le redirige la llamada al módulo de FUSE, y FUSE nos redirige la llamada a nuestro programa controlador, es decir, es nada más que una capa entre el kernel y nuestro programa en espacio de usuario. Todo esto nos permite tener nuestro propio sistema de archivos corriendo en espacio de usuario, además la interfaz ( o API ) de FUSE es mucho más sencilla que la del kernel. Podríamos tener por ejemplo un programa que utilice FUSE para mostrar el contenido de un archivo TAR como un directorio más. Por ejemplo GmailFS es un programa que permite usar nuestra cuenta en Gmail como un disco rígido más, y está implementado usando FUSE. También existe WikipediaFS y otros proyectos interesantes.

Usando FUSE

La API que presenta FUSE es en extremo sencilla, simplemente para realizar un sistema de archivos en espacio de usuario hay que incluir el archivo de encabezado fuse.h y enlazar con la librería libfuse. Y obviamente preparar todo :) para esto basta con declarar una estructura llamada fuse_operations que simplemente contiene varios punteros a funciones que serán llamados para cada operación. Luego se termina el programa con la función fuse_main. Por lo general las funciones deben devolver 0 en caso de éxito y un número negativo indicando el error en caso de falla. La excepción a esto son las llamadas write y read que deben devolver un nro. positivo representando la cantidad de bytes leídos, cero en caso de EOF o un número negativo en caso de error. Las verdaderas llamadas al sistema en linux devuelven -1 y establecen errno al valor adecuado, nosotros lo que debemos hacer es devolver un número negativo indicando el error, y FUSE se encarga de devolver al proceso llamante -1 y setear errno con el valor absoluto de nuestra función. Es decir, si nosotros devolvemos -ENOENT, FUSE se va a encargar de devolver -1 y setear errno en ENOENT. Los miembros básicos de la estructura fuse_operations son:

int (*getattr) (const char *, struct stat *);
int (*open) (const char *, struct fuse_file_info *);
int (*read) (const char *, char *, size_t, off_t, struct fuse_file_info *);
int (*readdir) (const char *, void *, fuse_fill_dir_t, off_t, struct fuse_file_info *);
  • getattr: Esta función es llamada cuando se quieren obtener los atributos de un archivo, de hecho, cuando se le hace un “stat” a un archivo se llama a esta función. El primer parámetro es de entrada e indica el path del archivo. El segundo parámetro es de salida y es la estructura stat a rellenar. Una devolución de cero indica éxito.
  • open: Se llama cuando se quiere abrir un archivo, el primer parámetro es el path del archivo, el segundo parámetro es una estructura que contiene información acerca de los flags de apertura, y además nos permite devolver un handler. Como esta estructura será pasada a nosotros en las funciones read, write y alguna otra, nosotros podemos utilizar el handler que le pasamos para saber de que archivo se habla, en todo caso no es un parámetro necesario para un sistema de archivos sencillo y lo podemos ignorar. Lo único que hay que hacer es comprobar si se puede abrir el archivo, en caso afirmativo devolvemos cero.
  • read: Se llama cuando se quiere leer un archivo. El primer parámetro como de costumbre es el path al archivo, el segundo parámetro es el buffer donde almacenar los datos, el tercer parámetro es la cantidad de bytes a leer, el cuarto el desplazamiento y el quinto es el mismo que el de open. Se devuelve la cantidad de bytes leídos.
  • readdir: Se utiliza para leer un directorio. El primer parámetro es el path del directorio, el segundo una estructura que hay que rellenar, el tercero es una función que usamos para rellenar la estructura del segundo parámetro y los otros dos los podemos ignorar.
También tenemos estos otros miembros que ya son un poquito más interesantes quizás:
int (*mknod) (const char *, mode_t, dev_t);
int (*unlink) (const char *);
int (*rename) (const char *, const char *);
int (*truncate) (const char *, off_t);
int (*write) (const char *, const char *, size_t, off_t, struct fuse_file_info *);
  • unlink: Borra un archivo, el único parámetro es el path.
  • rename: Renombra un archivo, el primer parámetro es el nombre viejo y el segundo el nuevo.
  • truncate: Trunca un archivo, le cambia el tamaño, ya sea a un tamaño menor o mayor. El primer parámetro es el path y el segundo es el nuevo tamaño.
  • write: Escribe un archivo, el primer parámetro es el path. El segundo el buffer que contiene los datos, el tercero la cantidad de bytes a escribir, el cuarto el offset en el cual escribir y el último es la estructura con información del archivo ( lo mismo que se le pasa a open y a read ).
  • mknod: Crea un nuevo nodo o archivo. No solamente sirve para crear archivos, también sirve para crear fifos, archivos de dispositivos y demás cosas que se puede hacer con una llamada mknod común y corriente. El primer parámetro es el path a crear, el segundo representan los permisos y el tipo de archivo, el tercero contiene el número mayor y menor en caso de que estemos creando un dispositivo.
Hay un punto a tener en cuenta, el path que nos pasa FUSE siempre es un path "absoluto" pero referenciado a nuestro sistema de archivos. Supongamos que tenemos nuestro sistema de archivso montado en /mnt/fuse y tenemos un archivo llamado fuse.txt, si alguien ejecuta la siguiente llamada al sistema:
int fd = open ( "/tmp/fuse/fuse.txt", O_RDONLY );
FUSE efectuará una llamada a open pasándonos como path la cadena "/fuse.txt". Aún quedan más miembros, les recomiendo revisar el archivo de cabecera fuse.h ya que está muy bien documentado.

UN EJEMPLO SENCILLO

Vamos a relizar un sencillo ejemplo, el programa lo único que hara será mostrar dos archivos, cada uno contendrá una sencillo texto. Lo primero que debemos hacer es incluir los archivos necesarios y declarar la estructura:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fuse.h>

struct fuse_operations operaciones;
Ahora empezaremos a definir nuestras funciones, como es un sistema de archivos tan sencillo simplemente rellenaremos los primeros miembros que documentamos más arriba. Pero antes vamos a declarar los nombres de los archivos y sus contenidos:
static const char *archivo1 = "/hola.txt";
static const char *archivo2 = "/chau.txt";

static const char *contenido1 = "Hola, ¿cómo te va?";
static const char *contenido2 = "Bien, lástima que no me pueda quedar a charlar.";
Ahora si, las funciones ( NOTA: Cuando declaro datos o funciones static es para que sean privadas, es decir, no exportarlos ): La primera de todas, readdir:
static int leer_directorio ( const char *path, void *buffer, fuse_fill_dir_t rellenar, off_t offset, struct fuse_file_info *info )
{
	/* En caso de que no estemos haciendo referencia al directorio principal devolvemos -ENOENT,
	   esto provocará que la lectura de directorio devuelva -1 y errno se establezca en ENOENT. */
	if ( strcmp ( path, "/" ) )
		return -ENOENT;

	/* Rellenamos el buffer conteniendo el listado de directorio, las entradas obligadas son . y ..
	   Como notarán a archivo1 y a archivo2 le sumamos 1, esto es para saltarse la primer / */
	rellenar ( buffer, ".", NULL, 0 );
	rellenar ( buffer, "..", NULL, 0 );
	rellenar ( buffer, archivo1 + 1, NULL, 0 );
	rellenar ( buffer, archivo2 + 1, NULL, 0 );
	
	return 0; /* Todo salió bien :) */
}

static int tomar_atributos ( const char *path, struct stat *info )
{
	memset ( info, 0, sizeof ( struct stat ) ); /* Primero la llenamos de cero */
	
	if ( ! strcmp ( path, "/" ) )
	{
		info->st_mode = S_IFDIR | 0555; /* Es un directorio, permisos de r-xr-xr-x ( 0555 ) */
		info->st_nlink = 2; /* Dos links */
	}

	else if ( ! strcmp ( path, archivo1 ) || ! strcmp ( path, archivo2 ) )
	{
		info->st_mode = S_IFREG | 0444; /* Es un archivo regular, permisos r--r--r-- ( 0444 ) */
		info->st_nlink = 1; /* Un solo enlace */
		if ( ! strcmp ( path, archivo1 ) )
			info->st_size = strlen ( contenido1 ); /* Tamaño del archivo 1 ... */
		else
			info->st_size = strlen ( contenido2 ); /* Tamaño del archivo 2 ... */
	}

	else
		return -ENOENT; /* Si no lo encontramos devolvemos ENOENT en errno y -1 a quien nos llamó */
	
	return 0; /* Todo OK */
}

static int abrir_archivo ( const char *path, struct fuse_file_info *info )
{
	/* Si es uno de los dos archivos lo abrimos */
	if ( ! strcmp ( path, archivo1 ) || ! strcmp ( path, archivo2 ) )
		return 0;

	return -ENOENT; /* Y si no no existe */
}

static int leer_archivo ( const char *path, char *buffer, size_t tamano, off_t offset, struct fuse_file_info *info )
{
	int len;
	const char *ptr;
	
	/* Nos fijamos si es un archivo válido */
	if ( ! strcmp ( path, archivo1 ) )
	{
		len = strlen ( contenido1 );
		ptr = contenido1;
	}	

	else if ( ! strcmp ( path, archivo2 ) )
	{
		len = strlen ( contenido2 );
		ptr = contenido2;
	}	

	else
		return -ENOENT; /* De lo contrario devolvemos ENOENT */
	
	/* Si estamos en un offset mayor no leemos ningún byte */
	if ( offset > len )
		return 0;

	/* Si nos queremos pasar no te dejo */	
	if ( offset + tamano > len )
		tamano = len - offset;
	
	strncpy ( buffer, ptr + offset, tamano );
	
	return tamano; /* Devuelo la cantidad de bytes leídos */	
}
Ahora necesitamos rellenar la estructura principal y ejecutar el fuse_main:
int main ( int argc, char *argv [] )
{
	operaciones.getattr = tomar_atributos;
	operaciones.open    = abrir_archivo;
	operaciones.readdir = leer_directorio;
	operaciones.read    = leer_archivo;
	
	return fuse_main ( argc, argv, &operaciones );
}
Antes de compilar nada deberemos agregar las dos siguientes líneas antes de la inclusión del archivo fuse.h
#define _FILE_OFFSET_BITS   64
#define FUSE_USE_VERSION    22
Las cuales especifican la versión de la API que estamos usando y otros datos, si no declaramos esto el programa no compilará, ya que la librería estaría en modo de compatibilidad con la API 2.1 o la 1.1, por eso elegimos 2.2 :) Para compilar el programa ejecutamos el siguiente comando:
gcc programa.c -o programa -lfuse
y para montarlo ( siendo root ) hacemos así:
./programa mnt/
ejemplo:
$ gcc programa.c -o programa -lfuse
$ mkdir mnt
$ ./programa mnt/
$ cd mnt
$ ls
$ cat hola.txt && echo
$ cat chau.txt && echo
$ cd ..
$ umount mnt/
$ rmdir mnt
En caso de que necesitemos depurar nuestro código podemos hacer esto:
$ ./programa -d mnt/
Esto le dice a FUSE que inicie en modo debug, por lo cual tenemos una terminal asociada y podemos imprimir mensajes, además FUSE se encarga de imprimir sus mensajes propios mostrando que llamada realizó y que se devolvió.

Referencias