wandres.dev
ESCRIBIR UN FILESYSTEM · libfs, un FS en memoria

Registrar y montar: file_system_type y la fábrica de superbloques

Un sistema de archivos se anuncia al núcleo con `register_filesystem` sobre un `struct file_system_type`, y cada montaje fabrica un superbloque a través de su callback `.mount`, que delega en `mount_nodev` o `mount_bdev` según haya o no dispositivo de respaldo, y termina llamando a tu `fill_super`. Aquí desmontamos ese contrato pieza a pieza.

⏱ 17 min

Rellenar las cuatro tablas del nivel anterior no basta: el núcleo aún no sabe que tu sistema de archivos existe. Hacen falta dos actos. Primero, registrarlo: publicar un struct file_system_type en una lista global para que mount -t lo encuentre por su nombre. Segundo, montarlo: cada vez que alguien pide montar ese tipo, el núcleo llama a tu callback .mount, que debe fabricar un superbloque, poblarlo con fill_super y devolver el dentry de su raíz. Este nivel diseca ese doble contrato entre el tipo, que es único y registrado, y sus instancias, que son tantas como montajes.

🎯 Al terminar esta lección sabrás
  • Registrar y retirar un sistema de archivos con register_filesystem y unregister_filesystem.
  • Rellenar un struct file_system_type con .mount, .kill_sb, .fs_flags y .owner.
  • Elegir entre mount_nodev, mount_bdev y mount_single según el respaldo.
  • Entender el papel de fill_super, de d_make_root y del kill_sb correcto.

Registrar el tipo

El registro es una sola llamada, casi siempre en el module_init. register_filesystem inserta tu struct file_system_type en una lista enlazada global que el núcleo consulta cuando la orden mount trae un -t nombre. Retirarlo en el module_exit es simétrico, y falla con -EBUSY si aún hay algún superbloque vivo:

#include <linux/module.h>
#include <linux/fs.h>

#define MYFS_MAGIC 0x6d796673   /* 'myfs' en ASCII */

static int myfs_fill_super(struct super_block *sb, void *data, int silent);

static struct dentry *myfs_mount(struct file_system_type *fs_type,
				 int flags, const char *dev_name, void *data)
{
	/* sin dispositivo de respaldo: superbloque anonimo, en RAM */
	return mount_nodev(fs_type, flags, data, myfs_fill_super);
}

static struct file_system_type myfs_type = {
	.owner    = THIS_MODULE,
	.name     = "myfs",           /* el nombre para mount -t myfs */
	.mount    = myfs_mount,       /* la fabrica de superbloques */
	.kill_sb  = kill_litter_super,/* como destruir uno al desmontar */
	.fs_flags = FS_USERNS_MOUNT,  /* montable dentro de un user namespace */
};
MODULE_ALIAS_FS("myfs");

static int __init myfs_init(void)
{
	return register_filesystem(&myfs_type);
}
static void __exit myfs_exit(void)
{
	unregister_filesystem(&myfs_type);
}
module_init(myfs_init);
module_exit(myfs_exit);
MODULE_LICENSE("GPL");

El campo .owner igual a THIS_MODULE es el que impide que el módulo se descargue mientras haya un montaje activo: el conteo de referencias del módulo se incrementa por cada superbloque vivo.

La función mount: elegir la fábrica

El callback .mount no construye el superbloque a mano; delega en una de las fábricas que la infraestructura del VFS ofrece, y cada una encapsula de dónde sale el almacenamiento. La diferencia entre un sistema de archivos en memoria y uno en disco cabe en qué helper eliges:

/* sin respaldo: cada montaje crea un superbloque nuevo (ramfs, tmpfs, sysfs) */
struct dentry *mount_nodev(struct file_system_type *fs_type, int flags,
			   void *data,
			   int (*fill_super)(struct super_block *, void *, int));

/* respaldo en un dispositivo de bloques (ext4, xfs, btrfs) */
struct dentry *mount_bdev(struct file_system_type *fs_type, int flags,
			  const char *dev_name, void *data,
			  int (*fill_super)(struct super_block *, void *, int));

/* un unico superbloque compartido por todos los montajes de ese tipo */
struct dentry *mount_single(struct file_system_type *fs_type, int flags,
			    void *data,
			    int (*fill_super)(struct super_block *, void *, int));

mount_nodev inventa un número de dispositivo anónimo y crea un superbloque flamante en cada montaje: es lo que usa un sistema de archivos en RAM. mount_bdev abre el dispositivo de bloques nombrado en dev_name, lee su superbloque físico y, si ya estaba montado, reutiliza la misma instancia. mount_single mantiene un solo superbloque para todos los montajes, útil en pseudo-sistemas de estado global. Las tres convergen en lo mismo: internamente llaman a sget, que localiza o crea el struct super_block, y después invocan tu fill_super sobre él.

fill_super: poblar el superbloque y crear la raíz

fill_super es donde tu sistema de archivos toma un superbloque recién nacido y lo deja utilizable: fija el tamaño de bloque, el número mágico, la tabla s_op, el techo de tamaño de archivo, y —lo esencial— crea el inodo raíz y lo envuelve en el dentry raíz con d_make_root:

static int myfs_fill_super(struct super_block *sb, void *data, int silent)
{
	struct inode *root;

	sb->s_magic     = MYFS_MAGIC;
	sb->s_op        = &myfs_super_ops;
	sb->s_blocksize = PAGE_SIZE;
	sb->s_blocksize_bits = PAGE_SHIFT;
	sb->s_maxbytes  = MAX_LFS_FILESIZE;
	sb->s_time_gran = 1;

	root = myfs_make_inode(sb, NULL, S_IFDIR | 0755);  /* nivel 38.4 */
	if (!root)
		return -ENOMEM;

	sb->s_root = d_make_root(root);   /* consume el inodo; lo libera si falla */
	if (!sb->s_root)
		return -ENOMEM;              /* no hay iput: d_make_root ya lo hizo */

	return 0;
}

d_make_root es el idioma moderno que sustituyó al viejo d_alloc_root: recibe el inodo raíz, le crea un dentry sin nombre y, si algo falla, hace el iput por ti. Devolver el dentry raíz desde .mount es literalmente lo que el VFS injerta en el punto de montaje.

flowchart TD
U[Proceso llama a mount] --> SYS[Capa de syscalls del VFS]
SYS --> FT[fs_type punto mount]
FT --> MN[mount_nodev]
MN --> SGET[sget localiza o crea el super_block]
SGET --> FS[fill_super del sistema de archivos]
FS --> INODO[Crea el inodo raiz]
INODO --> ROOT[d_make_root crea el dentry raiz]
ROOT --> DEN[Devuelve el dentry raiz]
DEN --> GRAFT[El VFS injerta el dentry en el mountpoint]

kill_sb: cómo destruir el superbloque

El campo .kill_sb es el reverso de .mount: se ejecuta al desmontar, y debe deshacer exactamente lo que la fábrica montó. La elección va emparejada con el helper de montaje. Para mount_bdev corresponde kill_block_super, que sincroniza y cierra el dispositivo. Para mount_nodev hay dos opciones: kill_anon_super, que solo suelta el dispositivo anónimo, y kill_litter_super, que además recorre y libera todo el árbol de dentries que se creó en RAM. Un sistema de archivos en memoria que dejó al usuario crear directorios y archivos debe usar kill_litter_super, o esos dentries quedarían colgando.

ℹ️
La API moderna: fs_context e init_fs_context

El callback .mount es la API clásica, plenamente soportada y la más clara para aprender. Pero desde hace varios ciclos el árbol migra a la API de contexto de montaje: en lugar de .mount rellenas .init_fs_context, que prepara un struct fs_context con sus operaciones, y tu get_tree llama a get_tree_nodev(fc, myfs_fill_super) o get_tree_bdev(fc, myfs_fill_super). Esa API separa el parseo de opciones de la construcción del superbloque y permite remonta jes más limpios. ramfs y tmpfs ya la usan; mount_nodev y get_tree_nodev son dos caras del mismo mecanismo.

El file_system_type es la clase; el superbloque es la instancia

Detente en la asimetría que estructura todo este nivel, porque es la misma distinción entre clase y objeto que reconociste en el address_space, ahora aplicada al volumen entero. El struct file_system_type es uno: se registra una vez, vive en una lista global del núcleo y no representa ningún sistema de archivos concreto, sino la capacidad de fabricarlos; es, en términos de orientación a objetos, la clase, con su nombre y su método constructor. El struct super_block, en cambio, es la instancia: hay uno por cada montaje, cada uno con su propio inodo raíz, su propio conjunto de inodos cacheados y su propio estado, y todos comparten el código de las tablas de operaciones pero no los datos. El callback .mount es, con precisión, un método de fábrica: recibe el tipo y las opciones y devuelve el objeto raíz recién construido, un dentry que el VFS injerta en el árbol de nombres global. Y sget es el detalle que revela la sofisticación del diseño: no siempre fabrica: para un dispositivo de bloques ya montado, devuelve el superbloque existente, porque montar dos veces el mismo disco debe dar la misma instancia, mientras que para un sistema en RAM fabrica siempre uno nuevo, porque dos mount -t tmpfs son dos volúmenes independientes. Esa política —cuándo compartir instancia y cuándo crearla— es toda la diferencia semántica entre un sistema de archivos con estado en disco y uno efímero, y el núcleo la expresa eligiendo qué fábrica invocas. Registrar es declarar una clase; montar es instanciarla; desmontar es destruir la instancia sin tocar la clase. Cuando ves el subsistema con esas tres palabras, el resto es detalle.

⚔️ Registra y monta un tipo propio
  1. Escribe el module_init y el module_exit mínimos con register_filesystem y unregister_filesystem, y explica por qué el segundo puede devolver -EBUSY.
  2. Justifica por qué un sistema de archivos en RAM usa mount_nodev y uno en disco usa mount_bdev, en términos de qué hace sget en cada caso.
  3. Describe qué ocurre paso a paso desde que .mount llama a mount_nodev hasta que se devuelve el dentry raíz.
  4. Explica la diferencia entre kill_anon_super y kill_litter_super y en qué caso elegir mal fuga memoria.
  5. Reescribe el .mount clásico como la pareja .init_fs_context más get_tree_nodev y señala qué responsabilidad cambia de sitio.