La lección anterior terminó con un cabo suelto deliberado. Sabemos que el inodo 1180934 es el fichero de lecturas del 31 de agosto: guarda su tamaño, su dueño, sus fechas y sus bloques. Y sabemos que no guarda su nombre. Pero entonces, cuando escribes cat /var/lib/meteora/lecturas/2026-08-31.dat, ¿quién convierte esa cadena de 43 caracteres en el número 1180934?
La respuesta es el directorio, y merece una lección entera por tres razones. La primera es que su diseño explica comportamientos que desconciertan a todo el mundo: por qué borrar un fichero se llama "desenlazar", por qué un fichero borrado sigue ocupando 17 GB, por qué un enlace simbólico se rompe y uno duro no, y por qué no puedes enlazar duro un directorio. La segunda es que la resolución de rutas es una de las operaciones más ejecutadas de todo el sistema —millones de veces por segundo en un servidor con carga— y entender su coste explica decisiones de diseño reales. Y la tercera es que la organización de los nombres es un problema clásico de estructuras de datos con consecuencias medibles: un directorio con 100.000 ficheros de lecturas se comporta de forma radicalmente distinta según cómo esté organizado por dentro.
Vamos a abrir un directorio y ver qué hay dentro, recorrer la evolución histórica de las organizaciones hasta llegar al árbol de UNIX, seguir paso a paso la conversión de una ruta en un inodo contando los accesos a disco, distinguir con precisión los dos tipos de enlace, medir el coste de buscar en directorios enormes, y situar las rutas de Meteora en el estándar FHS.
Contenido
- El directorio es un fichero que contiene pares (nombre, inodo)
- Evolución de las organizaciones de directorios
- Rutas absolutas y relativas,
.y.. - El directorio de trabajo:
chdir,getcwdy por qué es por proceso - Resolución de rutas paso a paso, con el coste contado
- La caché de dentries: por qué eso no cuesta lo que parece
- Enlaces duros: un inodo, varios nombres
- Enlaces simbólicos: un fichero que contiene una ruta
- Organización interna y el coste de los directorios enormes
- El estándar FHS y por qué Meteora está donde está
- Borrado: qué hace realmente
unlink
El directorio es un fichero que contiene pares (nombre, inodo)
Empecemos por la afirmación central, que suele sonar rara la primera vez:
Un directorio es un fichero normal cuyo contenido es una tabla de pares (nombre, número de inodo). Su única particularidad es que el tipo de su inodo es
d, y que el núcleo se reserva el derecho de escribirlo él mismo.
Tiene su inodo, su tamaño, sus permisos, sus fechas y sus bloques de datos, exactamente igual que 2026-08-31.dat. Lo que cambia es qué hay dentro de esos bloques.
Vamos a demostrarlo. Primero, el directorio tiene tamaño y bloques como cualquier fichero:
$ stat /var/lib/meteora/lecturas Fichero: /var/lib/meteora/lecturas Tamaño: 4096 Bloques: 8 Bloque E/S: 4096 directorio Dispositivo: 9,0 Nodo-i: 1180929 Enlaces: 3 Acceso: (0750/drwxr-x---) Uid: (990/meteora) Gid: (990/meteora)
4.096 bytes, 8 bloques de 512 B, inodo 1180929. Es un fichero. Ahora intentemos leerlo como tal:
Falla, y no porque no haya contenido, sino porque desde 1988 Linux prohíbe read() sobre un directorio: si cada programa interpretara el formato binario a mano, cambiar la estructura interna de ext4 rompería medio sistema. En su lugar hay una llamada al sistema específica, getdents64(), que devuelve las entradas en un formato independiente del sistema de archivos. Es la misma idea de indirección que veremos como VFS en Particiones, Montaje y Sistema de Archivos Virtual.
Con strace se ve que ls la usa:
$ strace -e trace=openat,getdents64 ls /var/lib/meteora/lecturas openat(AT_FDCWD, "/var/lib/meteora/lecturas", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3 getdents64(3, 0x5586a2f1b0d0 /* 8 entries */, 32768) = 240 getdents64(3, 0x5586a2f1b0d0 /* 0 entries */, 32768) = 0
ls abre el directorio con O_DIRECTORY, pide sus entradas con getdents64 —ocho de golpe, 240 bytes— y vuelve a llamar hasta recibir 0, que significa "no hay más". Todo ls, todo find y todo os.listdir() acaban aquí.
Para ver el contenido crudo hace falta debugfs:
$ sudo debugfs -R "ls -l /var/lib/meteora/lecturas" /dev/md0 1180929 40750 (2) 990 990 4096 1-Sep-2026 00:00 . 1180928 40750 (2) 990 990 4096 1-Sep-2026 00:00 .. 1180934 100640 (1) 990 990 17280000 31-Aug-2026 23:59 2026-08-31.dat 1180935 100640 (1) 990 990 17280000 1-Sep-2026 12:40 2026-09-01.dat 1180936 120777 (7) 990 990 14 1-Sep-2026 00:00 hoy.dat 1180940 40750 (2) 990 990 4096 1-Sep-2026 00:00 archivo
Ahí está la tabla: seis pares (nombre, inodo). La primera columna es el número de inodo y la última el nombre; entre medias, el modo en octal con el tipo delante (40 directorio, 100 regular, 120 enlace simbólico).
El formato real de una entrada en ext4, que es de una sencillez casi decepcionante:
struct ext4_dir_entry_2 {
__le32 inode; /* 4 B: número de inodo, 0 = entrada borrada */
__le16 rec_len; /* 2 B: longitud TOTAL de esta entrada */
__u8 name_len; /* 1 B: longitud del nombre (máx. 255) */
__u8 file_type; /* 1 B: 1=regular 2=dir 7=symlink... */
char name[]; /* el nombre, sin terminador nulo */
};Ocho bytes de cabecera más el nombre, redondeado a múltiplo de 4. La entrada de 2026-08-31.dat (14 caracteres) ocupa 8 + 14 = 22 → 24 bytes. Dos detalles con consecuencias:
rec_lenes la longitud total, no la del nombre. Recorrer el directorio consiste en avanzarrec_lenbytes desde cada entrada. Cuando se borra una entrada, el sistema no mueve nada: simplemente suma surec_lena la de la entrada anterior, que se la "traga". Por eso un directorio que tuvo un millón de ficheros y ahora tiene tres sigue ocupando megabytes: los huecos existen pero el fichero no encoge.file_typeduplica información que ya está en el inodo. Es una optimización deliberada: permite afind -type ffiltrar sin leer el inodo de cada entrada, ahorrando un acceso por fichero.
Y el detalle que más consecuencias tiene, ya visible arriba: el mismo número de inodo puede aparecer en varias entradas. Eso son los enlaces duros del apartado 7.
Evolución de las organizaciones de directorios
El árbol de UNIX no cayó del cielo. Es el cuarto intento de resolver el mismo problema, y conocer los tres anteriores explica por qué el actual es como es.
Nivel único. Todos los ficheros en un solo espacio de nombres plano; es lo que tenían los primeros sistemas por lotes y lo que tienen aún muchos dispositivos empotrados. Trivial de implementar, pero con un problema fatal: las colisiones de nombres. Si dos usuarios quieren un datos.dat, uno pierde.
Dos niveles. Un directorio por usuario colgando de un directorio maestro. Resuelve las colisiones y aporta aislamiento, pero es rígido: un usuario no puede organizar sus 400 ficheros en subgrupos, y compartir con otro es incómodo. Meteora no podría separar lecturas/ de archivo/.
Árbol jerárquico. Un directorio puede contener directorios, sin límite de profundidad. Es la estructura de UNIX y de Windows: organización natural por temas, nombres únicos garantizados por la ruta completa, y exactamente un camino desde la raíz hasta cada fichero. Su limitación es que no permite compartir de verdad: si 2026-08-31.dat también debe aparecer en /srv/publicacion/, la única opción es copiarlo —34 MB en lugar de 17, y divergencia en cuanto uno cambie—.
Grafo acíclico. El árbol más la posibilidad de que varios nombres apunten al mismo fichero, mientras no se formen ciclos. Es lo que UNIX consigue con los enlaces. Aporta compartición real, pero trae dos problemas nuevos: el recorrido puede visitar un fichero dos veces (find, du y tar llevan cuenta de los inodos ya vistos), y el borrado deja de ser obvio —si dos rutas son el mismo inodo, ¿borrar una destruye el contenido?—. La solución es el contador de enlaces del apartado 7.
Grafo general. Si además se permiten ciclos —un directorio que se contiene a sí mismo por un camino indirecto—, aparecen dos problemas graves:
- Recorridos infinitos. Un
findsin protección entraría en un bucle eterno. Habría que detectar ciclos en cada recorrido, con coste y memoria. - Recolección de basura. El contador de enlaces deja de servir. Si
A/contiene un enlace aB/yB/contiene un enlace aA/, y borramos ambos nombres desde fuera, los dos contadores siguen valiendo 1 —se sostienen mutuamente— pero nadie puede llegar a ellos. Ese espacio quedaría perdido para siempre salvo que se ejecutase un recolector de basura por alcanzabilidad, recorriendo el disco entero desde la raíz.
Y por eso UNIX toma una decisión tajante que ahora entiendes:
No se permiten enlaces duros a directorios. Solo
.y.., que crea el propio núcleo y son ciclos controlados y conocidos.
Con esa única prohibición, la estructura de directorios queda garantizada como acíclica, y el contador de enlaces basta como recolector de basura: nunca puede haber una isla inalcanzable con contador positivo. Es una restricción pequeña a cambio de eliminar dos clases enteras de problemas.
| Organización | Colisiones | Compartición | Ciclos | Borrado seguro | Usada hoy |
|---|---|---|---|---|---|
| Nivel único | Sí | No | No | Trivial | Empotrados simples |
| Dos niveles | No | Difícil | No | Trivial | Sistemas históricos |
| Árbol | No | No | No | Trivial | FAT (sin enlaces) |
| Grafo acíclico | No | Sí | No | Contador de enlaces | UNIX, Linux, NTFS |
| Grafo general | No | Sí | Sí | Necesita recolector | Ninguno serio |
Rutas absolutas y relativas, . y ..
Una ruta es la receta para llegar a un fichero recorriendo el grafo.
- Absoluta: empieza por
/y parte de la raíz./var/lib/meteora/lecturas/2026-08-31.dat. Significa lo mismo la ejecute quien la ejecute y desde donde la ejecute. - Relativa: no empieza por
/y parte del directorio de trabajo actual del proceso.lecturas/2026-08-31.datsignifica cosas distintas según dónde estés.
Todo directorio contiene siempre dos entradas que crea el núcleo al crearlo:
| Entrada | Apunta a | Para qué sirve |
|---|---|---|
. |
Su propio inodo | Referirse al directorio actual: ./programa, cp fichero . |
.. |
El inodo de su padre | Subir un nivel: cd .., ../config/ |
En la raíz hay una excepción elegante: /.. apunta a /. La raíz es su propio padre, así que cd /../../../.. te deja en / sin error. Es lo que impide salirse del árbol por arriba.
Estas dos entradas explican el contador de enlaces de los directorios, que confunde a mucha gente:
¿Por qué 3? Porque tres nombres apuntan a ese inodo:
lecturas, la entrada en el directorio padre/var/lib/meteora/.., dentro del propiolecturas/..., dentro de su único subdirectorio,archivo/.
De ahí la regla: el contador de enlaces de un directorio es 2 + el número de subdirectorios. Un truco práctico que sale de aquí: para saber cuántos subdirectorios tiene un directorio sin recorrerlo, basta con stat -c %h y restar 2. Con un directorio de 100.000 entradas, eso es instantáneo frente a los segundos que tardaría ls -l | grep ^d.
El directorio de trabajo: chdir, getcwd y por qué es por proceso
Cada proceso tiene su directorio de trabajo actual (current working directory, cwd), guardado en su task_struct —la estructura que diseccionamos en Gestión de Procesos—. Es un puntero a un inodo de directorio, no una cadena de texto.
Se ve en /proc, como todo lo demás:
$ ls -l /proc/2841/cwd lrwxrwxrwx 1 meteora meteora 0 sep 1 12:44 /proc/2841/cwd -> /var/lib/meteora
Y se manipula con dos llamadas al sistema:
#include <unistd.h>
char buf[PATH_MAX];
if (chdir("/var/lib/meteora/lecturas") == -1) /* cambia el cwd del proceso */
perror("chdir");
if (getcwd(buf, sizeof buf) == NULL) /* reconstruye la ruta actual */
perror("getcwd");
printf("Trabajando en %s\n", buf); /* → /var/lib/meteora/lecturas */chdir() cambia el puntero de inodo del proceso —requiere permiso de ejecución en el destino, como veremos en Seguridad y Permisos— y getcwd() hace lo contrario: parte del inodo del cwd y sube por .. hasta la raíz, buscando en cada padre el nombre que corresponde al inodo del hijo, y compone la cadena al revés. Por eso getcwd() es sorprendentemente cara y por eso puede fallar con ENOENT si alguien ha borrado un directorio de la ruta mientras estabas dentro.
Por qué el cwd es por proceso y no global merece respuesta explícita. Si fuera global, dos procesos que hicieran cd se pisarían: meteo-api cambiaría de directorio y el agregador escribiría en el sitio equivocado, una condición de carrera de manual (03-01) sobre estado compartido. Al ser por proceso, se hereda en el fork() —el hijo empieza donde estaba el padre, y por eso cd /tmp && ls funciona como esperas— y se explica el desconcierto clásico: un script que hace cd /var/lib/meteora no cambia el directorio de tu shell, porque corre en un proceso hijo cuyo cwd muere con él. Para que afecte a la shell hay que ejecutarlo con source o ..
Un detalle práctico importante para servicios: un demonio que mantiene su cwd en un directorio impide desmontar ese sistema de archivos (el "target is busy" de 04-03). Por eso los demonios bien escritos hacen chdir("/") al arrancar, y por eso systemd ofrece WorkingDirectory=.
Resolución de rutas paso a paso, con el coste contado
Esta es la operación central de la lección. Cuando meteo-api ejecuta:
el núcleo tiene que convertir 43 caracteres en el inodo 1180934. El algoritmo, llamado resolución de rutas (path resolution), es un bucle:
graph TB
A["Ruta: /var/lib/meteora/lecturas/2026-08-31.dat"] --> B{"¿Empieza por /?"}
B -->|Sí| C["inodo actual = inodo de la raíz (nº 2)"]
B -->|No| D["inodo actual = inodo del cwd del proceso"]
C --> E["Tomar el siguiente componente"]
D --> E
E --> F{"¿Es un directorio?<br/>¿Hay permiso x?"}
F -->|No| G["Error: ENOTDIR o EACCES"]
F -->|Sí| H["Buscar el nombre en sus entradas"]
H --> I{"¿Encontrado?"}
I -->|No| J["Error: ENOENT"]
I -->|Sí| K["inodo actual = el inodo hallado"]
K --> L{"¿Es enlace simbólico?"}
L -->|Sí| M["Sustituir por su destino<br/>y reiniciar (máx. 40 veces)"]
M --> E
L -->|No| N{"¿Quedan componentes?"}
N -->|Sí| E
N -->|No| O["Devolver el inodo final"]
Ejecutado sobre nuestra ruta, con la cuenta de accesos suponiendo cachés vacías:
| Paso | Componente | Qué hace el núcleo | Accesos a disco |
|---|---|---|---|
| 0 | / |
Coge el inodo 2 (la raíz siempre es el inodo 2 en ext4) | 1 (inodo) |
| 1 | var |
Lee los bloques de datos de /, busca var → inodo 131073 |
1 (datos) + 1 (inodo) |
| 2 | lib |
Lee los datos de /var, busca lib → inodo 262145 |
1 + 1 |
| 3 | meteora |
Lee los datos de /var/lib, busca meteora → inodo 1180928 |
1 + 1 |
| 4 | lecturas |
Lee los datos de /var/lib/meteora → inodo 1180929 |
1 + 1 |
| 5 | 2026-08-31.dat |
Lee los datos de lecturas, busca el nombre → inodo 1180934 |
1 + 1 |
| 6 | — | Lee el inodo final para comprobar permisos y tipo | (ya contado) |
| Total | 11 accesos |
Once accesos a disco para abrir un fichero. En un NVMe, con 80 µs cada uno, son unos 0,9 ms; en el HDD de 02-05, con 8 ms de latencia media, serían 88 milisegundos. Y todo eso antes de leer un solo byte de datos.
Tres consecuencias directas que vale la pena interiorizar:
- Cada componente de la ruta cuesta.
/a/b/c/d/e/f/g/fichero.dates más caro de abrir que/datos/fichero.dat. Rutas absurdamente profundas tienen un coste real, aunque pequeño. - El permiso de ejecución se comprueba en cada directorio de la ruta, no solo en el fichero final. Es el tema de 04-06, pero el mecanismo es este bucle.
- Abrir una vez y reutilizar el descriptor es mucho más barato que abrir el fichero en cada operación. Es la razón por la que el
ingestorabre2026-08-31.datal empezar el día y lo mantiene abierto, en lugar de abrirlo y cerrarlo en cada lectura recibida.
Si algún componente intermedio es un enlace simbólico, se sustituye por su destino y el bucle se reinicia. Linux limita a 40 las resoluciones encadenadas; al superarlo devuelve ELOOP ("Demasiados niveles de enlaces simbólicos"), que es la protección contra un enlace que apunta a sí mismo.
La caché de dentries: por qué eso no cuesta lo que parece
Once accesos por open() sería inaceptable: un servidor abre miles de ficheros por segundo y la mayoría comparten los primeros componentes de la ruta. La solución del núcleo es la caché de dentries (dentry cache o dcache).
Una dentry (directory entry) es una estructura en memoria que asocia (inodo del directorio padre, nombre) → inodo hijo. La dcache es una tabla hash de dentries, con la ruta parcial como clave. Su efecto es demoledor: si /var/lib/meteora/lecturas está en caché —y lo estará, porque se usa constantemente—, la resolución de nuestra ruta pasa de 11 accesos a disco a uno, el del inodo final, y a menudo a cero si también está cacheado.
$ sudo slabtop -o | grep -E 'dentry|inode_cache' OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME 418320 411927 98% 0,19K 19920 21 79680K dentry 96432 94215 97% 0,58K 3444 28 55104K ext4_inode_cache
En este meteo-01 hay 418.320 dentries cacheadas ocupando 78 MiB, con un 98 % activas. Ese consumo de RAM es lo que hace que el sistema de archivos parezca rápido, y es también parte de lo que free cuenta como memoria "disponible": son cachés reclamables, exactamente como las páginas de 02-04.
Dos detalles que se ven en la práctica:
Dentries negativas. La caché guarda también los "no existe": si un intérprete busca un módulo por una lista de veinte rutas, la primera consulta de cada una cuesta accesos a disco y las siguientes devuelven ENOENT desde memoria.
Se puede vaciar para medir. Para comparar el coste real con y sin caché:
sync # volcar escrituras pendientes
echo 3 | sudo tee /proc/sys/vm/drop_caches # 3 = dentries + inodos + páginas
time cat /var/lib/meteora/lecturas/2026-08-31.dat > /dev/null # frío
time cat /var/lib/meteora/lecturas/2026-08-31.dat > /dev/null # calienteLos valores de drop_caches son 1 (caché de páginas), 2 (dentries e inodos) y 3 (ambos). Solo en pruebas: en producción vaciar las cachés provoca un pico de latencia mientras todo se vuelve a leer. La diferencia entre la ejecución fría y la caliente suele ser de uno a dos órdenes de magnitud, y es la medida directa del valor de la dcache.
Enlaces duros: un inodo, varios nombres
Un enlace duro es simplemente otra entrada de directorio que apunta al mismo número de inodo. No es una copia ni una referencia especial: es un nombre más, con exactamente la misma categoría que el original.
$ cd /var/lib/meteora/lecturas $ ls -li 2026-08-31.dat 1180934 -rw-r----- 1 meteora meteora 17280000 ago 31 23:59 2026-08-31.dat $ ln 2026-08-31.dat /var/lib/meteora/archivo/agosto-final.dat $ ls -li 2026-08-31.dat /var/lib/meteora/archivo/agosto-final.dat 1180934 -rw-r----- 2 meteora meteora 17280000 ago 31 23:59 2026-08-31.dat 1180934 -rw-r----- 2 meteora meteora 17280000 ago 31 23:59 .../agosto-final.dat
Fíjate en las dos cosas que han cambiado: el mismo inodo 1180934 en los dos, y el contador de enlaces ha pasado de 1 a 2. Y lo que no ha cambiado: el espacio en disco.
Los 17 MB no se han duplicado porque no hay dos ficheros: hay un fichero con dos nombres. Si escribes por uno, lo ves por el otro, porque es el mismo inodo, los mismos bloques y los mismos bytes.
Ahora, borrar uno:
$ rm 2026-08-31.dat $ ls -li /var/lib/meteora/archivo/agosto-final.dat 1180934 -rw-r----- 1 meteora meteora 17280000 ago 31 23:59 agosto-final.dat
El fichero sigue ahí, con el contador de vuelta a 1. rm no destruye nada: quita un nombre y decrementa el contador. Solo cuando el contador llega a 0 el sistema libera los bloques.
Las dos restricciones de los enlaces duros, con su porqué:
No pueden cruzar sistemas de archivos. Un enlace duro es un número de inodo, y los números de inodo son únicos solo dentro de su sistema de archivos (04-01). El inodo 1180934 existe en /dev/md0 y probablemente también en /dev/sda1, siendo cosas distintas. Una entrada de directorio no tiene espacio para decir "de qué dispositivo":
$ ln /var/lib/meteora/lecturas/2026-09-01.dat /tmp/prueba.dat ln: no se puede crear el enlace duro '/tmp/prueba.dat' => ...: Enlace cruzado no válido
El error EXDEV ("Invalid cross-device link") es el mismo que hace que mv entre discos distintos tenga que copiar y borrar en lugar de renombrar, con la diferencia de rendimiento que eso supone: mover 17 GB dentro del mismo sistema de archivos es instantáneo; entre dos, tarda minutos.
No pueden enlazar directorios. Es la prohibición del apartado 2: garantiza que el grafo sea acíclico y que el contador de enlaces baste como recolector de basura.
$ sudo ln /var/lib/meteora/lecturas /var/lib/meteora/atajo ln: /var/lib/meteora/lecturas: Operación no permitida
Enlaces simbólicos: un fichero que contiene una ruta
Un enlace simbólico es un fichero de tipo l cuyo contenido es una cadena de texto con una ruta. Nada más. Cuando el núcleo lo encuentra durante la resolución, sustituye el enlace por esa cadena y continúa.
$ ln -s 2026-09-01.dat hoy.dat $ ls -li hoy.dat 1180936 lrwxrwxrwx 1 meteora meteora 14 sep 1 00:00 hoy.dat -> 2026-09-01.dat
Tres observaciones cargadas de información: tiene inodo propio (1180936), luego es un fichero distinto; su tamaño es 14, exactamente los caracteres de 2026-09-01.dat, que son todo su contenido; y sus permisos lrwxrwxrwx son siempre 777 y no significan nada, porque los que se aplican son los del destino.
Un detalle de implementación bonito: ext4 aprovecha los 60 bytes que el inodo reserva para punteros de bloques y, si la ruta ocupa menos de 60 caracteres, la guarda ahí mismo. Se llama fast symlink y no consume ningún bloque de datos: resolverlo no cuesta ni un acceso extra. Casi todos los enlaces simbólicos reales caben.
La diferencia crucial con el enlace duro es que el simbólico apunta a un nombre, no a un inodo. De ahí sus dos propiedades características:
Puede cruzar sistemas de archivos y enlazar directorios, porque una cadena de texto no tiene esas limitaciones:
Puede quedarse roto. Si el destino desaparece o se renombra, el enlace sigue existiendo y apuntando a una ruta que ya no resuelve:
$ rm 2026-09-01.dat $ ls -l hoy.dat lrwxrwxrwx 1 meteora meteora 14 sep 1 00:00 hoy.dat -> 2026-09-01.dat $ cat hoy.dat cat: hoy.dat: No existe el fichero o el directorio
El enlace está perfectamente sano; lo que falta es el destino. Los enlaces rotos se localizan con find /var/lib/meteora -xtype l.
Y una trampa que muerde a todo el mundo: los enlaces relativos se resuelven respecto al directorio del enlace, no respecto a quien lo usa.
$ ln -s 2026-09-01.dat /tmp/hoy.dat # ¡MAL! $ cat /tmp/hoy.dat cat: /tmp/hoy.dat: No existe el fichero o el directorio
/tmp/hoy.dat apunta a 2026-09-01.dat, que el núcleo busca en /tmp/. Regla práctica: usa rutas absolutas en los enlaces salvo que enlace y destino estén en el mismo directorio o quieras que el conjunto sea movible.
La tabla comparativa completa:
| Aspecto | Enlace duro | Enlace simbólico |
|---|---|---|
| Qué es | Otra entrada con el mismo inodo | Fichero con una ruta dentro |
| Inodo | El mismo que el original | Propio y distinto |
Tipo en ls -l |
- (indistinguible) |
l |
| Contador de enlaces del original | Aumenta | No cambia |
| Cruza sistemas de archivos | No (EXDEV) |
Sí |
| Enlaza directorios | No | Sí |
| Sobrevive al borrado del original | Sí (el contenido sigue) | No (queda roto) |
| Sobrevive a renombrar el original | Sí | No |
| Espacio ocupado | 24 bytes en el directorio | Un inodo (+0 bloques si < 60 B) |
| Coste al resolver | Ninguno | Una resolución extra |
| ¿Cuál es el "original"? | Ninguno: son idénticos | El destino, claramente |
| Comando | ln origen nuevo |
ln -s origen nuevo |
| Uso típico | Copias de seguridad incrementales, deduplicación | Versiones (hoy.dat), /usr/bin, bibliotecas |
La fila de "¿cuál es el original?" es la que más cuesta aceptar: tras ln a b, no hay forma de saber cuál se creó primero. Las dos entradas son igual de legítimas; el inodo no guarda ninguna preferencia.
Meteora usa los dos, y cada uno donde toca: un simbólico hoy.dat que se reapunta cada medianoche al fichero del día (los clientes siempre piden hoy.dat y el enlace se rehace con ln -sfn, que es atómico), y enlaces duros en las copias de seguridad diarias, donde los ficheros que no han cambiado se enlazan en lugar de copiarse —es exactamente lo que hace rsync --link-dest y por lo que treinta copias diarias de 17 GB pueden ocupar 18 GB en total.
Organización interna y el coste de los directorios enormes
Hasta ahora hemos dicho que un directorio "contiene una tabla". ¿Cómo está organizada esa tabla? Es una decisión de estructuras de datos con efectos medibles.
Lista lineal. Las entradas, una detrás de otra, en el orden en que se crearon. Buscar un nombre significa recorrer desde el principio comparando cadenas.
- Buscar: O(n). Crear: O(n), porque hay que comprobar que el nombre no existe.
- Es lo que hacía ext2 y lo que sigue haciendo ext4 en directorios pequeños.
Hagamos el cálculo con un caso realista. Supón que Meteora, en lugar de un fichero por día, hubiera guardado un fichero por cada diez minutos durante dos años: 105.120 ficheros en lecturas/. Con entradas de 24 bytes de media:
- Tamaño del directorio: 105.120 × 24 ≈ 2,5 MB, es decir 616 bloques de 4 KiB.
- Buscar un nombre: leer una media de 308 bloques y comparar 52.560 cadenas.
- Y lo peor: crear un fichero nuevo obliga a recorrer los 616 bloques enteros para verificar que el nombre no existe.
El resultado es la patología clásica: ls tarda segundos, y crear los ficheros se vuelve cada vez más lento a medida que el directorio crece. Es un comportamiento cuadrático en el conjunto: crear n ficheros cuesta O(n²).
Tabla hash. Se aplica una función hash al nombre y se salta directamente al cajón que le corresponde.
- Buscar: O(1) en el caso medio. Es lo que usan ext4 (
dir_index) y NTFS con variantes. - Inconveniente: el recorrido completo devuelve las entradas en orden de hash, que parece aleatorio.
Árbol B / B+. Las entradas se mantienen ordenadas en un árbol equilibrado.
- Buscar: O(log n), y además el recorrido sale ordenado y los rangos son eficientes.
- Es lo que usan XFS, Btrfs y NTFS.
En ext4, la solución concreta se llama htree (hashed tree) y se activa con la característica dir_index. Es un híbrido: aplica una variante de MD4 al nombre y usa el hash como clave de un árbol de uno o dos niveles, cada uno con 4 KiB de índice. Con dos niveles direcciona del orden de millones de entradas con como máximo tres accesos: nodo raíz, nodo hoja y bloque de datos.
La comparación, sobre el directorio hipotético de 105.120 ficheros:
| Organización | Buscar un nombre | Crear un fichero | Listar todo | Orden de salida |
|---|---|---|---|---|
| Lista lineal | 308 bloques (media) | 616 bloques | 616 bloques | Creación |
| htree (ext4) | 3 bloques | 3 bloques | 616 bloques | Hash (aparente azar) |
| Árbol B+ (XFS) | ~3 bloques | ~3 bloques | 616 bloques | Alfabético |
De 308 a 3 accesos: cien veces menos. Se comprueba si está activo con:
$ sudo tune2fs -l /dev/md0 | grep features Filesystem features: has_journal ext_attr dir_index extent 64bit metadata_csum
dir_index está ahí, y en cualquier ext4 creado en los últimos quince años lo estará. Si por lo que sea no lo estuviera, se activa con tune2fs -O dir_index /dev/md0 seguido de e2fsck -D para reconstruir los índices.
Dos consecuencias prácticas del htree que conviene conocer. La primera es que ls sale desordenado y ordenarlo cuesta: el recorrido devuelve las entradas en orden de hash y ls las ordena en memoria antes de imprimir nada, así que con 105.120 entradas ls -f o find . -maxdepth 1 son mucho más rápidos. La segunda es que listar en orden de hash destroza la localidad de los inodos: procesarlos en el orden que devuelve readdir() significa leerlos en orden aleatorio, lo que en un HDD son búsquedas continuas. La solución clásica —y lo que hacen tar y varias herramientas de copia de seguridad— es leer todos los nombres, ordenarlos por número de inodo y procesarlos así.
Y la conclusión de diseño, que es la que importa: incluso con htree, un directorio con cien mil ficheros es una mala idea. El listado sigue siendo O(n), las copias de seguridad se ralentizan, ls con * puede desbordar la línea de comandos, y cualquier herramienta que ordene consume memoria. La práctica correcta es repartir en subdirectorios, que es exactamente lo que hace Meteora con su archivo histórico:
/var/lib/meteora/lecturas/2026-08-31.dat ← el día en curso y los recientes /var/lib/meteora/archivo/2026/08/2026-08-01.dat ← histórico por año/mes
Con esa jerarquía, ningún directorio pasa de 31 entradas. Es el mismo patrón que usan Git para sus objetos (ab/cdef...) y los cachés web de todo el mundo.
El estándar FHS y por qué Meteora está donde está
El Filesystem Hierarchy Standard (FHS) es la convención que dice qué va en cada directorio de la raíz en Linux. No lo impone el núcleo: es un acuerdo que hace que un administrador sepa dónde mirar en cualquier distribución.
| Directorio | Qué contiene | ¿Persistente? | ¿Compartible? |
|---|---|---|---|
/bin, /sbin |
Binarios esenciales del sistema (hoy, enlaces a /usr) |
Sí | Sí |
/boot |
Núcleo, initramfs y cargador de arranque | Sí | No |
/dev |
Ficheros de dispositivo (devtmpfs, 04-03) | No | No |
/etc |
Configuración del sistema, específica de esta máquina | Sí | No |
/home |
Directorios personales de los usuarios | Sí | Sí |
/lib |
Bibliotecas compartidas esenciales | Sí | Sí |
/mnt, /media |
Puntos de montaje temporales y de medios extraíbles | — | — |
/opt |
Software de terceros autocontenido | Sí | Sí |
/proc |
Información de procesos y del núcleo (procfs, 04-03) | No | No |
/root |
Directorio personal del superusuario | Sí | No |
/run |
Estado en ejecución: PID, sockets, FIFO (tmpfs) | No | No |
/srv |
Datos servidos por este sistema (web, ftp) | Sí | Sí |
/sys |
Interfaz con el modelo de dispositivos (sysfs, 04-03) | No | No |
/tmp |
Temporales de cualquier usuario, se vacía al arrancar | No | No |
/usr |
Programas y datos de solo lectura, no esenciales | Sí | Sí |
/var |
Datos variables: logs, colas, cachés, bases de datos | Sí | No |
La lógica de fondo son dos ejes: variable frente a estático (/var cambia constantemente, /usr solo al actualizar) y compartible frente a específico (/usr podría montarse por red y compartirse entre máquinas, /etc no).
Con eso, las rutas de Meteora dejan de ser arbitrarias:
| Ruta de Meteora | Por qué ahí |
|---|---|
/var/lib/meteora/lecturas/ |
/var/lib es el lugar canónico de los datos de estado de una aplicación: información que la app crea, modifica y necesita conservar entre reinicios. No es /srv porque no es contenido servido tal cual, ni /opt porque eso es para el software, no para sus datos |
/etc/meteora/meteora.conf |
/etc es configuración específica de esta máquina, editable por el administrador y que debe conservarse en las copias de seguridad y controlarse por versiones |
/var/log/meteora/meteo-api.log |
/var/log es el destino estándar de los registros, con rotación gestionada por logrotate |
/run/meteora/lecturas.fifo |
/run es tmpfs: se vacía al arrancar. Perfecto para una FIFO y un socket, que no deben sobrevivir a un reinicio: un socket huérfano de una ejecución anterior impediría arrancar el servicio |
/dev/shm/meteora-cache |
tmpfs para memoria compartida POSIX (03-03). Volátil por definición: es una caché |
/tmp/ |
Solo para temporales de vida corta, con mkstemp (04-04). Nunca para datos que deban durar |
La razón por la que /run y /dev/shm son tmpfs, y /var/lib no, resume el capítulo entero: la ubicación de un fichero declara sus garantías de persistencia. Poner el socket en /var/run (que hoy es un enlace simbólico a /run) o las lecturas en /tmp no son cuestiones de gusto: cambian lo que ocurre tras un reinicio.
Borrado: qué hace realmente unlink
Llegamos al comportamiento que más desconcierta, y que ahora tiene una explicación completa. La llamada al sistema que hay detrás de rm no se llama delete:
Sus pasos exactos:
- Resolver la ruta hasta el directorio padre y comprobar el permiso de escritura en el directorio (no en el fichero: 04-06).
- Eliminar la entrada (nombre, inodo) del directorio, absorbiendo su
rec_lenen la anterior. - Decrementar el contador de enlaces del inodo.
- Si el contador llega a 0 y ningún proceso tiene el fichero abierto: marcar el inodo como libre en su mapa de bits y liberar todos sus bloques.
- Si el contador llega a 0 pero algún proceso lo tiene abierto: no liberar nada. El inodo pasa a una lista de huérfanos y se liberará cuando se cierre el último descriptor.
El paso 5 es el que produce el incidente clásico. Alguien ve que /var/log/meteora/meteo-api.log ha crecido hasta 17 GB, lo borra con rm, y el espacio no aparece:
$ ls -l /var/log/meteora/ total 0 ← el fichero ya no está $ df -h /var/log S.ficheros Tamaño Usados Disp Uso% Montado en /dev/sda3 20G 19G 340M 99% /var/log ← ¡sigue lleno!
El fichero ya no tiene nombre, pero meteo-api lo tiene abierto. El contador de enlaces vale 0, pero el contador de descriptores abiertos vale 1, así que los bloques siguen reservados y —peor aún— el proceso sigue escribiendo en un fichero que ya nadie puede abrir.
El diagnóstico:
$ sudo lsof +L1 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME meteo-api 2841 meteora 5w REG 8,3 18253611008 0 4457 /var/log/meteora/meteo-api.log (deleted)
+L1 filtra los ficheros abiertos con menos de un enlace, es decir, los borrados. La columna NLINK vale 0 y el nombre lleva (deleted). Ahí está el culpable.
Las tres soluciones, de mejor a peor:
# 1. Recargar el servicio: cierra y reabre sus ficheros (03-03, SIGHUP)
sudo systemctl reload meteo-api # o kill -HUP 2841
# 2. Si no soporta recarga, reiniciarlo
sudo systemctl restart meteo-api
# 3. Vaciar el fichero SIN borrarlo (a través de /proc, sin reiniciar nada)
sudo truncate -s 0 /proc/2841/fd/5La tercera es un truco excelente: /proc/<pid>/fd/5 es un enlace al inodo huérfano, así que se puede acceder a él aunque no tenga nombre, y truncarlo a cero libera los bloques al instante con el servicio en marcha. Es el mismo /proc/<pid>/fd que exploraremos en Gestión de Archivos.
Y la lección de fondo: la forma correcta de vaciar un log es truncate -s 0 o : > fichero, nunca rm. Es exactamente lo que hace logrotate con la opción copytruncate.
Un corolario feliz del mismo mecanismo: borrar un fichero abierto es una técnica legítima y muy usada. Un programa puede crear un temporal, abrirlo, borrarlo inmediatamente y seguir usándolo por el descriptor. El fichero no tiene nombre, así que nadie más puede tocarlo, y el sistema lo libera solo cuando el proceso muere, incluso si muere de un kill -9. Lo veremos con mkstemp en 04-04.
Errores Comunes y Consejos
Creer que rm borra el fichero. Borra un nombre. Si hay otro enlace duro, el contenido sigue; si un proceso lo tiene abierto, el espacio sigue ocupado. rm es unlink, y el nombre de la llamada es literal.
Buscar espacio libre con du cuando df dice que está lleno. du recorre nombres, así que no ve los ficheros borrados que siguen abiertos. Si df y du discrepan en gigabytes, ejecuta lsof +L1.
Usar rutas relativas en enlaces simbólicos sin pensar. ln -s datos.dat /otro/sitio/enlace crea un enlace que busca datos.dat en /otro/sitio/. Usa rutas absolutas salvo que sepas exactamente lo que haces.
Confundir el contador de enlaces de un directorio con "cuántos ficheros tiene". Es 2 + número de subdirectorios; los ficheros regulares no cuentan.
Meter cien mil ficheros en un directorio. Aunque dir_index salva la búsqueda, el listado, el ordenado, las copias de seguridad y los comodines de la shell siguen siendo O(n) o peor. Y un directorio no encoge al borrar: uno que llegó a 2,5 MB seguirá tardando lo mismo en listarse aunque quede vacío, y la única cura es recrearlo. Reparte en subdirectorios desde el principio: migrarlo después es doloroso.
Dejar el cwd de un demonio dentro de un volumen que quieres desmontar. Es una causa habitual de "target is busy" (04-03). Los servicios deberían hacer chdir("/").
Consejo: usa ls -f o find -maxdepth 1 en directorios grandes, que evitan el ordenado en memoria, y stat -c '%i %h %n' para ver inodo, contador de enlaces y nombre de un vistazo.
Consejo: cuando dudes de si dos rutas son el mismo fichero, compara st_dev y st_ino, con stat -c '%d %i' o directamente find / -samefile ruta. Comparar contenidos es lento y comparar nombres no dice nada.
Ejercicios
Ejercicio 1: demostrar que el nombre no está en el inodo
Crea un fichero, hazle un enlace duro y uno simbólico, y diseña una secuencia de comandos que demuestre las cinco afirmaciones siguientes, mostrando la salida que lo prueba en cada caso: (a) el enlace duro y el original comparten inodo y el simbólico no; (b) crear el enlace duro no consume espacio de datos; (c) borrar el original no destruye el contenido si hay enlace duro; (d) borrar el original deja roto el enlace simbólico; (e) el contador de enlaces refleja exactamente el número de nombres. Explica en cada punto qué campo del inodo lo justifica.
Ejercicio 2: el coste de la resolución de rutas
Calcula el número de accesos a disco necesario para abrir /var/lib/meteora/archivo/2026/08/2026-08-15.dat con las cachés vacías, detallando los pasos. Después, suponiendo que /var/lib/meteora/archivo/2026/08/ ya está en la caché de dentries, recalcula. Con una latencia de 80 µs por acceso en NVMe y de 8 ms en HDD, da los cuatro tiempos. Finalmente, explica qué cambia si archivo es un enlace simbólico a /mnt/historico/meteora y cuántos accesos añade.
Ejercicio 3: el fichero borrado que no libera espacio
Reproduce el incidente completo: escribe un programa (en shell o en C) que abra un fichero, escriba en él continuamente y no lo cierre; en otra terminal, borra el fichero y comprueba que df no baja. Diagnostícalo con lsof y du frente a df, y resuélvelo sin matar el proceso. Después explica por qué logrotate con copytruncate existe, y qué habría pasado si en lugar de rm se hubiera usado truncate -s 0.
Soluciones
Solución 1
cd /tmp && mkdir demo && cd demo
dd if=/dev/urandom of=original.dat bs=1M count=10 status=none
df --output=avail /tmp | tail -1 # espacio libre ANTES: p.ej. 8123456
ln original.dat duro.dat
ln -s original.dat blando.dat
df --output=avail /tmp | tail -1 # espacio libre DESPUÉS: idéntico
ls -liSalida esperada:
264531 -rw-r--r-- 2 joan joan 10485760 sep 1 13:02 duro.dat 264531 -rw-r--r-- 2 joan joan 10485760 sep 1 13:02 original.dat 264532 lrwxrwxrwx 1 joan joan 12 sep 1 13:02 blando.dat -> original.dat
(a) original.dat y duro.dat comparten el inodo 264531; blando.dat tiene el 264532, propio. El enlace duro es una entrada de directorio más apuntando al mismo inodo; el simbólico es un fichero distinto cuyo contenido son los 12 caracteres original.dat.
(b) El espacio libre no ha cambiado tras ln: un enlace duro añade 24 bytes a un bloque del directorio ya reservado, sin tocar la zona de datos. El campo del inodo que lo justifica es que los punteros a bloques no se han duplicado: siguen siendo los mismos.
(c) y (e):
rm original.dat
ls -li duro.dat # → 264531 -rw-r--r-- 1 ... duro.dat
md5sum duro.dat # el contenido íntegro sigue ahíEl contador de enlaces pasó de 2 a 1. unlink decrementó el contador; como no llegó a 0, los bloques no se liberaron. El campo es i_nlink.
(d):
ls -l blando.dat # el enlace sigue existiendo, con su tamaño 12
cat blando.dat # cat: blando.dat: No existe el fichero o el directorio
find . -xtype l # ./blando.datEl enlace simbólico guarda el nombre original.dat, no el inodo. Al desaparecer ese nombre del directorio, la resolución falla con ENOENT en el último componente. Curiosamente, ln -s duro.dat blando2.dat funcionaría, porque el contenido sigue accesible por ese otro nombre: prueba de que el simbólico depende del nombre y el duro del inodo.
Solución 2
La ruta tiene 6 componentes: var, lib, meteora, archivo, 2026, 08, 2026-08-15.dat. En realidad son 7.
Cachés vacías. Un acceso para el inodo de la raíz, y por cada componente uno para leer los datos del directorio y otro para leer el inodo hallado:
1 (inodo raíz) + 7 × 2 = 15 accesos.
Con /var/lib/meteora/archivo/2026/08/ en la dcache, la resolución arranca directamente desde el inodo de 08/ (que la caché tiene), y solo queda buscar el último componente: 1 (datos de 08/) + 1 (inodo final) = 2 accesos. Y si el inodo final también estuviera cacheado, 0.
| Escenario | Accesos | NVMe (80 µs) | HDD (8 ms) |
|---|---|---|---|
| Cachés vacías | 15 | 1,2 ms | 120 ms |
| Dentries cacheadas | 2 | 0,16 ms | 16 ms |
Los 120 ms del HDD frío son la razón de que el primer arranque de un servicio con muchos ficheros sea tan lento y el segundo casi instantáneo, y una justificación numérica de la dcache.
Con archivo como enlace simbólico a /mnt/historico/meteora: al llegar a archivo, el núcleo lee su inodo, detecta el tipo l, y —si la ruta ocupa menos de 60 bytes, como aquí (24)— la lee del propio inodo, sin acceso extra. Después reinicia la resolución con la nueva ruta absoluta: raíz, mnt, historico, meteora, y luego 2026, 08 y el fichero.
Recuento en frío: 1 (raíz) + 2×3 (var, lib, meteora) + 2 (archivo, cuyo inodo ya trae el destino) + 1 (raíz otra vez, seguramente cacheada) + 2×3 (mnt, historico, meteora) + 2×3 (2026, 08, fichero) = ~22 accesos, unos 7 más. Si el enlace midiera 60 bytes o más, habría que leer además su bloque de datos: un acceso adicional por cada enlace atravesado. Es poco, pero explica por qué las cadenas largas de enlaces simbólicos en rutas críticas se notan.
Solución 3
Reproducción:
# Terminal 1
( while :; do dd if=/dev/zero bs=1M count=10 status=none; sleep 1; done ) > /tmp/gordo.log &
echo $! # anota el PID, p.ej. 5512
# Terminal 2
sleep 30
df -h /tmp | tail -1 # el uso sube
rm /tmp/gordo.log
ls -l /tmp/gordo.log # No existe
df -h /tmp | tail -1 # ¡el uso NO baja, y sigue subiendo!
du -sh /tmp # du muestra mucho menos que df: la discrepanciaDiagnóstico:
sudo lsof +L1 /tmp
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
# dd 5513 joan 1w REG 0,25 943718400 0 312 /tmp/gordo.log (deleted)NLINK 0 y (deleted): el inodo 312 no tiene ningún nombre pero sigue vivo porque el descriptor 1 del proceso 5513 lo mantiene abierto. unlink completó los pasos 1-3 y se detuvo en el 5.
Resolución sin matar el proceso:
/proc/<pid>/fd/1 da acceso al inodo huérfano a través del descriptor abierto. Truncarlo a 0 libera todos sus bloques al instante. El proceso sigue escribiendo —en un fichero que ahora empieza de cero—, sin haberse enterado.
Por qué existe copytruncate. Cuando logrotate renombra un log y crea uno nuevo, un servicio que mantenga el descriptor abierto seguirá escribiendo en el fichero renombrado, porque el descriptor apunta al inodo, no al nombre (esa es toda la lección). Hay dos soluciones: enviar SIGHUP al servicio para que reabra el fichero por su nombre —lo elegante, y lo que hace Meteora desde 03-03—, o usar copytruncate, que copia el contenido y trunca el original a cero sin cambiar el inodo, de modo que el descriptor del servicio sigue siendo válido. Es la opción para programas que no saben reabrir su log, y tiene el riesgo de perder las líneas escritas entre la copia y el truncado.
Con truncate -s 0 en lugar de rm no habría habido incidente en ningún momento: el nombre sigue existiendo, el inodo sigue siendo el mismo, el descriptor del proceso sigue siendo válido, y los bloques se liberan de inmediato. La única peculiaridad es que un proceso con O_APPEND continúa al final (que ahora es 0) mientras que uno sin O_APPEND mantiene su desplazamiento antiguo y crea un fichero disperso (04-04). Por eso la regla es: para vaciar un log en uso, truncate -s 0 o : > fichero, jamás rm.
Conclusión
Un directorio es un fichero cuyo contenido es una tabla de pares (nombre, número de inodo). Lo hemos abierto con debugfs y visto la estructura real de ext4: 8 bytes de cabecera con inodo, rec_len, name_len y file_type, más el nombre. De ese formato salen dos comportamientos que se ven a diario: los directorios no encogen al borrar entradas, y un mismo inodo puede aparecer en varias entradas, que es la definición de enlace duro.
La organización en grafo acíclico de UNIX es el cuarto intento histórico, y cada anterior cayó por un motivo concreto: el nivel único por las colisiones de nombres, los dos niveles por su rigidez, el árbol puro por no permitir compartir. El grafo general se descarta por dos problemas graves —recorridos infinitos y basura inalcanzable que el contador de enlaces no detecta—, y por eso UNIX prohíbe los enlaces duros a directorios: una sola restricción que garantiza la aciclicidad y hace del contador de enlaces un recolector de basura suficiente.
La resolución de rutas es un bucle componente a componente que arranca en la raíz o en el cwd del proceso, comprueba tipo y permiso en cada paso, y expande los enlaces simbólicos con un tope de 40 para evitar ELOOP. Con cachés vacías cuesta 11 accesos a disco para /var/lib/meteora/lecturas/2026-08-31.dat —0,9 ms en NVMe, 88 ms en HDD—, y con la caché de dentries caliente baja a uno o a ninguno. Esos 78 MiB de dentries en meteo-01 son la razón de que el sistema de archivos parezca instantáneo.
Los dos tipos de enlace se distinguen por una sola cosa, de la que se deduce todo lo demás: el duro apunta a un inodo y el simbólico a un nombre. Por eso el duro no cruza sistemas de archivos ni enlaza directorios, sobrevive al borrado y al renombrado, y no tiene "original"; y por eso el simbólico cruza lo que quiera, se rompe con facilidad y cuesta una resolución extra —aunque en ext4, si mide menos de 60 caracteres, ni siquiera consume un bloque—.
El coste de los directorios enormes es un problema clásico de estructuras de datos: la lista lineal hace la búsqueda O(n) y la creación de n ficheros O(n²), mientras que el htree de ext4 (dir_index) resuelve en tres accesos lo que costaba trescientos. Pero incluso así, cien mil ficheros en un directorio siguen siendo mala idea, y la respuesta correcta es repartir en subdirectorios, como hace el archivo histórico de Meteora en archivo/2026/08/.
El FHS explica por qué cada ruta de Meteora está donde está, y su lógica se resume en una frase que conviene memorizar: la ubicación de un fichero declara sus garantías de persistencia. /var/lib para los datos que deben durar, /etc para la configuración de esta máquina, /var/log para los registros, y /run y /dev/shm —que son tmpfs— para lo que debe desaparecer al reiniciar.
Y unlink cierra el círculo del inodo sin nombre: quita una entrada de directorio, decrementa el contador de enlaces, y libera los bloques solo si el contador llega a 0 y nadie lo tiene abierto. De ahí el incidente de los 17 GB que no se liberan, que se diagnostica con lsof +L1 y se cura con truncate -s 0 /proc/<pid>/fd/N sin reiniciar nada.
Nos falta el último eslabón. Hemos dicho "el sistema de archivos de /var/lib/meteora", "el de /run", "el de /proc", como si el árbol único que recorremos con las rutas estuviera hecho de piezas distintas. Y lo está: / es un ext4 sobre una partición, /var/lib/meteora es otro ext4 sobre el RAID /dev/md0, /run es tmpfs en RAM, /proc no tiene dispositivo detrás. ¿Cómo se pegan todas esas piezas en un árbol continuo por el que la resolución de rutas camina sin enterarse del cambio? ¿Qué ocurre exactamente al ejecutar mount? ¿Y cómo puede el mismo open() funcionar sobre un SSD, sobre la RAM y sobre un servidor al otro lado de la red?
Es lo que veremos en Particiones, Montaje y Sistema de Archivos Virtual.
Fundamentos de Sistemas Operativos
Módulo 1: Introducción a los Sistemas Operativos
- Conceptos Básicos de Sistemas Operativos
- Historia y Evolución de los Sistemas Operativos
- Tipos de Sistemas Operativos
- Funciones Principales de un Sistema Operativo
- Arquitectura del Núcleo: Monolítico, Microkernel e Híbrido
- Modo Usuario, Modo Núcleo y Llamadas al Sistema
Módulo 2: Gestión de Recursos
- Gestión de Procesos
- Planificación de la CPU
- Gestión de Memoria
- Memoria Virtual y Paginación
- Gestión de Almacenamiento
- Gestión de Dispositivos
- Controladores, Interrupciones y Operaciones de E/S
Módulo 3: Concurrencia
- Conceptos de Concurrencia
- Hilos y Procesos
- Comunicación entre Procesos (IPC)
- Sincronización y Exclusión Mutua
- Problemas Clásicos de Concurrencia
- Interbloqueos: Prevención, Detección y Recuperación
Módulo 4: Estructuras de Archivos
- Sistemas de Archivos
- Estructuras de Directorios
- Particiones, Montaje y Sistema de Archivos Virtual
- Gestión de Archivos
- Asignación de Espacio, Journaling e Integridad
- Seguridad y Permisos de Archivos
Módulo 5: Protección y Seguridad del Sistema
- Principios de Protección y Control de Acceso
- Usuarios, Autenticación y Escalada de Privilegios
- Amenazas Comunes y Endurecimiento del Sistema
- Auditoría, Registros y Respuesta a Incidentes
Módulo 6: Virtualización y Contenedores
- Virtualización: Hipervisores y Máquinas Virtuales
- Contenedores: Namespaces y cgroups
- El Sistema Operativo en la Nube
- Sistemas Operativos Móviles y de Tiempo Real
