En orden para soportar múltiples sistemas de archivos, Linux contiene un nivel especial de interfaces del núcleo llamado VFS (Interruptor de Sistemas de Ficheros Virtuales). Esto es muy similar a la interfaz vnode/vfs encontrada en los derivados de SVR4 (originalmente venían de BSD y de las implementaciones originales de Sun).
La antememoria de inodos de Linux es implementada en un simple fichero,
fs/inode.c, el cual consiste de 977 lineas de código. Es interesante notar que no se han realizado muchos cambios en él durante los últimos 5-7 años: uno todavía puede reconocer algún código comparando la última version con, digamos, 1.3.42.
La estructura de la antememoria de inodos Linux es como sigue:
- Una tabla global hash, inode_hashtable, donde cada inodo es ordenado por el valor del puntero del superbloque y el número de inodo de 32bit. Los inodos sin un superbloque (inode->i_sb
NULL
) son añadidos a la lista doblemente enlazada encabezada por anon_hash_chain en su lugar. Ejemplos de inodos anónimos son los conectores creados por net/socket.c:sock_alloc(), llamado por fs/inode.c:get_empty_inode().
- Una lista global del tipo "en_uso" (inode_in_use), la cual contiene los inodos válidos con i_count>0 y i_nlink>0. Los inodos nuevamente asignados por get_empty_inode() y get_new_inode() son añadidos a la lista inode_in_use.
- Una lista global del tipo "sin_usar" (inode_unused), la cual contiene los inodos válidos con i_count = 0.
- Una lista por cada superbloque del tipo "sucia" (sb->s_dirty) que contiene los inodos válidos con i_count>0, i_nlink>0 y i_state & I_DIRTY. Cuando el inodo es marcado como sucio, es añadido a la lista sb->s_dirty si el está también ordenado. Manteniendo una lista sucia por superbloque de inodos nos permite rápidamente sincronizar los inodos.
- Una antememoria propia de inodos - una antememoria SLAB llamada inode_cachep. Tal como los objetos inodos son asignados como libres, ellos son tomados y devueltos a esta antememoria SLAB.
Los tipos de listas son sujetadas desde
inode->i_list, la tabla hash desde
inode->i_hash. Cada inodo puede estar en una tabla hash y en uno, y en sólo uno, tipo de lista (en_uso, sin_usar o sucia).
Todas estas listas están protegidas por un spinlock simple:
inode_lock.
El subsistema de caché de inodos es inicializado cuando la función
inode_init() es llamada desde
init/main.c:start_kernel(). La función es marcada como
init, lo que significa que el código será lanzado posteriormente. Se le pasa un argumento simple - el número de páginas físicas en el sistema. Esto es por lo que la antememoria de inodos puede configurarse ella misma dependiendo de cuanta memoria está disponible, esto es, crea una tabla hash más grande si hay suficiente memoria.
Las únicas estadísticas de información sobre la antememoria de inodos es el número de inodos sin usar, almacenados en
inodes_stat.nr_unused y accesibles por los programas de usuario a través de los archivos
/proc/sys/fs/inode-nr y
/proc/sys/fs/inode-state.
Podemos examinar una de las listas desde
gdb en un núcleo en funcionamiento de esta forma:
(gdb) printf "%d\n", (unsigned long)(&((struct inode *)0)->i_list)
8
(gdb) p inode_unused
$34 = 0xdfa992a8
(gdb) p (struct list_head)inode_unused
$35 = {next = 0xdfa992a8, prev = 0xdfcdd5a8}
(gdb) p ((struct list_head)inode_unused).prev
$36 = (struct list_head *) 0xdfcdd5a8
(gdb) p (((struct list_head)inode_unused).prev)->prev
$37 = (struct list_head *) 0xdfb5a2e8
(gdb) set $i = (struct inode *)0xdfb5a2e0
(gdb) p $i->i_ino
$38 = 0x3bec7
(gdb) p $i->i_count
$39 = {counter = 0x0}
Destacar que restamos 8 de la dirección 0xdfb5a2e8 para obtener la dirección de
struct inode (0xdfb5a2e0) de acuerdo a la definición de la macro
list_entry() de
include/linux/list.h.
Para entender cómo trabaja la antememoria de inodos, déjanos seguir un tiempo de vida de un inodo de un fichero regular en el sistema de ficheros ext2, el cómo es abierto y cómo es cerrado:
fd = open("file", O_RDONLY);
close(fd);
La llamada al sistema
open(2) es implementada en la función
fs/open.c:sys_open y el trabajo real es realizado por la función
fs/open.c:filp_open(), la cual está dividida en dos partes:
- open_namei(): rellena la estructura nameidata conteniendo las estructuras dentry y vfsmount.
- dentry_open(): dado dentry y vfsmount, esta función asigna una nueva struct file y las enlaza a todas ellas; también llama al método específico del sistema de ficheros f_op->open() el cual fue inicializado en inode->i_fop cuando el inodo fue leído en open_namei() (el cual suministra el inodo a través de dentry->d_inode).
La función
open_namei() interactúa con la antememoria dentry a través de
path_walk(), el cual en el regreso llama a
real_lookup(), el cual llama al método específico del sistema de ficheros
inode_operations->lookup(). La misión de este método es encontrar la entrada en el directorio padre con el nombre correcto y entonces hace
iget(sb, ino) para coger el correspondiente inodo - el cual nos trae la antememoria de inodos. Cuando el inodo es leido, el dentry es instanciado por medio de
d_add(dentry, inode). Mientras estamos en él, nótese que en los sistemas de ficheros del estilo UNIX que tienen el concepto de número de inodos en disco, el trabajo del método lookup es mapear su bit menos significativo al actual formato de la CPU, ej. si el número de inodos en la entrada del directorio sin formato (específico del sistema de ficheros) está en el formato de 32 bits little-endian uno haría:
unsigned long ino = le32_to_cpu(de->inode);
inode = iget(sb, ino);
d_add(dentry, inode);
Por lo tanto, cuando abrimos un fichero nosotros llamamos a
iget(sb, ino) el cual es realmente
iget4(sb, ino, NULL, NULL), el cual hace:
- Intenta encontrar un inodo con el superbloque emparejado y el número de inodo en la tabla hash bajo la protección de inode_lock. Si el inodo es encontrado, su cuenta de referencia (i_count) es incrementada; si era 0 anteriormente al incremento y el inodo no estaba sucio, es quitado de cualquier tipo de lista (inode->i_list) en la que esté (tiene que estar en la lista inode_unused, por supuesto) e insertado en la lista del tipo inode_in_use; finalmente inodes_stat.nr_unused es decrementado.
- Si el inodo está actualmente bloqueado, esperaremos hasta que se desbloquee, por lo tanto está garantizado que iget4() devolverá un inodo desbloqueado.
- Si el inodo no fue encontrado en la tabla hash entonces es la primera vez que se pide este inodo, por lo tanto llamamos a get_new_inode(), pasándole el puntero al sitio de la tabla hash donde debería de ser insertado.
- get_new_inode() asigna un nuevo inodo desde la antememoria SLAB inode_cachep, pero esta operación puede bloquear (asignación GFP_KERNEL), por lo tanto el spinlock que guarda la tabla hash tiene que ser quitado. Desde que hemos quitado el spinlock, entonces debemos de volver a buscar el inodo en la tabla; si esta vez es encontrado, se devuelve (después de incrementar la referencia por iget) el que se encontró en la tabla hash y se destruye el nuevamente asignado. Si aún no se ha encontrado en la tabla hash, entonces el nuevo inodo que tenemos acaba de ser asignado y es el que va a ser usado; entonces es inicializado a los valores requeridos y el método específico del sistema de ficheros sb->s_op->read_inode() es llamado para propagar el resto del inodo. Esto nos proporciona desde la antememoria de inodos la vuelta al código del sistema de archivos - recuerda que venimos de la antememoria de inodos cuando el método específico del sistema de ficheros lookup() llama a iget(). Mientras el método s_op->read_inode() está leyendo el inodo del disco, el inodo está bloqueado (i_state = I_LOCK); él es desbloqueado después de que el método read_inode() regrese y todos los que están esperando por el hayan sido despertados.
Ahora, veamos que pasa cuando cerramos este descriptor de ficheros. La llamada al sistema
close(2) está implementada en la función
fs/open.c:sys_close(), la cual llama a
do_close(fd, 1) el cual rompe (reemplaza con NULL) el descriptor del descriptor de ficheros de la tabla del proceso y llama a la función
filp_close(), la cual realiza la mayor parte del trabajo. La parte interesante sucede en
fput(), la cual chequea si era la última referencia al fichero, y si es así llama a
fs/file_table.c:_fput() la cual llama a
fput() en la cual es donde sucede la interacción con dcache (y entonces con la memoria intermedia de inodos - ¡recuerda que dcache es la memoria intermedia de inodos Maestra!). El
fs/dcache.c:dput() hace
dentry_iput() la cual nos brinda la vuelta a la memoria intermedia de inodos a través de
iput(inode), por lo tanto déjanos entender
fs/inode.c:iput(inode):
- Si el parámetro pasado a nosotros es NULL, no hacemos nada y regresamos.
- Si hay un método específico del sistema de archivos sb->s_op->put_inode(), es llamada inmediatamente sin mantener ningún spinlock (por lo tanto puede bloquear).
- El spinlock inode_lock es tomado y i_count es decrementado. Si NO era la última referencia a este inodo entonces simplemente chequeamos si hay muchas referencias a el y entonces i_count puede urdir sobre los 32 bits asignados a el si por lo tanto podemos imprimir un mensaje de peligro y regresar. Nótese que llamamos a printk() mientras mantenemos el spinlock inode_lock - esto está bien porque printk() nunca bloquea, entonces puede ser llamado absolutamente en cualquier contexto (¡incluso desde el manejador de interrupciones!).
- Si era la última referencia activa entonces algún trabajo necesita ser realizado.
EL trabajo realizado por
iput() en la última referencia del inodo es bastante complejo, por lo tanto lo separaremos en una lista de si misma:
- Si i_nlink
0 (ej. el fichero fué desenlazado mientras lo manteníamos abierto) entonces el inodo es quitado de la tabla hash y de su lista de tipos; si hay alguna página de datos mantenida en la antememoria de páginas para este inodo, son borradas por medio de truncate_all_inode_pages(&inode->i_data). Entonces el método específico del sistema de archivos s_op->delete_inode() es llamado, el cual típicamente borra la copia en disco del inodo. Si no hay un método s_op->delete_inode() registrado por el sistema de ficheros (ej. ramfs) entonces llamamos a clear_inode(inode), el cual llama s_op->clear_inode() si está registrado y si un inodo corresponde a un dispositivo de bloques, esta cuenta de referencia del dispositivo es borrada por bdput(inode->i_bdev).
- Si i_nlink != 0 entonces chequeamos si hay otros inodos en el mismo cubo hash y si no hay ninguno, entonces si el inodo no está sucio lo borramos desde su tipo de lista y lo añadimos a la lista inode_unused incrementando inodes_stat.nr_unused. Si hay inodos en el mismo cubo hash entonces los borramos de la lista de tipo y lo añadimos a la lista inode_unused. Si no había ningún inodo (NetApp .snapshot) entonces lo borramos de la lista de tipos y lo limpiamos/destruimos completamente.