libfs: la biblioteca para sistemas de archivos en memoria
libfs es el arsenal de operaciones VFS ya escritas que el núcleo ofrece a los sistemas de archivos en memoria: la familia `simple_*` implementa el espacio de nombres manipulando solo el dcache, el contenido de los archivos vive en el page cache vía `simple_read_folio` y `simple_write_begin`, y `simple_fill_super` levanta un árbol entero de un tirón. ramfs y tmpfs son poco más que su pegamento.
Para toda una clase de sistemas de archivos —los que viven en RAM y los pseudo-sistemas como sysfs o debugfs— resulta que las cachés que el VFS ya mantiene son el almacenamiento: el dcache guarda la estructura de directorios y el page cache guarda el contenido de los archivos. libfs es la biblioteca que explota esa coincidencia. Su familia de funciones simple_* implementa cada verbo del VFS sin tocar ningún disco, porque no hay disco: crear un archivo es instanciar un dentry, borrarlo es bajar un conteo de enlaces, leerlo es devolver un folio del cache. Escribir un sistema de archivos en memoria con libfs es, en su mayor parte, elegir qué helpers enchufar.
- Entender por qué en un sistema en memoria el dcache y el page cache son el almacenamiento.
- Conocer las familias de helpers
simple_*: espacio de nombres, recorrido, contenido y superbloque. - Construir un árbol estático de archivos con
simple_fill_superystruct tree_descr. - Ver cómo ramfs se reduce a enchufar estos helpers a las cuatro tablas.
El dcache es el almacenamiento
Aquí está el cambio de mentalidad. En un sistema de archivos en disco, unlink debe marcar bloques como libres, actualizar mapas de asignación y programar un writeback. En uno en memoria no hay nada de eso: el archivo es su inodo en RAM y su nombre es un dentry en el dcache. Por eso simple_unlink cabe en cuatro líneas —baja el conteo de enlaces y suelta el dentry— y el inodo se libera solo cuando su última referencia cae, gracias a generic_delete_inode:
/* fs/libfs.c (recortado) */
int simple_unlink(struct inode *dir, struct dentry *dentry)
{
struct inode *inode = d_inode(dentry);
inode_set_ctime_current(inode);
inode_set_mtime_to_ts(dir, inode_set_ctime_current(dir));
drop_nlink(inode); /* un enlace menos: al llegar a 0, el inodo muere */
dput(dentry); /* suelta la referencia del dentry del padre */
return 0;
}
simple_link es el reflejo: sube nlink, toma una referencia con ihold e instancia el dentry con d_instantiate. Y simple_lookup es aún más revelador: como todo lo que existe ya está en el dcache, un fallo de búsqueda significa que el archivo no existe, así que instancia un dentry negativo y devuelve NULL. No hay a dónde ir a mirar: el cache es la fuente de la verdad.
Las familias de helpers
libfs agrupa sus funciones por la tabla que rellenan. Estas son las que usarás una y otra vez:
/* espacio de nombres (inode_operations de un directorio) */
struct dentry *simple_lookup(struct inode *, struct dentry *, unsigned int);
int simple_link(struct dentry *, struct inode *, struct dentry *);
int simple_unlink(struct inode *, struct dentry *);
int simple_rmdir(struct inode *, struct dentry *);
int simple_rename(struct mnt_idmap *, struct inode *, struct dentry *,
struct inode *, struct dentry *, unsigned int);
/* recorrer un directorio (file_operations), ya empaquetadas: */
extern const struct file_operations simple_dir_operations;
extern const struct inode_operations simple_dir_inode_operations;
/* contenido del archivo servido desde el page cache (address_space_operations) */
int simple_read_folio(struct file *, struct folio *);
int simple_write_begin(struct file *, struct address_space *, loff_t pos,
unsigned len, struct folio **, void **fsdata);
int simple_write_end(struct file *, struct address_space *, loff_t pos,
unsigned len, unsigned copied, struct folio *, void *fsdata);
/* superbloque y ciclo de vida (super_operations) */
int simple_statfs(struct dentry *, struct kstatfs *);
int generic_delete_inode(struct inode *); /* borra al llegar a 0 referencias */
/* no-operaciones para huecos que no aplican */
int noop_fsync(struct file *, loff_t, loff_t, int);
bool noop_dirty_folio(struct address_space *, struct folio *);
simple_dir_operations merece una mirada: recorre el directorio leyendo directamente los dentries hijos del dcache, sin más fuente de datos:
/* fs/libfs.c */
const struct file_operations simple_dir_operations = {
.open = dcache_dir_open,
.release = dcache_dir_close,
.llseek = dcache_dir_lseek,
.read = generic_read_dir,
.iterate_shared = dcache_readdir, /* readdir leyendo el dcache */
.fsync = noop_fsync,
};
Espacio de nombres
simple_lookup, simple_link, simple_unlink, simple_rmdir, simple_rename: crean y borran entradas moviendo solo dentries y contadores nlink.
Recorrer directorios
simple_dir_operations y dcache_readdir: enumeran los hijos leyéndolos del dcache, sin bloques ni índices en disco.
Contenido en page cache
simple_read_folio, simple_write_begin, simple_write_end: un archivo cuyo contenido vive entero en folios del page cache.
Construcción
simple_fill_super con struct tree_descr levanta un árbol estático; d_make_root crea la raíz.
Un árbol entero de un tirón: simple_fill_super
Cuando tu sistema de archivos expone un conjunto fijo de archivos —el caso típico de un pseudo-sistema de control—, no hace falta escribir fill_super a mano. simple_fill_super recibe un vector de struct tree_descr y construye la raíz y cada archivo con sus file_operations:
struct tree_descr {
const char *name;
const struct file_operations *ops;
int mode;
};
/* la entrada 0 esta reservada; un name vacio termina el vector */
static const struct tree_descr myfs_files[] = {
[1] = { "control", &myfs_control_fops, 0644 },
[2] = { "status", &myfs_status_fops, 0444 },
{ "", NULL, 0 }
};
static int myfs_fill_super(struct super_block *sb, struct fs_context *fc)
{
return simple_fill_super(sb, MYFS_MAGIC, myfs_files);
}
ramfs, desnudo
La demostración de la potencia de libfs es que ramfs —un sistema de archivos en RAM plenamente funcional, con directorios, archivos, enlaces y mmap— apenas escribe código propio. Sus cuatro tablas son casi enteramente helpers:
static const struct super_operations ramfs_ops = {
.statfs = simple_statfs,
.drop_inode = generic_delete_inode,
.show_options = ramfs_show_options,
};
static const struct inode_operations ramfs_dir_inode_operations = {
.create = ramfs_create, /* fino wrapper sobre ramfs_mknod */
.lookup = simple_lookup,
.link = simple_link,
.unlink = simple_unlink,
.mkdir = ramfs_mkdir,
.rmdir = simple_rmdir,
.mknod = ramfs_mknod,
.rename = simple_rename,
};
const struct address_space_operations ram_aops = {
.read_folio = simple_read_folio,
.write_begin = simple_write_begin,
.write_end = simple_write_end,
.dirty_folio = noop_dirty_folio,
};
Solo ramfs_mknod, ramfs_create y ramfs_mkdir son código de ramfs, y los tres son envoltorios de tres líneas alrededor de un ramfs_get_inode que fabrica el inodo y lo instancia en el dentry. Todo lo demás —el recorrido de rutas, la lectura, la escritura, el borrado, el renombrado— es libfs.
ramfs no tiene límite ni respaldo: llena la RAM hasta reventar. tmpfs (shmem) parte del mismo esqueleto libfs pero añade contabilidad de páginas, un límite de tamaño y, sobre todo, la capacidad de mandar sus folios a swap bajo presión de memoria. Por eso /tmp y /dev/shm son tmpfs y no ramfs: quieres un sistema en RAM que ceda memoria cuando el sistema aprieta.
Reflexiona sobre lo que libfs revela del diseño del núcleo, porque es más profundo que un ahorro de código. El VFS, para acelerar los sistemas de archivos en disco, mantiene dos cachés enormes: el dcache, que memoriza la estructura de nombres para no recorrer el disco en cada open, y el page cache, que memoriza el contenido de los archivos para no releerlo. Ambas cachés son, por necesidad, implementaciones completas de una jerarquía de directorios y de un almacén de contenido en memoria: saben crear entradas, buscarlas, enumerarlas, guardar bytes por offset, servirlos, invalidarlos. La observación que funda libfs es que si tu sistema de archivos vive precisamente en RAM, entonces esas cachés no son una copia acelerada de un original en disco: son el original. No hay nada detrás que respaldar. Y en ese instante todo el trabajo de un sistema de archivos se evapora: lookup no busca en ningún sitio porque si no está en el dcache no existe; unlink no libera bloques porque el archivo era su inodo en RAM; read no lee de ningún dispositivo porque el folio del page cache ya contiene el dato. ramfs, dicho sin metáfora, es el page cache con un punto de montaje. libfs no es una comodidad para principiantes: es la constatación de que el núcleo ya llevaba dentro un sistema de archivos en memoria, oculto en su maquinaria de caché, y de que basta con darle un nombre y registrarlo para sacarlo a la luz. Cuando internalizas esto, entiendes por qué debugfs, sysfs, configfs, tracefs y una docena más se construyen sobre libfs: todos son vistas en memoria de estado del núcleo, y todos descubren que la caché que existía para acelerar el disco es, por sí sola, su implementación entera.
- Explica, línea por línea, por qué
simple_unlinkno necesita liberar ningún bloque y cuándo muere realmente el inodo. - Argumenta por qué
simple_lookuppuede afirmar que un archivo no existe con solo mirar el dcache. - Construye un
struct tree_descrcon dos archivos y describe qué hacesimple_fill_supercon la entrada índice 0 y con la denamevacío. - Recorre las cuatro tablas de ramfs y marca qué campos son helpers de libfs y cuáles son código propio de ramfs, y por qué esos pocos no podían ser genéricos.
- Explica la diferencia funcional entre ramfs y tmpfs y qué operación de gestión de memoria añade el segundo.