[Traducido del artículo original de M.Tim Jones del 30 de octubre de 2007 para IBM]

Una revisión por capas basada en la estructura

Cuando se trata de sistemas de ficheros, Linux® es la navaja suiza de los sistemas operativos. Linux soporta un gran número de sistemas de ficheros, desde sistemas con diario (journaling) a clustering o criptográficos. Linux es una maravillosa plataforma para usar sistemas de ficheros estándar o los más exóticos, e incluso para el desarrollo de sistemas de ficheros. Este artículo explora el sistema de ficheros virtual (VFS por sus siglas en inglés) —algunas veces llamado el sistema de ficheros conmutador (switch) — en el kernel de Linux y después revisa algunas de las principales  estructuras que mantienen los sistemas de ficheros unidos.

Arquitectura básica de un sistema de ficheros

La aquitectura de sistema de ficheros de Linux es un ejemplo interesante de complejidad de abstracción. Usando un conjunto común de funciones de interfaz (API), una gran variedad de sistemas de ficheros pueden ser soportados con una gran variedad de dispositivos de almacenamiento. Tomemos, por ejemplo, la llamada a la función “leer” (read), que permite que cierto número de bytes sean leídos de un descriptor de fichero dado. La función “leer” no sabe nada de los tipos de sistemas de ficheros, tales como EXT3 o NFS. Tampoco sabe nada de medios de almacenamiento particulares sobre los que el sistema de ficheros está montado, tales como discos de tipo AT Attachment Packet Interface (ATAPI), discos Serial-Attached SCSI (SAS) o discos Serial Advanced Technology Attachment (SATA). Aun así, cuando la función “leer” es llamada para un fichero abierto, los datos son devueltos tal como se esperaba. Este artículo explora como se hace esto e investiga las principales estructuras de la capa de sistema de ficheros de Linux.

¿Qué es un sistema de ficheros?

Empezaré con una respuesta a la pregunta más básica, la definición de un sistema de ficheros. Un sistema de ficheros es una organización de datos y metadatos en un dispositivo de almacenamiento. Con una definición tan vaga como esa, ya se ve que el código requerido para soportarla será interesante. Como he dicho, hay muchos tipos de sistemas de ficheros y medios. Con todas estas variantes, cabe esperar que la interfaz de un sistema de ficheros Linux esté implementada como una arquitectura de capas, separando la capa de interfaz de usuario de la implementación del sistema de ficheros y de los controladores que manejan los dispositivos de almacenamiento.

Los sistemas de ficheros como protocolos

Otra manera de ver un sistema de ficheros es como un protocolo. Tal como los protocolos de red (p.ej., el protocolo IP) dar significado a las secuencias de datos atravesando la Internet, los sistemas de ficheros dan significado a los datos en un medio de almacenamiento particular.

Montaje

Asociar un dispositivo de almacenamiento a un sistema de ficheros en Linux es un proceso conocido como montaje (mounting). El comando “mount” es usado para adjuntar un sistema de ficheros a la actual jerarquía de sistema de ficheros (raíz, o root en inglés). Durante el montaje se debe indicar un tipo de sistema de ficheros, un dispositivo de almacenamiento que contenga un sistema de ficheros de ese tipo [Nota del traductor: en el texto original indica que debe ser un sistema de ficheros, pero eso no es exactamente así, por eso aquí está corregido] y un punto de montaje dentro de la jerarquía.

Para ilustrar las posibilidades de la capa de sistema de ficheros de Linux ( y el uso del comando mount ), se puede crear un sistema de ficheros en un fichero dentro del propio sistema de ficheros. Esto se consigue creando primero un fichero de un tamaño determinado usando el comando dd (copia un fichero usando /dev/zero como origen) — en otras palabras, un fichero inicializado con ceros, tal como se muestra en el Listado 1:

Listado 1. Creando un fichero inicializado a cero
$ dd if=/dev/zero of=file.img bs=1k count=10000
  10000+0 records in
  10000+0 records out
$

Ahora tenemos un fichero llamado “file.img” de 10MB de tamaño. A continuación, usaremos el comando “losetup” para asociar un dispositivo de bucle (loop) al fichero (para que funcione como un dispositivo de bloques en lugar de como un fichero normal dentro de un sistema de ficheros, es decir, lo convertimos en un dispositivo de almacenamiento):

$ losetup /dev/loop0 file.img
$

Con el fichero ahora como un dispositivo de bloques (representado por /dev/loop0), creamos un sistema de fichero en él con el comando mke2fs. Este comando crea un nuevo sistema de ficheros de tipo EXT2 del tamaño especificado, tal como se indica en el Listado 2:

Listado 2. Creando un sistema de ficheros EXT2 dentro de un dispositivo en bucle
 $ mke2fs -c /dev/loop0 10000
 mke2fs 1.35 (28-Feb-2004)
 max_blocks 1024000, rsv_groups = 1250, rsv_gdb = 39
 Filesystem label=
 OS type: Linux
 Block size=1024 (log=0)
 Fragment size=1024 (log=0)
 2512 inodes, 10000 blocks
 500 blocks (5.00%) reserved for the super user
 ...
 $

El fichero file.img, representado por el dispositivo de bucle (/dev/loop0), está ahora montado en el punto de montaje /mnt/point1 usando el comando mount. Destacar la especificación del sistema de ficheros como de tipo EXT2. Cuando esté montado, se puede tratar este nuevo punto de montaje como un nuevo sistema de ficheros usando el comando ls, tal como muestra el Listado 3:

Listado 3. Creando un punto de montaje y montando el sistema de ficheros a través de un dispositivo de bucle.
 $ mkdir /mnt/point1
 $ mount -t ext2 /dev/loop0 /mnt/point1
 $ ls /mnt/point1
 lost+found
 $

Como se muestra en el Listado 4, se puede continuar este proceso creando un nuevo fichero dentro del nuevo sistema de ficheros montado, asociando con un dispositivo de bucle, y creando otro sistema de fichero en él:

Listado 4. Creando un nuevo sistema de ficheros de bucle dentro de un sistema de ficheros de bucle.
 $ dd if=/dev/zero of=/mnt/point1/file.img bs=1k count=1000
 1000+0 records in
 1000+0 records out
 $ losetup /dev/loop1 /mnt/point1/file.img
 $ mke2fs -c /dev/loop1 1000
 mke2fs 1.35 (28-Feb-2004)
 max_blocks 1024000, rsv_groups = 125, rsv_gdb = 3
 Filesystem label=
 ...
 $ mkdir /mnt/point2
 $ mount -t ext2 /dev/loop1 /mnt/point2
 $ ls /mnt/point2
 lost+found
 $ ls /mnt/point1
 file.img lost+found
 $

A partir de esta simple demostración, es fácil ver lo poderoso que puede ser el sistema de ficheros de Linux (y el dispositivo de bucle). Puede usarse la misma aproximación para crear sistemas de ficheros encriptados con el dispositivo de bucle en un fichero. Esto es extremadamente útil para proteger tus datos mediante el montaje transitorio del fichero usando el dispositivo de bucle cuando sea necesario.

Arquitectura de sistema de ficheros

Ahora que hemos visto la construcción de un sistema de ficheros en acción, vuelvo a la arquitectura de la capa de sistema de ficheros de Linux. Este artículo muestra el sistema de ficheros de Linux desde dos perspectivas. La primera perspectiva es la de una arquitectura de alto nivel. La segunda excarva un poco más hondo y explora la capa de sistema de ficheros a partir de las estructuras principales que la implementan.

Arquitectura de alto nivel

Mientras que la mayoria del código de un sistema de ficheros existe en el nucleo (kernel en inglés) — excepto en el caso de los sistemas de ficheros del espacio de usuario, de los que hablaremos más tarde –, la arquitectura mostrada en la Figura 1 muestra las relaciones entre los principales componentes de un sistema de ficheros tanto en el espacio de usuario como en el núcleo.

Figura 1. Arquitectura de los componentes de un sistema de ficheros

Figura 1. Arquitectura de los componentes de un sistema de ficheros

El espacio de usuario (user space) contiene las aplicaciones (por ejempo, el usuario del sistema de ficheros) y las librerias GNU de C (glibc), que proveen la interfaz de usuario para las llamadas al sistema de ficheros (open, read, write, close). La interfaz de llamadas a sistema actua como un conmutador, concentrando las llamadas a sistema desde el espacio de usuario hacia los puntos de entrada apropiados dentro del espacio del núcleo (kernel space).

El VFS es el interfaz primario de los sistemas de ficheros subyacentes. Este componente exporta un conjunto de interfaces y después las abstrae para los sistemas de ficheros individuales, que pueden comportarse de maneras muy diferentes unos de otros. Existen dos cachés para los objetos del sistema de ficheros (inodos y entradas de directorio), que definiré brevemente. Cada una proveé un contenedor de objetos usados recientemente por el sistema de ficheros.

Cada implementación particular de un sistema de ficheros, tales como EXT2, JFS, etc., exporta un conjunto común de interfaces que son usadas (y esperadas) por el VFS. El buffer caché almacena peticiones entre el sistema de ficheros y los dispositivos de bloques que manipula. Por ejemplo, peticiones de lectura y escritura al dispositivo subyacente son pasadas a través del buffer caché. Esto permite que las peticiones sean cacheadas ahí para un acceso más rápido (en lugar de salir directamente hacia el dispositivo físico). El buffer caché es manejado como un conjunto de las listas menos usadas recientemente (LRU, por sus siglas en inglés). Es importante fijarse que se puede usar el comando sync para liberar todo el buffer caché hacia el medio de almacenamiento (para forzar la salida de todos los datos no escritos hacia los controladores de dispositivo, y a continuación hacia el dispositivo de almacenamiento).

Esta es la vista aérea de un VFS y de los componentes de un sistema de ficheros. Ahora echemos un vistazo a las principales estructuras que implementan este subsystema.

¿Qué es un dispositivo de bloques?

Un dispositivo de bloques es aquel en el que los datos que envia y recibe se mueven en bloques (tales como sectores de discos) y soporta atributos tales como buffering y acceso aleatorio (no es necesario que lea bloques secuencialmente, pero si que pueda acceder cualquier bloque en cualquier momento). Los dispositivos de bloques incluyen discos duros, CD-ROMs, y discos RAM. En contraste con los dispositivos de caracteres, de los que difieren en que ellos no tienen un medio fisicamente direccionable.  Los dispositivos de caracteres incluyen puertos serie y dispositivos de cinta, en los que los datos son secuenciados caracter a caracter.

Principales estructuras

Linux ve todos los sistemas de ficheros desde la perspectiva de un conjunto común de objetos. Estos objetos son el superbloque, el inodo, las entradas-D (dentry), y el fichero. En la raíz de cada sistema de ficheros está el superbloque, que describe y mantiene el estado del sistema de ficheros. Cada objeto que es gestionado dentro de un sistema de ficheros (fichero o directorio) está representado en Linux como un inodo. El inodo contiene todos los metadatos para gestionar objetos en el sistema de ficheros (incluyendo las operaciones que son posibles sobre él). Otro conjunto de estructuras, llamadas entradas-D (dentries), son usadas para hacer la traducción entre nombres e inodos, para los que existe una caché de directorios que mantiene los más recientemente usados. La entrada-D también mantiene las relaciones entre directorios y ficheros a través del sistema de ficheros. Finalmente, un fichero VFS representa un fichero abierto (mantiene un estado para el fichero abierto tal como el desplazamiento de lectura, y demás).

Capa del sistema de ficheros virtual (VFS)

El VFS actua como el nivel raíz de la interfaz de sistema de ficheros. El VFS mantiene registro de los sistemas de ficheros actualmente soportados, así como de aquellos sistemas de ficheros que están actualmente montados.

Los sistemas de ficheros pueden ser añadidos o eliminados de Linux dinámicamente usando un conjunto de funciones de registro. El núcle guarda una lista de los sistemas de ficheros actualmente soportados, que puede ser vista desde el espacio de usuario a través del sistema de ficheros /proc. Este fichero virtual también muestra los dispositivos actualmente asociados con algún sistema de ficheros. Para añadir un nuevo sistema de ficheros a Linux, se llama a la función register_filesystem. Ésta toma un único argumento que define la referencia a una estructura de sistema de ficheros (file_system_type), que define el nombre de un sistema de ficheros, un conjunto de atributos, y dos funciones de superbloque. Un sistema de ficheros también puede ser des-registrado.

Al registrar un nuevo sistema de ficheros, se ubica éste y la información pertinente sobre la lista file_systems (ver Figura 2 y linux/include/linux/mount.h). Esta lista define los sistemas de ficheros que pueden ser soportados. Se puede ver esta lista tecleando cat /proc/filesystems en la linea de comandos.

Figura 2. Sistemas de ficheros registros con el núcleo

Figura 2. Sistemas de ficheros registros con el núcleo

Otra estructura mantenida en el VFS son los sistemas de ficheros montados (ver Figura 3). Esto proveé los sistemas de ficheros que actualmente están montados (ver linux/include/linux/fs.h). Esto enlaza con la estructura de superbloque, que exploraré a continuación.

Lista de sistemas de ficheros montados

Figura 3. Lista de sistemas de ficheros montados

Superbloque

El superbloque es una estructura que representa un sistema de ficheros. Incluye la información necesaria para manejar el sistema de ficheros durante las operaciones. Incluye el nombre del sistema de ficheros (tal como EXT2), el tamaño del sistema de ficheros y su estado, una referencia al dispositivo de bloques, e información de metadatos (tal como listas libres y demás). El superbloque es típicamente almacenado en el medio de almacenamiento pero puede ser creado en tiempo real si no existe uno. Se puede ver la estructura de superbloque (Figura 4) en linux/include/linux/fs.h.

Figura 4. Estructura de superbloque y operaciones de inodo

Figura 4. Estructura de superbloque y operaciones de inodo

Un elemento importante del superbloque es la definición de las operaciones del superbloque. Esta estructura define un conjunto de funciones para manejo de inodos dentro del sistema de ficheros. Por ejemplos, los inodos pueden ser asignados con alloc_inode o eliminado con destroy_inode. Puedes leer y escribir inodos con read_inode and write_inode o sincronizar el sistema de ficheros con sync_fs. Se pueden encontrar la estructura de las super_operations (operaciones de superbloque) en /linux/include/linux/fs.h. Cada sistema de ficheros proveé sus propios métodos de inodos, que implementan las operaciones y proveén la abstracción común de la capa VFS.

inodo y entrada-d

El inodo representa un objeto en el sistema de ficheros con un identificador único. Cada sistema de ficheros particular proveé métodos para convertir un nombre de fichero en un identificador único de inodo y despues a una referencia a un inodo. Una porción de la estructura de un inodo se muestra en la Figura 5 junto con algunas de las estructuras relacionadas. Cabe notar en particular las estructuras inode_operations (operaciones sobre un inodo) y file_operations (operaciones sobre ficheros). Cada una de esas estructuras se refiere a las operaciones individuales que pueden ser ejecutadas sobre un inodo concreto. Por ejemplo, inode_operations define aquellas operaciones que operan directamente sobre un inodo, mientras que file_operations se refiere a aquellos émtodos relacionados con ficheros y directorios (las llamadas a sistema estándar).

Figura 5. La estructura de un inodo y las estructuras relacionadas

Figura 5. La estructura de un inodo y las operaciones asociadas

Los inodos y entradas-d más recientemente usados son guardados en la caché de inodos y directorios respectivamente. Cabe destacar que para cada inodo en la caché de inodos hay una entrada-d correspondiente en la caché de directorios. Pueden encontrarse las estructuras de inodo y entrada-d definidas en ./linux/include/linux/fs.h

El caché de bloques (buffer caché)

Excepto en las implementaciones particulares de cada sistema de ficheros (que puede ser encontrada en ./linux/fs), la parte baja de la capa de sistema de ficheros es el buffer caché. Este elemento mantiene un registro de las peticiones de lecturas y escrituras de cada implementacion particular del sistema de ficheros y de los dispositivos físicos (a través de los controladores de dispositivos). Por cuestiones de eficiencia, Linux mantiene una caché de las peticiones para evitar tener que volver a ir al dispositivo físico para cada petición. En su lugar, los bloques (páginas) más recientemente usados son cacheados aquí para ser rápidamente devueltos al sistema de ficheros particular.

Sistemas de ficheros interesantes

Este artículo no va a extenderse explorando los sistemas de ficheros particulares que están disponibles para Linux, pero es importante hablar de ellos, aunque sea de pasada. Linux soporta un amplio abanico de sistemas de ficheros diferentes, desde los antiguos sistemas de ficheros tales como MINIX, MS-DOS, y ext2, hasta los nuevos sistemas de ficheros con bitácora (journaling) tales como ext3, JFS y ReiserFS. Adicionalmente, Linux soporta sistemas de ficheros criptográficos tales como CFS, y sistemas de ficheros virtuales tales como /proc.

Por último, un sistema de ficheros digno de mención es el sistema de ficheros dentro del espacio de usuario, o FUSE (por sus siglas en inglés). Este es un interesante proyecto que permite al usuario dirigir las peticiones al sistema de ficheros a través del VFS de vuelta hacia el espacio de usuario. De modo que si alguna vez has jugado con la idea de crear tu propio sistema de ficheros, este es un gran punto de inicio.

Conclusiones

Mientras que la implementación de un sistema de ficheros es cualquier cosa menos trivial, este es un gran ejemplo de una arquitectura escalable y extensible. La arquitectura de sistema de ficheros ha evolucionado a través de los años pero ha soportado exitósamente muchos tipos diferentes de sistemas de ficheros y muchos tipos de dispositivos de almacenamiento objetivo. Usando una arquitectura basada en conectores (plug-ins) con varios niveles de funciones indirectas, será interesante ver la evolución del sistema de ficheros de Linux en el futuro cercano.