El sistema de ficheros virtual (VFS) de Linux (II)

Autor: Juan Antonio Martínez Castaño
E-mail: jantonio@dit.upm.es
Web: http://www.dit.upm.es/~jantonio

Copyright

Este artículo es Copyright 1999 de Juan Antonio Martínez Castaño y se distribuye bajo las siguientes condiciones:

Indice


Entradilla

Continuando con la serie dedicada al Linux Virtual File System, vamos a estudiar en esta entrega el funcionamiento del VFS, desde el procedimiento de montaje hasta como se llega a las funciones de E/S sobre el dispositivo

Presentación

En el número Anterior de Linux Actual, se procedió a describir el por qué de la existencia del Sistema de Ficheros Virtual de Linux, elementos de que contaba un filesystem, y la operación de registro de un sistema de ficheros en el núcleo. En este artículo, continuando la serie, vamos a describir el proceso de montaje y desmontaje, y los diversos mecanismos de que Linux se vale para navegar a través del sistema de ficheros

Introducción

Hagamos un pequeño resumen de la entrega anterior:

Antes de seguir, es recomendable que el lector tenga a mano el número anterior de Linux Actual, y repase cuidadosamente las estructuras de datos y operaciones de registro de sistemas de ficheros descritos en él. En el CD-Rom que se acompaña a la revista se incluye la documentación existente en la actualizad sobre el VFS, así como diversos ejemplos de sistemas de ficheros, alguno de ellos desarrollado por el autor. Donde sea posible, se remitirá al código fuente del núcleo Linux, utilizándose -a menos que se diga lo contrario- la versión 2.12 del núcleo Linux

Una vez preparados, podemos continuar:


La operación de montaje

La operación siguiente a la del registro del sistema de ficheros es la de montaje. la operación de montaje se realiza de la siguiente manera

Con la operación mount() cada sistema de ficheros personaliza un filesystem dado en una instancia del mismo

Echemos un vistazo a la lista de operaciones que se pueden realizar con el superbloque. Recordemos que el superbloque suele contener información sobre el sistema de ficheros, punteros a los inodos, etc. Del mismo modo debe contener los métodos de acceso al inodo raíz del sistema de ficheros. Lo usual es que las operaciones sobre el inodo raíz sean ligeramente distintas a las del resto de los inodos ( pues intervienen punteros al superbloque, así como a estructuras ajenas al sistema de ficheros ). Por ello es normal que existan una familia de inode_operations y de root_inode_operations. El listado 1 ilustra la lista de operaciones sobre el superbloque:

struct super_operations {
        void (*read_inode) (struct inode *);
        void (*write_inode) (struct inode *);
        void (*put_inode) (struct inode *);
        void (*delete_inode) (struct inode *);
        int (*notify_change) (struct dentry *, struct iattr *);
        void (*put_super) (struct super_block *);
        void (*write_super) (struct super_block *);
        int (*statfs) (struct super_block *, struct statfs *, int);
        int (*remount_fs) (struct super_block *, int *, char *);
        void (*clear_inode) (struct inode *);
        void (*umount_begin) (struct super_block *);
};
Listado 1: Operaciones sobre el superbloque

Distinguimos entre operaciones sobre el inodos y sobre el superbloque. todas las operaciones sobre inodos y ficheros pasan por la lectura de los datos del inodo. Para ello, existen una serie de entradas en la lista de operaciones sobre el superbloque, que permiten la lectura, escritura, asignación o liberación de inodos. La estructura inode que el VFS pasa al superbloque contiene información sobre el inodo deseado. Un inodo especial es el inodo raíz, o root_inode
Recordemos que en el 99% de los casos, el inodo raíz corresponde casi siempre a un directorio, denominado directorio raíz; pero no siempre tiene por qué ser así: en los casos de redirectores ( loop devices, cryptfs, etc ) las estructuras no corresponden a directorios, sino que redirigen las operaciones a otros comandos
Del mismo modo, aunque insistiremos posteriormente sobre ello, las operaciones, se realizan sobre el inodo, no sobre los ficheros, directorios ( según corresponda ) asociados al inodo. Es muy importante que el lector distinga las operaciones sobre inodos de las operaciones sobre los ficheros asociados

Las operaciones sobre inodos realizadas en el superbloque no tienen sino la misión de proporcionar al VFS punteros a posteriores operaciones. Empecemos pues a navegar por el sistema de ficheros

Es preciso saber distinguir entre operaciones sobre inodos y operaciones sobre ficheros

La figura 1 ilustra las estructuras y punteros que maneja el VFS para poder mantener y operar con los diversos sistemas de ficheros montados en el sistema

estructuras usadas en mount()
Figura 1:Estructuras utilizadas en las operaciones de mount()


Navegando por el Sistema de Ficheros

Una vez montado el sistema de ficheros, la primera operación sobre él es la lectura del inodo raíz. El descriptor del superbloque contiene un identificador del root_inode_number, que es utilizada por el VFS para acceder al primer inodo del sistema de ficheros
Vamos a ilustrar el proceso con un ejemplo: tenemos el un sistema de ficheros montado sobre /mnt/mi_sistema, y dentro de él queremos acceder al fichero directorio1/mi_fichero. Esto es: queremos leer el fichero /mnt/mi_sistema/directorio1/mi_fichero . El listado 2 indica las estructuras de operaciones que se pueden realizar con inodos y ficheros.

struct file_operations {
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, struct dentry *);
	int (*fasync) (int, struct file *, int);
	int (*check_media_change) (kdev_t dev);
	int (*revalidate) (kdev_t dev);
	int (*lock) (struct file *, int, struct file_lock *);
};

struct inode_operations {
	struct file_operations * default_file_ops;
	int (*create) (struct inode *,struct dentry *,int);
	int (*lookup) (struct inode *,struct dentry *);
	int (*link) (struct dentry *,struct inode *,struct dentry *);
	int (*unlink) (struct inode *,struct dentry *);
	int (*symlink) (struct inode *,struct dentry *,const char *);
	int (*mkdir) (struct inode *,struct dentry *,int);
	int (*rmdir) (struct inode *,struct dentry *);
	int (*mknod) (struct inode *,struct dentry *,int,int);
	int (*rename) (struct inode *, struct dentry *,
			struct inode *, struct dentry *);
	int (*readlink) (struct dentry *, char *,int);
	struct dentry * (*follow_link) (struct dentry *, struct dentry *, unsigned int);
	int (*readpage) (struct file *, struct page *);
	int (*writepage) (struct file *, struct page *);
	int (*bmap) (struct inode *,int);
	void (*truncate) (struct inode *);
	int (*permission) (struct inode *, int);
	int (*smap) (struct inode *,int);
	int (*updatepage) (struct file *, struct page *, unsigned long, unsigned int, int);
	int (*revalidate) (struct dentry *);
};
Listado 2: Operaciones sobre inodos y ficheros

  1. El primer paso es la lectura del root_inode. Para ello, el VFS llama a la rutina read_inode() del array de operaciones del superbloque, indicando que va a leer el inodo raíz. read_inode() devuelve una estructura inode, que a su vez incluye un puntero a una estructura inode_operations que contiene los punteros a las operaciones que se pueden realizar con el root inode. En nuestro caso el root inode corresponde a un directorio. Recordemos que cada estructura inode_operations incluye un puntero a una estructura file_operations que indica las operaciones que se pueden realizar con el fichero asociado a dicho inodo
  2. El VFS recibe el puntero al inodo, y encuentra que es un directorio ( el directorio "." que corresponde al directorio raíz del sistema de ficheros ). Por consiguiente la primera operación es leer el contenido de dicho directorio, para poder seguir buscando el fichero pedido. Recordemos que un directorio no es sino un fichero, luego por consiguiente invocará a la siguiente secuencia de funciónes
  3. Readdir() va obteniendo una lista de parejas nombre-inodo. El sistema de caché de directorios va almacenando las sucesivas entradas. Al encontrar el fichero deseado ( /mnt/mi_sistema/directorio1 ) el VFS obtiene el inodo buscado y vuelve a ejecutar sb->read_inode() para obtener información del inodo encontrado
  4. Puesto que "directorio1" es a su vez un directorio, se repite el proceso anterior haciendo un readdir() sobre directorio1, hasta encontrar la entrada "mi_fichero" que era lo que estabamos buscando. En todo este proceso el sistema de caché del VFS ha ido almacenando todos los inodos y directorios encontrados, de manera que no son necesarias nuevas operaciones de entrada/salida para recuperar la información.
  5. Finalmente, el VFS efectua un open() y un read() sobre "mi_fichero" que era la operación deseada

Notese que en ningún momento el VFS ha necesitado saber nada de las características del sistema de ficheros subyacente. Todas las operaciones se realizan mediante punteros a funciones, y es el código de cada sistema de ficheros en particular el que realiza el trabajo. Todos los detalles quedan ocultos al VFS que se dedica únicamente a gestionar el caché y a secuenciar los accesos. Esta separación de tareas y simplicidad en el proceso es el corazón de la potencia del Virtual File System de Linux

super_ops
Figura 2: Resumen de operaciones con el SuperBloque


Operaciones de entrada salida

Deliberadamente hemos omitido los procesos de entrada/salida de datos de la explicación anterior, pero el lector tendrá claro que los datos tienen que salir de algun sitio... es necesario un interfaz con los dispositivos físicos y un mecanismo de copia de datos desde / hacia el soporte físico. En el ejemplo anterior, estas operaciones se han realizado en los siguientes casos:

¿ Cómo se realizan estas operaciones ?. El lector recordará del número anterior de Linux Actual, que en el descriptor del sistema de ficheros había una entrada que indicaba al núcleo que dicho sistema de ficheros requería un dispositivo físico. Este flag es hace que el núcleo permita que el sistema de ficheros pueda acceder a dispositivos físicos, habilitando las operaciones de bloqueo/desbloqueo del proceso asociado a la operación de entrada salida.

Entre las estructuras del superbloque se deben encontrar las herramientas adecuadas para poder acceder a los bloques de datos del dispositivo asociado que correspondan a los inodos. Del mismo modo cada inodo contiene una lista de bloques asociados al fichero indexado por el inodo. Un tercer conjunto de datos nos indica la lista de bloques libres y ocupados en el sistema de ficheros. Con estas herramientas, el sistema de ficheros es capaz de solicitar operaciones de entrada/salida sobre el driver correspondiente.

Recordamos al lector que las entradas y salidas sobre un sistema de ficheros son operaciones de acceso en modo bloque, esto es, se leen o escriben de una vez un conjunto de datos. Del mismo modo la estructura del superbloque contiene información sobre el tamaño de los bloques que maneja el sistema de ficheros dado.

Para la realización de las operaciónes de lectura/escritura, se dispone de las primitivas bread() y bwrite(). Se recuerda al lector que estas operaciones son bloqueantes: el proceso asociado a la operacion de E/S queda a la espera de la ejecución, pudiendo serle cedido el control de ejecución de la CPU a otro proceso. El sistema de ficheros deberá ser capaz de ser diseñado de manera que sea reentrante: esto es, que varios procesos puedan estar ejecutándose concurrentemente sobre un sistema de ficheros dado. El núcleo Linux dispone de diversas primitivas de bloqueo y exclusión mutua que facilitan esta tarea


Temas avanzados

Aparte de las operaciones de lectura y escritura existen otro tipo de operaciones que se pueden realiar con las diversas estructuras de un sistema de ficheros. Vamos a describir alguna de ellas:

  1. Cuando un inodo corresponde a un enlace simbólico, el VFS puede realizar, bien la lectura del enlace en sí, o bien resolver el enlace, buscando el fichero asociado. Para el primer caso, hay que tener en cuenta que es política habitual que para el caso de enlaces simbólicos, y siempre que la longitude del nombre del fichero asociado al enlace sea menor que el tamaño reservado en el inodo a los punteros a los bloques ( que dado que en este caso no se usan, pues un enlace simbólico no es un fichero, sino un puntero a un fichero ) sean dedicados a almacenar el nombre del fichero al que apunta el enlace. Este sistema -para algunos algo truculento-,permite ahorrar una operación de entrada/salida a la hora de seguir el enlace.
    La operación realizada para resolver el enlace simbólico se indexa mediante la función follow_link de la estructura inode_operations del inodo correspondiente al enlace simbólico
  2. En ocasiones, especialmente cuando se busca un acceso inmediato a los datos de un fichero, como pueda ser en el caso de una base de datos, o en la carga de un fichero ejecutable, se realiza la operación de "mapeado en memoria" del fichero. Consiste en hacer que el fichero pueda ser accesible desde el proceso que lo solicita como si fuera parte del mapa de memoria del proceso.
    El núcleo Linux provee de una función mmap() por defecto, que permite utilizar los datos de bloques del descriptor del fichero contenido en el inodo asociado, para realizar la operación de mapeo. En el caso de que no se pueda utilizar dicha estructura, debido a que no se siga un sistema de bloques/inodos, el usuario deberá proveer su propia rutina mmap() que debe retornar el bloque asociado a la posición de memoria pedida por el Virtual File System.
    Si se desea utilizar la función generic_mmap() prevista por el núcleo Linux, ésta deberá ser incluída en la estructura inode_operations del inodo asociado. Recuérdese que sólo se pueden ejecutar aplicaciones binarias que residan en sistemas de ficheros que soporten la función mmap()
  3. En algunos sistemas de ficheros, las operaciones sobre inodos y ficheros no implican de forma tan directa un acceso al dispositivo físico: en el caso de un loop filesystem, éste simula un dispositivo de modo bloque para lo que no es sino un sistema de acceso en modo carácter a un fichero
  4. En otros casos, no hay una correspondencia real inodo-fichero: en el caso de los sistemas de red, los inodos son generados al vuelo bajo demanda. El NFS guarda una tabla de inodos virtuales y su correspondencia, existiendo unos mecanismos de validación y de control de consistencia.
    En otros casos, como el ejemplo que se incluye en el CD-Rom del "tar filesystem" la lista de inodos es generada al vuelo al realizar la operación de montaje. En el caso del "ROM Filesystem" un programa "mkromfs" genera off-line un fichero que corresponde al futuro sistema de ficheros, conteniendo la información de inodos y bloques precalculada.... cada implementación tiene sus propios detalles y trucos. Precisamente y gracias al VFS podemos obviar todas estas diferencias y tratar de forma unificada todos los sistemas de ficheros que Linux soporta

La figura 3 ilustra un resumen de operaciones realizables con inodos

inode_operations
Figura 3: Resumen de operaciones con inodos


Resumen. Conclusiones

En este capítulo dedicado al Virtual File System hemos descrito las operaciones de montaje y desmontaje, viendo cómo navega Linux entre los diversos componentes de un sistema de ficheros dados. Se han visto los mecanismos de las operaciones con inodos y ficheros, y se ha hecho una introducción a cómo se maneja el caché desde el VFS

Para cuendo el lector lea este artículo, estará posiblemente disponible la versión 2.4 del núcleo Linux. Si bien la metodología de trabajo con el VFS no cambia drásticamente con la nueva versión, el uso y gestión del VFS caché ha sido reestructurado por completo, añadiendole mucha mayor funcionalidad, rapidez y efectividad. La principal mejora respecto a la versión 2.2 del núcleo es que ya no existen diferencias entre el caché de lectura y el de escritura. Del mismo modo, las rutinas de validación de entradas en el caché se han reescrito y optimizado, arreglándose diversos problemas que surgían con las operaciones del caché en sistemas multiprocesador

La versatilidad y la potencia del Virtual File System de Linux permite a nuestro sistema operativo favorito en uno de los más potentes y flexibles. Muchos fabricantes de otros sistemas operativos tipo UNIX están empezando a portar sus sistemas de ficheros a Linux, como pueda ser Sillicon Graphics y su sistema de ficheros de IRIX. La posibilidad de montar sistemas de ficheros ficticios como capa intermedia entre el dispositivo físico y el VFS permite prácticas como el modo loop, sistemas de ficheros encriptados, ram disks... El nuevo núcleo 2.4 soporta a su vez el manejo de dispositivos de modo bloque en modo caracter, lo que facilita el acceso "crudo" a los datos. El lector puede comprobar leyendo esta serie de artículos, como todas estas operaciones son implementadas de una forma sencilla y altamente eficiente -como no podría ser menos- en nuestro Sistema Operativo favorito.


Referencias

En el CD-Rom que se acompaña a esta revista se incluye la mayor parte de la documentación disponible sobre el Virtual File System:

Del mismo modo, se incluyen los fuentes de diversos sistemas de ficheros , como un "tar filesystem", para visualización directa de ficheros tar, "intermezzo" un sistema de caché tipo CODA, o AFS, pero que utiliza el sistema de ficheros ext2 en lugar del mapeado en ficheros, y "cachefs", un esqueleto de un sistema de caché, utilizado en el número Anteriór de Linux Actual para describir el VFS y el /proc filesystem

Y por supuesto "Use the source, Luke", el código fuente del núcleo Linux es , como siempre, la mejor referencia.