struct file y file_operations: el archivo abierto
El `struct file` representa un archivo abierto por un proceso, con su propio offset y sus flags, y su tabla de `file_operations` es la vtable que atienden read, write, mmap e ioctl. Cómo se relaciona con el descriptor de archivo, por qué cada open crea uno nuevo, y cómo el char device del nivel 13 clava aquí sus operaciones.
El inodo es el archivo; la dentry es su nombre. Pero ninguno de los dos sabe dónde estás leyendo ahora mismo. Esa información —el offset actual, los flags de apertura, si abriste para leer o escribir— pertenece a un tercer objeto que nace con cada open() y muere con cada close(): el struct file. Es el archivo abierto, la sesión de un proceso con un inodo. Y su tabla de file_operations es la que de verdad ejecuta read y write. Aquí cierra el círculo que abriste en el nivel 13: la tabla que escribiste para tu char device era, sin que lo supieras, una vtable del VFS.
- Entender el
struct filecomo un archivo abierto, con offset y flags propios de cada apertura. - Distinguir el descriptor de archivo (
fd) delstruct filey de la tabla de descriptores. - Reconocer
file_operationscomo la vtable del archivo abierto y sus métodos clave. - Conectar el char device del nivel 13 con el VFS: cómo
chrdev_openinstala lasfile_operationsdel driver.
El archivo abierto y su offset
Cada open() que triunfa asigna un struct file. En los núcleos actuales su disposición está afinada al milímetro para caber en pocas líneas de caché:
/* include/linux/fs.h (muy recortado) */
struct file {
file_ref_t f_ref; /* refcount moderno, antes f_count */
fmode_t f_mode; /* FMODE_READ, FMODE_WRITE... */
const struct file_operations *f_op; /* la vtable del archivo abierto */
struct address_space *f_mapping; /* el page cache del inodo */
void *private_data; /* estado privado del driver */
struct inode *f_inode; /* el inodo abierto */
unsigned int f_flags; /* O_NONBLOCK, O_APPEND... */
const struct cred *f_cred; /* credenciales del que abrio */
struct path f_path; /* dentry mas vfsmount */
loff_t f_pos; /* EL offset actual de lectura */
struct file_ra_state f_ra; /* estado de readahead */
};
El campo que justifica la existencia del objeto es f_pos: el offset donde caerá el próximo read o write. Por eso el offset no puede vivir en el inodo: dos procesos que abren el mismo archivo tienen cada uno su propio struct file con su propio f_pos, y avanzan de forma independiente. f_op es la vtable; f_mapping es el atajo al page cache del inodo (nivel 24); y private_data es el bolsillo donde tu driver guarda estado por-apertura, como ya usaste en el nivel 13.
El struct file vive por conteo de referencias. fget toma una referencia y fput la suelta; cuando f_ref cae a cero, __fput ejecuta la limpieza. Ahí es donde el VFS llama a f_op->release —la función que escribiste en el nivel 13— exactamente una vez, cuando se cierra el último descriptor que apuntaba a ese archivo abierto. Por eso close() no siempre dispara release: si otro fd clonado por dup sigue vivo, la referencia no llega a cero. En los núcleos recientes f_ref es un file_ref_t, un contador con un sesgo que detecta el paso a cero sin operaciones atómicas caras en el camino común, y __fput se difiere a una tarea con task_work para no correr en contextos delicados.
fd, struct file y la tabla de descriptores
El número que maneja el espacio de usuario —el fd— no es el struct file: es un índice en la tabla de descriptores del proceso, que apunta al struct file. Esta indirección explica comportamientos clásicos de Unix:
flowchart LR P[Proceso] --> FDT[tabla de descriptores] FDT -->|fd 0| F0[struct file A] FDT -->|fd 1| F1[struct file B] FDT -->|fd 3| F1 F0 -->|f_inode| I1[inode] F1 -->|f_inode| I2[inode]
Un dup() copia la entrada de la tabla: dos fd apuntan al mismo struct file, comparten f_pos, y escribir por uno avanza el offset del otro. En cambio, dos open() del mismo archivo crean struct file distintos con offsets independientes. Y un fork() copia la tabla de descriptores entera pero comparte los struct file: por eso padre e hijo comparten posición en stdout. Toda esa semántica se deduce de un único hecho: quién comparte el struct file y quién no.
file_operations: la vtable del archivo abierto
La tabla que gobierna qué ocurre en cada syscall sobre el archivo abierto es file_operations. Es la más rica del VFS porque es la que ve el espacio de usuario:
/* include/linux/fs.h (recortado) */
struct file_operations {
struct module *owner;
loff_t (*llseek)(struct file *, loff_t, int);
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter)(struct kiocb *, struct iov_iter *); /* forma vectorial */
ssize_t (*write_iter)(struct kiocb *, struct iov_iter *);
int (*iterate_shared)(struct file *, struct dir_context *); /* readdir */
__poll_t (*poll)(struct file *, struct poll_table_struct *);
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
int (*mmap)(struct file *, struct vm_area_struct *);
int (*open)(struct inode *, struct file *);
int (*release)(struct inode *, struct file *);
int (*fsync)(struct file *, loff_t, loff_t, int datasync);
};
Hay dos familias de lectura. read es la forma simple que implementaste en el nivel 13, con un puntero de usuario y un tamaño. read_iter es la moderna: recibe un iov_iter que abstrae el destino (uno o varios búferes, incluso páginas del propio núcleo) y es la que usa el page cache genérico. Un sistema de archivos serio implementa read_iter; un char device sencillo se conforma con read. El VFS prueba una y luego la otra, como viste en vfs_read del nivel 37.1.
El char device del nivel 13, cerrado por dentro
En el nivel 13 escribiste una file_operations y la registraste en un struct cdev. Ahora puedes ver cómo el VFS la enchufa. Cuando abres un nodo de char device, el inodo lleva en i_fop no tus operaciones, sino unas genéricas del subsistema, def_chr_fops, cuyo único trabajo es hacer de trampolín:
/* fs/char_dev.c */
static const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};
static int chrdev_open(struct inode *inode, struct file *filp)
{
struct cdev *p = inode->i_cdev; /* el cdev del nivel 13 */
const struct file_operations *fops;
fops = fops_get(p->ops); /* TUS operaciones del driver */
replace_fops(filp, fops); /* filp->f_op = fops */
if (filp->f_op->open)
return filp->f_op->open(inode, filp); /* llama a tu mi_open */
return 0;
}
La línea decisiva es replace_fops(filp, fops): sustituye la f_op genérica del struct file por la tabla concreta de tu driver. A partir de ese instante, cada read sobre ese descriptor entra por filp->f_op->read, que es tu mi_read. El def_chr_fops es un arranque que existe solo para descubrir el cdev correcto —porque un mismo major agrupa muchos minors, cada uno con posiblemente otro driver— e intercambiar la vtable. El polimorfismo del VFS se resuelve en tiempo de apertura, no de registro.
chrdev_open llama a tu open, y ahí es donde sueles guardar en filp->private_data el puntero a la estructura de tu dispositivo (recuperada del cdev con container_of). En los siguientes read, write e ioctl de ese descriptor, filp->private_data te devuelve tu contexto sin búsquedas globales. Es el patrón canónico de todo driver de carácter.
Reúne los tres objetos y mira la gramática que forman. El inodo es el sustantivo: la cosa, el archivo que existe con independencia de que alguien lo mire. La dentry es el nombre propio con que lo invocas. Y el struct file es el verbo en gerundio: el estar leyéndolo, la acción en curso de un proceso concreto sobre esa cosa. Esta separación entre lo que una cosa es y lo que le está pasando ahora es lo que permite que mil procesos abran /etc/passwd a la vez sin pisarse: comparten un solo inodo —una sola copia de los datos y metadatos, un solo page cache— pero cada uno lleva su propio struct file con su propio f_pos, su propia posición en la lectura, sus propios flags. Fundir el offset en el inodo destruiría esa concurrencia; ponerlo en el descriptor de usuario impediría que dup y fork compartieran posición cuando deben. El struct file es exactamente la capa que faltaba: ni tan compartida como el inodo ni tan privada como el fd, sino el objeto de granularidad justa que captura una apertura. Y cuando entiendes que su f_op no está fijada por el inodo sino que puede ser reemplazada en el open —como hace chrdev_open con tu driver—, comprendes el último grado de libertad del VFS: no solo cada tipo de objeto elige sus operaciones, sino que cada apertura concreta puede elegir las suyas. Ahí es donde tu código del nivel 13 se enganchó, sin ceremonia, a la maquinaria universal de los archivos.
- Con
ls -l /proc/self/fd, observa cómo los descriptores de un proceso apuntan a archivos, tuberías y sockets, todos comostruct file. - Escribe un programa que abra el mismo archivo dos veces y demuestre que cada descriptor tiene su propio
f_pos; luego repítelo condupy muestra que comparten offset. - En tu char device del nivel 13, guarda un puntero en
filp->private_dataduranteopeny recupéralo enread. - En las fuentes, sigue
chrdev_openhastareplace_fopsy explica por qué laf_opdel inodo no es directamente la del driver. - Razona por qué
read_itery noreades la operación que implementan los sistemas de archivos que se apoyan en el page cache.