El camino de open y read: de la ruta al dato
La síntesis del VFS: cómo open resuelve una ruta con el path walk, obtiene el inodo llamando al lookup del sistema de archivos cuando falta en la dcache, asigna e instala el struct file, y cómo el read posterior delega en read_iter para servir el dato desde el page cache o el disco.
Llega el momento de ensamblar las piezas. Tienes el superbloque de un montaje, los inodos sin nombre, las dentries que les ponen nombre y los struct file de cada apertura. Ahora recorramos las dos syscalls que los ponen a todos a trabajar juntos: open(), que traduce una ruta de texto en un descriptor, y read(), que convierte ese descriptor en bytes. Seguir su camino de principio a fin es ver el VFS entero en movimiento: un path walk que consulta la dcache, un lookup que baja al disco cuando falla, un struct file que se fabrica e instala, y una delegación final en el sistema de archivos concreto.
- Seguir
open()desde la syscall hasta el path walk que resuelve la ruta componente a componente. - Ver cómo un fallo de dcache invoca
i_op->lookuppara traer el inodo del disco. - Entender cómo se asigna el
struct file, se fija suf_opy se instala el descriptor. - Recorrer el
read()posterior hastaread_itery su delegación en el page cache o el sistema de archivos.
open, capa a capa
La syscall openat desciende por una escalera de funciones cuyo objetivo es doble: resolver la ruta hasta un inodo y fabricar el struct file que lo represente:
/* fs/open.c y fs/namei.c (esquema de la cadena) */
do_sys_openat2() /* prepara flags y modo */
-> do_filp_open() /* orquesta la resolucion */
-> path_openat() /* asigna el struct file vacio */
-> link_path_walk() /* recorre los componentes intermedios */
-> do_open() / open_last_lookups() /* resuelve el ultimo y abre */
path_openat reserva pronto un struct file vacío con alloc_empty_file y lo va rellenando a medida que la ruta se resuelve. El estado del recorrido vive en un struct nameidata: la ruta actual, el componente pendiente, las banderas LOOKUP_RCU y demás. Es el cuaderno de bitácora del path walk.
El path walk: resolver componente a componente
link_path_walk parte de un punto inicial —la raíz / o el directorio de trabajo— y consume la ruta un componente cada vez. Por cada uno calcula el hash del nombre y lo busca en la dcache bajo el directorio actual. Si acierta, avanza; si falla, cae al lookup del sistema de archivos:
/* fs/namei.c (esquema muy simplificado de walk_component) */
static int walk_component(struct nameidata *nd, int flags)
{
struct dentry *dentry;
dentry = lookup_fast(nd); /* dcache: rcu-walk o ref-walk */
if (unlikely(!dentry)) {
/* fallo de dcache: hay que preguntar al sistema de archivos */
dentry = lookup_slow(&nd->last, nd->path.dentry, flags);
if (IS_ERR(dentry))
return PTR_ERR(dentry);
}
/* avanza nd->path al componente recien resuelto */
return step_into(nd, flags, dentry);
}
El caso interesante es el fallo. lookup_slow acaba llamando a la operación de metadatos del directorio, i_op->lookup, que es responsabilidad del sistema de archivos concreto. Ext4 leerá el bloque de directorio del disco, buscará la entrada por nombre, y si la encuentra traerá su inodo con iget y lo enganchará a la dentry con d_splice_alias:
/* esquema de una implementacion de lookup en un fs concreto */
static struct dentry *ext4_lookup(struct inode *dir, struct dentry *dentry,
unsigned int flags)
{
struct inode *inode = NULL;
ino_t ino = ext4_find_entry(dir, &dentry->d_name); /* busca en el disco */
if (ino)
inode = ext4_iget(dir->i_sb, ino); /* trae el inodo */
return d_splice_alias(inode, dentry); /* nombre e inodo unidos */
}
Tras esta llamada la dentry queda poblada en la dcache: la próxima resolución de esa ruta será un acierto directo, sin disco. Si el nombre no existía, la dentry se queda negativa (nivel 37.3) y cachea también esa ausencia. Así se llena la dcache: fallo a fallo, cada lookup que baja al disco deja tras de sí una entrada que evita el siguiente viaje.
flowchart TD
O[openat con la ruta] --> W[link_path_walk]
W --> C[walk_component por cada nombre]
C --> LF{lookup_fast acierta en la dcache}
LF -->|si| NX[avanza al siguiente componente]
LF -->|no| LS[lookup_slow]
LS --> IL[i_op lookup del sistema de archivos]
IL --> IG[iget trae el inodo del disco]
IG --> SP[d_splice_alias puebla la dentry]
SP --> NX
NX --> DO[do_open fabrica el struct file]Fabricar el struct file
Resuelto el último componente, el núcleo tiene la dentry final y su inodo. Ahora completa el struct file que reservó al principio, en do_dentry_open. Aquí es donde se fija la vtable del archivo abierto:
/* fs/open.c (esquema de do_dentry_open) */
static int do_dentry_open(struct file *f, ...)
{
struct inode *inode = f->f_path.dentry->d_inode;
f->f_inode = inode;
f->f_mapping = inode->i_mapping; /* atajo al page cache */
f->f_op = fops_get(inode->i_fop); /* vtable por defecto del inodo */
if (f->f_op->open) {
int error = f->f_op->open(inode, f); /* el open concreto */
if (error)
return error;
}
return 0;
}
Dos líneas condensan todo el nivel. f->f_op = fops_get(inode->i_fop) instala en el archivo abierto la tabla de operaciones que el inodo declaró (nivel 37.2). Y la llamada a f->f_op->open da al sistema de archivos o al driver su oportunidad de intervenir; es exactamente el gancho por donde chrdev_open (nivel 37.4) reemplaza la f_op por la de tu char device. Con el struct file listo, fd_install(fd, file) lo cuelga de la tabla de descriptores y el número fd regresa al espacio de usuario.
read: del descriptor al dato
El read(fd, buf, n) posterior es corto porque el trabajo duro ya está hecho. El núcleo traduce fd a struct file, y el resto es la delegación que viste en el nivel 37.1:
/* fs/read_write.c (esquema) */
ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd); /* fd -> struct file */
loff_t pos = file_pos_read(f.file); /* lee f_pos */
ssize_t ret = vfs_read(f.file, buf, count, &pos);
if (ret >= 0)
file_pos_write(f.file, pos); /* avanza f_pos */
fdput_pos(f);
return ret;
}
vfs_read invoca f_op->read_iter, que para casi todos los sistemas de archivos es filemap_read: busca el folio en el page cache y, si falla, dispara la lectura de disco con a_ops->read_folio (nivel 24). El offset f_pos se lee antes y se reescribe después: por eso cada struct file avanza su propia posición. La cadena completa —fd, struct file, f_op, read_iter, page cache, address_space_operations, bio— atraviesa cada capa que has estudiado, y en su último eslabón el formato concreto decide si el dato viene de un SSD, de la red o de RAM.
Contempla el viaje entero de una sola letra. Escribes open("/home/ana/carta.txt") y entregas al núcleo una cadena de texto, la cosa más tonta que existe: bytes ASCII sin estructura. Lo que el núcleo hace con ella es una de las transformaciones más elegantes de todo el sistema. Trocea la cadena por las barras y, componente a componente, la convierte en un camino físico a través de estructuras de datos: cada nombre se resuelve buscándolo en la dcache bajo su padre, y ese salto —de un nombre a la dentry hija, de la dentry a su inodo, del inodo al directorio siguiente— repetido tantas veces como barras haya, va tejiendo un descenso por el árbol de directorios que solo toca el disco cuando la caché tiene un hueco. Al final del descenso hay un inodo, y del inodo cuelga una tabla de punteros a función que nadie eligió por ti: la eligió el sistema de archivos cuando ese inodo se cargó. Fabricas un struct file, le clavas esa tabla, y desde ese momento cada read que emitas es una llamada indirecta que aterriza en el código de ext4, de NFS o de tu driver, sin que la syscall genérica sepa jamás cuál. Ese es el VFS completo, y su lección última es que el polimorfismo no es un lujo de los lenguajes orientados a objetos: es la única forma sensata de que un núcleo soporte cincuenta sistemas de archivos sin cincuenta implementaciones de read. Una ruta de texto entra por un extremo; por el otro sale una llamada a la función exacta que sabe traer ese dato. Todo lo demás —superbloques, inodos, dentries, files, caches— es la maquinaria que hace posible esa única traducción, ejecutada millones de veces por segundo en cada máquina Linux del planeta.
- Con
strace -e openat,read cat /etc/hostname, observa elopenatque devuelve unfdy elreadque lo consume. - Usa
sudo perf trace -e 'vfs_*' cat archivoofuncgraphpara ver la cadena real de funciones del VFS durante una lectura. - Explica, para la ruta
/a/b/c, cuántas invocaciones awalk_componentocurren y qué papel juega la dcache en cada una. - Describe qué hace
d_splice_aliastras unlookupcon éxito y por qué la siguiente resolución de esa ruta no toca el disco. - Traza la línea que va de
f->f_op = fops_get(inode->i_fop)endo_dentry_openhastafilemap_read, nombrando cada objeto del VFS que interviene.