1. Qué es un archivo
Un proceso trabaja con su memoria, y en el tema 27 vimos que esa memoria le pertenece solo a él y solo mientras existe. Hace falta otro recurso para lo que debe sobrevivir al proceso, compartirse con otros o conservarse con el equipo apagado.
Un archivo es una secuencia de bytes con nombre, que el sistema operativo conserva en un dispositivo de almacenamiento. Un sistema de archivos es la parte del sistema operativo que organiza esos bytes en el dispositivo y permite encontrarlos por su nombre.
La definición tiene tres partes, y cada una es una decisión de diseño heredada de Unix:
- Una secuencia de bytes. El kernel no sabe ni le importa qué contiene un archivo. Texto, una imagen, un programa: para él son bytes numerados desde cero. La estructura la ponen los programas. Una extensión como
.txto.jpges una convención entre personas y aplicaciones, sin significado para el kernel. - Con nombre. Los programas no dicen «el bloque 81 204 del disco», sino
/home/diego/notas.txt. Traducir un nombre a una ubicación es la tarea central del sistema de archivos. - Conservada. A diferencia de la memoria, persiste. Es el nivel inferior de la jerarquía del tema 7.
El archivo es una abstracción en el sentido del tema 2: oculta que el disco está hecho de bloques de tamaño fijo, que los datos de un archivo pueden estar dispersos y que hay discos de tecnologías muy distintas. El programa ve una secuencia continua de bytes, y nada más.
2. Nombre, inodo y datos
Intuitivamente, un archivo «es» su nombre. Por dentro no es así. En los sistemas de archivos de la familia Unix, un archivo tiene tres componentes separados:
| Componente | Contiene | Dónde está |
|---|---|---|
| La entrada de directorio | Un nombre y un número de inodo. Nada más. | En el directorio que contiene al archivo. |
| El inodo | Todo lo que se sabe del archivo, salvo su nombre y su contenido: tipo, permisos, dueño, tamaño, fechas y dónde están los datos. | En una tabla del sistema de archivos. Se lo identifica por su número. |
| Los bloques de datos | El contenido. | Repartidos por el disco, donde haya lugar. |
El inodo —de index node— es el archivo propiamente dicho. El nombre es solo una etiqueta que lleva hasta él. Esta separación, que parece un detalle interno, explica varios comportamientos que de otro modo resultan sorprendentes; los vemos en las secciones 4 y 11.
Un directorio, en este esquema, no es un contenedor. Es un archivo cuyo contenido es una tabla de nombres y números de inodo. «Estar dentro» de un directorio significa figurar en su tabla.
3. Lo que el kernel sabe de un archivo
El comando stat muestra el contenido del inodo de un archivo:
$ echo "primera version" > notas.txt
$ stat notas.txt
File: notas.txt
Size: 16 Blocks: 8 IO Block: 4096 regular file
Device: 259,2 Inode: 1840200 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ diego) Gid: ( 1000/ diego)
Access: 2026-10-12 12:40:11.482913772 -0300
Modify: 2026-10-12 12:40:11.482913772 -0300
Change: 2026-10-12 12:40:11.482913772 -0300
Birth: 2026-10-12 12:40:11.482913772 -0300
| Campo | Significado |
|---|---|
Size | El tamaño en bytes: los quince caracteres y el salto de línea. |
Blocks | El espacio ocupado en el disco, en unidades de 512 bytes. Son 4096 bytes. |
Device | El dispositivo donde está el sistema de archivos. |
Inode | El número de inodo. |
Links | Cuántos nombres apuntan a este inodo. |
Access, Uid, Gid | Los permisos, y el usuario y el grupo dueños. Los vemos en el tema 30. |
Access, Modify, Change | Cuándo se leyó por última vez, cuándo cambió su contenido y cuándo cambió su inodo. |
Las dos primeras cifras resaltadas muestran una consecuencia del almacenamiento por bloques: un archivo de 16 bytes ocupa 4096 en el disco. El sistema de archivos reparte el espacio en bloques enteros, igual que la memoria se reparte en páginas, y el último bloque de cada archivo queda parcialmente vacío. El nombre del archivo no figura en ningún campo del inodo: stat lo muestra porque se lo pasamos como argumento.
Un programa obtiene la misma información con la llamada al sistema stat. El siguiente la usa para informar de qué tipo es un archivo, un dato que nos servirá en la sección 7:
#include <stdio.h>
#include <sys/stat.h>
int main(int argc, char *argv[])
{
struct stat s;
if (argc != 2) {
fprintf(stderr, "uso: %s archivo\n", argv[0]);
return 1;
}
if (lstat(argv[1], &s) == -1) { /* pedir el inodo al kernel */
perror(argv[1]);
return 1;
}
const char *tipo = S_ISREG(s.st_mode) ? "archivo regular" :
S_ISDIR(s.st_mode) ? "directorio" :
S_ISLNK(s.st_mode) ? "enlace simbolico" :
S_ISCHR(s.st_mode) ? "dispositivo de caracteres" :
S_ISBLK(s.st_mode) ? "dispositivo de bloques" :
S_ISFIFO(s.st_mode) ? "tuberia con nombre" :
S_ISSOCK(s.st_mode) ? "socket" : "desconocido";
printf("Tipo: %s\n", tipo);
printf("Inodo: %lu\n", (unsigned long)s.st_ino);
printf("Enlaces: %lu\n", (unsigned long)s.st_nlink);
printf("Tamano: %ld bytes\n", (long)s.st_size);
printf("Permisos: %o\n", (unsigned)(s.st_mode & 0777));
return 0;
}
$ gcc -Wall -Wextra -g info.c -o info
$ ./info notas.txt
Tipo: archivo regular
Inodo: 1840200
Enlaces: 1
Tamano: 16 bytes
Permisos: 664
$ ./info /
Tipo: directorio
Inodo: 2
Enlaces: 20
Tamano: 4096 bytes
Permisos: 755
La función se llama lstat y no stat para que, ante un enlace simbólico, informe sobre el enlace y no sobre su destino. La segunda ejecución muestra que el directorio raíz es un archivo como los demás, con su inodo, que en este sistema de archivos lleva siempre el número 2: es el punto de partida conocido para encontrar todos los otros.
4. Enlaces: varios nombres, un archivo
Si el nombre es solo una entrada que apunta a un inodo, nada impide que dos entradas apunten al mismo. Eso es un enlace duro, y se crea con ln:
$ ln notas.txt copia.txt
$ ls -li notas.txt copia.txt
1840200 -rw-rw-r-- 2 diego diego 16 oct 12 12:40 copia.txt
1840200 -rw-rw-r-- 2 diego diego 16 oct 12 12:40 notas.txt
$ echo "agregado" >> copia.txt # escribir usando un nombre…
$ cat notas.txt # …y leer usando el otro
primera version
agregado
$ rm notas.txt
$ cat copia.txt
primera version
agregado
La opción -i de ls añade el número de inodo, y la tercera columna es el contador de enlaces. Los dos nombres tienen el mismo inodo: no hay un original y una copia, sino un archivo con dos nombres, perfectamente equivalentes. Lo que se escribe por uno se lee por el otro, porque los datos son los mismos.
El final de la sesión revela qué hace rm. No destruye el archivo: quita un nombre. La llamada al sistema correspondiente se llama, con propiedad, unlink. El kernel descuenta uno del contador de enlaces, y solo cuando llega a cero —y nadie tiene el archivo abierto— libera el inodo y los bloques.
La otra clase de enlace funciona de manera distinta:
$ ln -s copia.txt atajo.txt
$ ls -l atajo.txt
lrwxrwxrwx 1 diego diego 9 oct 12 12:44 atajo.txt -> copia.txt
$ cat atajo.txt
primera version
agregado
$ rm copia.txt
$ cat atajo.txt
cat: atajo.txt: No such file or directory
Un enlace simbólico es un archivo aparte, con su propio inodo, cuyo contenido es una ruta: aquí, los nueve caracteres de copia.txt. Cuando un programa lo abre, el kernel lee esa ruta y sigue por ella. Si el destino desaparece, el enlace queda apuntando a nada.
| Enlace duro | Enlace simbólico | |
|---|---|---|
| Es | Otro nombre para el mismo inodo | Un archivo que contiene una ruta |
| Inodo | El mismo que el original | Uno propio |
| Si se borra el original | El archivo sigue existiendo | Queda roto |
| Puede apuntar a un directorio | No | Sí |
| Puede cruzar a otro sistema de archivos | No: los números de inodo son propios de cada uno | Sí |
| Se usa para | Copias de seguridad que no duplican datos | Atajos, y elegir entre versiones |
Encontramos enlaces simbólicos varias veces: /sbin/init, que apuntaba a systemd; el que creó systemctl enable en el tema 22; y exe y cwd en el directorio de cada proceso.
5. El árbol de directorios
Como un directorio puede contener otros directorios, el conjunto forma un árbol. En Unix hay un solo árbol, que nace en el directorio raíz, /. No existen letras de unidad: un segundo disco aparece como una rama más, según veremos en la sección siguiente.
Un archivo se designa con una ruta, y hay dos clases:
| Ruta absoluta | Ruta relativa | |
|---|---|---|
| Empieza | Con / | Sin / |
| Se interpreta desde | El directorio raíz | El directorio actual del proceso |
| Ejemplo | /home/diego/so/notas.txt | notas.txt, ../tema1/hola.c |
| Designa siempre lo mismo | Sí | No: depende de dónde esté el proceso |
El directorio actual es un dato de cada proceso, uno de los componentes que enumeramos en el tema 26, y se hereda del padre. Por eso cd es un comando interno de la shell y no un programa aparte: un programa que cambiara de directorio cambiaría el suyo, no el de la shell que lo lanzó.
Todo directorio contiene dos entradas especiales: ., que apunta a sí mismo, y .., que apunta a su padre. Son entradas reales, con su número de inodo; son las dos que aparecían de más al listar un directorio con getdents64 en el tema 10.
La organización de los primeros niveles sigue una convención común a todas las distribuciones. Varios de estos directorios los usamos a lo largo del curso:
| Directorio | Contenido | Lo usamos en |
|---|---|---|
/bin, /usr/bin | Los programas ejecutables. | Todo el curso |
/lib, /usr/lib | Las bibliotecas compartidas y los módulos del kernel. | Temas 9, 11 y 15 |
/etc | La configuración del sistema, en archivos de texto. | Temas 21 y 22 |
/home | Un directorio para los archivos de cada usuario. | — |
/root | El directorio del administrador. | — |
/boot | El kernel, el initramfs y el cargador de arranque. | Temas 11, 20 y 21 |
/var | Datos que cambian durante el funcionamiento: registros, colas, bases de datos. | Tema 22 |
/tmp | Archivos temporales, que cualquiera puede crear. | — |
/dev | Los archivos que representan dispositivos. | Temas 11 y 20; el 29 |
/proc, /sys | Información del kernel, presentada como archivos. | Casi todos; el 31 |
/run | Datos de los servicios en marcha. Se vacía en cada arranque. | Tema 22 |
/mnt, /media | Lugares donde montar otros dispositivos. | — |
6. Montar: muchos sistemas de archivos, un solo árbol
Un equipo tiene varios sistemas de archivos: uno por partición, más los de los dispositivos extraíbles. Para que todos formen un único árbol existe la operación de montar: asociar un sistema de archivos a un directorio del árbol, que se llama punto de montaje. Desde ese momento, lo que se ve al entrar en ese directorio es el contenido de ese otro sistema de archivos.
$ df -hT / /boot/efi /proc /dev /run
Filesystem Type Size Used Avail Use% Mounted on
/dev/nvme0n1p2 ext4 468G 92G 353G 21% /
/dev/nvme0n1p1 vfat 1.1G 6.2M 1.1G 1% /boot/efi
proc proc 0 0 0 - /proc
udev devtmpfs 7.8G 0 7.8G 0% /dev
tmpfs tmpfs 1.6G 2.1M 1.6G 1% /run
Las dos primeras líneas son las particiones del tema 20. La raíz del árbol está en la segunda partición, con formato ext4; la partición EFI, con formato FAT, está montada en /boot/efi. Un programa que recorre /boot/efi/EFI pasa de un disco, un formato y unas reglas a otros sin advertirlo.
Las tres últimas líneas son más curiosas: no tienen ningún disco detrás.
| Tipo | Dónde están los datos | Ejemplos |
|---|---|---|
| En disco | En una partición de un dispositivo. | ext4, XFS y Btrfs, los habituales de Linux; vfat, para compatibilidad; NTFS, el de Windows. |
| En memoria | En la memoria principal. Se pierden al apagar. | tmpfs, usado en /run; devtmpfs, en /dev. |
| Virtuales | En ningún lado: el kernel genera el contenido cuando alguien lo lee. | proc, en /proc; sysfs, en /sys. |
| En red | En otro equipo. | NFS; los de tipo FUSE que mencionamos en el tema 24. |
Que cosas tan distintas se usen con las mismas llamadas es obra de una capa del kernel, el sistema de archivos virtual o VFS. Define las operaciones que todo sistema de archivos debe ofrecer —buscar un nombre, leer, escribir— y cada uno las implementa a su manera. Cuando un programa llama a read, el VFS determina a qué sistema de archivos pertenece el archivo y le pasa el pedido. Es una tabla de funciones, la misma técnica de la tabla de llamadas al sistema y de la tabla de vectores de interrupción.
La lista de qué montar y dónde está en /etc/fstab, que el sistema de inicio procesa durante el arranque, como vimos en el tema 22. El comando findmnt muestra el árbol de montajes completo.
7. Todo es un archivo
En el tema 3 mencionamos una de las ideas de diseño de Unix: los dispositivos y los canales de comunicación se manejan con las mismas operaciones que los archivos. Con info podemos verlo. El primer carácter de cada línea de ls -l indica el tipo:
$ ls -ld /etc /etc/hostname /bin /dev/null /dev/nvme0n1 /run/dbus/system_bus_socket
drwxr-xr-x 142 root root 12288 oct 12 09:14 /etc
-rw-r--r-- 1 root root 7 abr 12 10:35 /etc/hostname
lrwxrwxrwx 1 root root 7 abr 12 10:31 /bin -> usr/bin
crw-rw-rw- 1 root root 1, 3 oct 12 09:14 /dev/null
brw-rw---- 1 root disk 259, 0 oct 12 09:14 /dev/nvme0n1
srw-rw-rw- 1 root root 0 oct 12 09:14 /run/dbus/system_bus_socket
$ ./info /dev/null | head -1
Tipo: dispositivo de caracteres
$ ./info /proc/cpuinfo | grep -E 'Tipo|Tamano'
Tipo: archivo regular
Tamano: 0 bytes
| Carácter | Tipo | Qué hay detrás |
|---|---|---|
- | Archivo regular | Bytes guardados en un sistema de archivos. |
d | Directorio | Una tabla de nombres e inodos. |
l | Enlace simbólico | Una ruta. |
c | Dispositivo de caracteres | Un driver que entrega o recibe una corriente de bytes: una terminal, /dev/null. |
b | Dispositivo de bloques | Un driver de un dispositivo de almacenamiento: un disco, una partición. |
p | Tubería con nombre | Un canal entre dos procesos. |
s | Socket | Un punto de conexión entre procesos. |
Solo los tres primeros tipos son archivos en el sentido corriente. Los otros cuatro tienen nombre, inodo y permisos, pero no tienen datos en el disco: abrirlos lleva a un driver o a un mecanismo del kernel. El último caso de la sesión es parecido: /proc/cpuinfo se declara como archivo regular de cero bytes y, sin embargo, cat obtiene de él varias pantallas de texto, porque el kernel fabrica el contenido en el momento de la lectura.
La ventaja de esta uniformidad la venimos aprovechando desde el tema 2:
- Los programas no necesitan saber con qué trabajan.
copiarlee del descriptor 0 y escribe en el 1, y sirve igual para un archivo, el teclado, una tubería o un dispositivo. - Las herramientas se aplican a todo.
cat,grepy las redirecciones funcionan sobre/procy/dev. En el tema 20 leímos la tabla de particiones de un disco conopenyread. - La protección es una sola. Los permisos de los archivos regulan también el acceso a los dispositivos: por eso hizo falta
sudopara leer/dev/nvme0n1.
La frase es un lema, no una ley. Hay operaciones que no encajan en leer y escribir bytes —expulsar un disco, cambiar la velocidad de un puerto—, y para ellas existe una llamada de uso general, ioctl. Y las conexiones de red se crean con llamadas propias, aunque luego se lean y se escriban como archivos. Los dispositivos son el contenido del tema 29.
8. Los descriptores
Desde el tema 14 usamos los números 0, 1 y 2 sin definirlos del todo. Abrir un archivo es costoso: el kernel debe recorrer la ruta, encontrar el inodo y comprobar los permisos. No tendría sentido repetirlo en cada lectura. Se hace una vez, con open, y el kernel entrega a cambio un número con el que el proceso se referirá a ese archivo de ahí en adelante.
Un descriptor de archivo es un número entero pequeño con el que un proceso identifica un archivo que tiene abierto. Es el índice de una tabla que el kernel mantiene para cada proceso.
Entre el descriptor y el archivo hay tres niveles, y la distinción entre ellos tiene consecuencias prácticas:
| Nivel | Hay una por | Contiene |
|---|---|---|
| Tabla de descriptores | Proceso | Para cada número, a qué archivo abierto se refiere. |
| Archivo abierto | Llamada a open | El modo en que se abrió y la posición: el byte por el que va la lectura o la escritura. |
| Inodo | Archivo | Lo que vimos en la sección 3. |
La figura muestra la situación habitual de un programa lanzado desde una terminal: los descriptores 0, 1 y 2 llevan los tres al mismo archivo abierto, que es la terminal. No se abrió tres veces: son tres índices para una misma apertura, heredados de la shell. El descriptor 3 corresponde a un archivo que el programa abrió por su cuenta y del que ya leyó cinco bytes.
El kernel muestra la tabla de descriptores de cada proceso en el directorio fd:
$ ls -l /proc/$$/fd
lrwx------ 1 diego diego 64 oct 12 12:50 0 -> /dev/pts/0
lrwx------ 1 diego diego 64 oct 12 12:50 1 -> /dev/pts/0
lrwx------ 1 diego diego 64 oct 12 12:50 2 -> /dev/pts/0
lrwx------ 1 diego diego 64 oct 12 12:50 255 -> /dev/pts/0
Es la tabla de la shell: sus tres descriptores estándar apuntan a /dev/pts/0, el archivo de dispositivo de esta terminal, el mismo que strace -y mostraba en el tema 16. El 255 es uno que Bash reserva para su uso interno.
9. Experimento: números y posiciones
El siguiente programa abre dos veces el mismo archivo y lee de a tres bytes, para mostrar cómo se asignan los números y dónde se guarda la posición:
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
/* Lee tres bytes de un descriptor y los muestra. */
static void leer3(const char *nombre, int fd)
{
char parte[4] = "";
if (read(fd, parte, 3) == -1)
perror("read");
printf("%s (descriptor %d) lee: %s\n", nombre, fd, parte);
}
int main(void)
{
int a = open("letras.txt", O_RDONLY);
int b = open("letras.txt", O_RDONLY); /* el mismo archivo, otra apertura */
if (a == -1 || b == -1) {
perror("letras.txt");
return 1;
}
leer3("a", a);
leer3("a", a);
leer3("b", b);
lseek(a, 0, SEEK_SET); /* volver al principio */
printf("-- lseek(a, 0) --\n");
leer3("a", a);
close(a);
int c = open("letras.txt", O_RDONLY);
printf("-- close(a), y un open nuevo devuelve %d --\n", c);
close(b);
close(c);
return 0;
}
$ gcc -Wall -Wextra -g abrir.c -o abrir
$ echo -n "ABCDEFGHIJ" > letras.txt
$ ./abrir
a (descriptor 3) lee: ABC
a (descriptor 3) lee: DEF
b (descriptor 4) lee: ABC
-- lseek(a, 0) --
a (descriptor 3) lee: ABC
-- close(a), y un open nuevo devuelve 3 --
La salida muestra cuatro reglas:
- El kernel asigna siempre el número libre más bajo. El 0, el 1 y el 2 estaban ocupados, de modo que las dos aperturas recibieron el 3 y el 4. Al cerrar el 3, la siguiente lo recibió otra vez.
- La posición avanza sola. La segunda lectura por
aentregó «DEF», no «ABC». El programa no indicó desde dónde leer: el kernel lo recuerda. Por esoreadno tiene un parámetro de posición. - Cada apertura tiene su posición.
bse refiere al mismo archivo, y empezó desde el principio. Son dos entradas distintas en el nivel intermedio de la figura. lseekcambia la posición sin leer nada. Es lo que permite el acceso directo a cualquier parte de un archivo.
La regla del número más bajo parece una curiosidad, y es la base de la sección siguiente.
10. Cómo redirige la shell
Cuando escribimos ls > lista.txt, el programa ls no recibe ningún argumento que le hable de un archivo. Escribe en el descriptor 1, como siempre. El trabajo lo hizo la shell antes de que ls empezara, en el intervalo entre fork y exec que señalamos en el tema 26: ya es un proceso distinto, y todavía ejecuta código de la shell.
La pieza clave es la llamada dup2(viejo, nuevo), que hace que el descriptor nuevo pase a referirse al mismo archivo abierto que viejo. El siguiente programa es lanzar.c, del tema 26, con tres líneas añadidas:
#include <fcntl.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
if (argc < 3) {
fprintf(stderr, "uso: %s archivo programa [argumentos]\n", argv[0]);
return 1;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
int fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror(argv[1]);
return 126;
}
dup2(fd, STDOUT_FILENO); /* el descriptor 1 pasa a ser ese archivo */
close(fd); /* el número original ya no hace falta */
execvp(argv[2], &argv[2]); /* el programa hereda la tabla ya modificada */
perror(argv[2]);
return 127;
}
waitpid(pid, NULL, 0);
return 0;
}
$ gcc -Wall -Wextra -g redirigir.c -o redirigir
$ ./redirigir salida.txt echo hola mundo
$ cat salida.txt
hola mundo
El comando echo no mostró nada en pantalla: su salida fue a parar al archivo. No fue modificado ni se enteró de nada. Cuando exec reemplaza el programa, la tabla de descriptores se conserva, y echo encontró en el número 1 un archivo donde normalmente hay una terminal. Es exactamente lo que hace la shell con echo hola mundo > salida.txt.
Todas las formas de redirección son variantes de la misma maniobra:
| Se escribe | La shell, en el hijo, antes de exec… |
|---|---|
programa > archivo | Abre el archivo para escritura, vaciándolo, y lo pone en el descriptor 1. |
programa >> archivo | Lo mismo, pero abriéndolo para añadir al final. |
programa < archivo | Abre el archivo para lectura y lo pone en el descriptor 0. |
programa 2> archivo | Lo pone en el descriptor 2. |
programa 2>&1 | Hace que el descriptor 2 se refiera a lo mismo que el 1. |
uno | otro | Crea una tubería. Pone su extremo de escritura en el descriptor 1 de uno y su extremo de lectura en el 0 de otro. |
Una tubería es un búfer que el kernel mantiene en memoria, con dos descriptores: lo que se escribe por uno se lee por el otro. Cuando el búfer se llena, el que escribe queda bloqueado; cuando se vacía, queda bloqueado el que lee. Es lo que hace que ls | wc -l funcione sin archivos intermedios, y uno de los casos en que write devuelve menos de lo pedido, según advertimos en el tema 14.
Con esto queda explicado por qué los programas de Unix leen de la entrada estándar y escriben en la salida estándar sin ocuparse de qué son. La combinación de descriptores heredados, la separación de fork y exec, y la uniformidad de los archivos permite conectar programas que no se conocen entre sí.
11. Borrar un archivo abierto
En el tema 25 afirmamos que Linux permite borrar un archivo que está en uso. Con las secciones 4 y 8 se puede ver por qué. Dejamos un programa leyendo un archivo, y lo borramos:
$ echo "contenido importante" > temporal.txt
$ tail -f temporal.txt & # un programa que lo mantiene abierto
[1] 8410
contenido importante
$ rm temporal.txt
$ ls temporal.txt
ls: cannot access 'temporal.txt': No such file or directory
$ ls -l /proc/8410/fd | grep temporal
lr-x------ 1 diego diego 64 oct 12 13:02 3 -> /home/diego/so/temporal.txt (deleted)
$ cat /proc/8410/fd/3
contenido importante
$ kill %1
El nombre desapareció del directorio, y el archivo sigue existiendo: el proceso 8410 lo tiene en su descriptor 3, y a través de /proc todavía se puede leer su contenido. La explicación está en la regla de la sección 4, que ahora se entiende completa. Un inodo se libera cuando se cumplen dos condiciones: que no quede ningún nombre que apunte a él, y que ningún proceso lo tenga abierto. rm eliminó el nombre. El archivo desaparecerá de verdad cuando tail termine.
El comportamiento tiene dos consecuencias prácticas, una buena y otra traicionera:
- Se pueden reemplazar archivos en uso. Al actualizar una biblioteca, el gestor de paquetes quita el nombre viejo y pone el nuevo. Los programas en marcha conservan la versión anterior hasta que terminan, y los que se inician después reciben la nueva. Es la razón por la que Linux rara vez pide reiniciar.
- Borrar no siempre libera espacio. Un caso clásico de administración: el disco se llenó por un archivo de registro enorme, alguien lo borra, y
dfsigue mostrando el disco lleno. El servicio que escribía en él lo mantiene abierto, y los bloques no se liberan hasta que ese servicio lo cierre o se reinicie. Los archivos en esa situación se encuentran consudo lsof +L1, o buscando(deleted)en los directoriosfd.
12. Del nombre al disco
Para cerrar, sigamos una operación completa a través de todas las capas: un programa abre /home/diego/so/notas.txt y lee sus primeros bytes.
- El programa llama a
opencon la ruta. La biblioteca hace la llamada al sistemaopenat. cambio de modo - El kernel recorre la ruta, un componente por vez. Parte del inodo raíz, busca
homeen su tabla, obtiene un inodo; buscadiegoen la de ese, y así hasta el final. En cada directorio comprueba que el proceso tenga permiso para atravesarlo. modo kernel - Comprueba los permisos del archivo contra la identidad del proceso, según el modo de apertura pedido. modo kernel
- Crea la entrada de archivo abierto, con posición cero, la anota en el primer lugar libre de la tabla de descriptores y devuelve ese número. modo kernel
- El programa llama a
readcon el descriptor. El kernel llega al inodo sin recorrer ninguna ruta, y el sistema de archivos determina qué bloques corresponden a la posición actual. modo kernel - Busca esos bloques en la caché de páginas. Si están, salta al paso 8. modo kernel
- Si no están, pide los bloques al driver del disco y pone al proceso a dormir. Cuando el dispositivo termina, una interrupción lo despierta. espera
- Copia los bytes al búfer del programa, avanza la posición y devuelve la cantidad leída. vuelta a modo usuario
Casi todo el curso está en esos ocho pasos: la llamada al sistema del tema 13, el envoltorio que cambia de nombre del 15, la caché del 7, el bloqueo y la interrupción del 17, el estado dormido del 26. El recorrido de la ruta del paso 2 es costoso, y por eso el kernel guarda también en memoria las traducciones recientes de nombres a inodos.
La escritura sigue el mismo camino con una diferencia importante, que explicamos en el tema 7. write copia los datos a la caché de páginas y vuelve de inmediato; el kernel los envía al disco más tarde. Durante ese intervalo, lo «guardado» está solo en memoria. Un programa que necesita la certeza de que sus datos llegaron al dispositivo debe pedirlo con fsync. A eso se suma el búfer de la biblioteca del tema 15, que retiene los datos antes incluso de que el kernel los vea. Entre un printf sobre un archivo y el disco hay dos esperas, cada una con su razón de ser y su manera de forzarla: fflush para la primera, fsync para la segunda.
Los pasos 6 y 7 dejan abierta una pregunta: qué hay del otro lado del driver. Es el asunto del tema 29.
13. Resumen
- Un archivo es una secuencia de bytes con nombre que el sistema conserva. El kernel no interpreta su contenido.
- Tiene tres partes: la entrada de directorio, con el nombre y un número de inodo; el inodo, con todos los demás datos; y los bloques con el contenido.
- Un directorio es un archivo que contiene una tabla de nombres e inodos.
- Un enlace duro es otro nombre para el mismo inodo. Un enlace simbólico es un archivo que contiene una ruta. Borrar es quitar un nombre.
- Hay un único árbol de directorios, que empieza en
/. Las rutas relativas se interpretan desde el directorio actual de cada proceso. - Montar es asociar un sistema de archivos a un directorio del árbol. Los hay en disco, en memoria, virtuales y en red, y el VFS los presenta a todos con la misma interfaz.
- Los dispositivos, las tuberías y los sockets tienen nombre e inodo y se usan con las mismas llamadas que los archivos regulares.
- Un descriptor es el índice de una tabla propia de cada proceso. El modo y la posición de lectura están en un nivel intermedio, uno por cada apertura.
- El kernel asigna siempre el descriptor libre más bajo, y la posición avanza con cada lectura o escritura.
- La shell redirige modificando la tabla de descriptores del proceso hijo, con
dup2, antes de ejecutar el programa. - Un archivo se libera cuando no tiene nombres y nadie lo tiene abierto. Por eso se puede borrar un archivo en uso, y por eso borrar no siempre libera espacio.
- Una lectura pasa por la resolución de la ruta, los permisos, la caché de páginas y, si hace falta, el driver del disco.
14. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué significa para el kernel la extensión .txt de un archivo?
Nada. Para el kernel, un archivo es una secuencia de bytes. Las extensiones son una convención entre usuarios y aplicaciones.
2. ¿Dónde está guardado el nombre de un archivo?
En el directorio que lo contiene, junto al número de su inodo. El inodo no contiene el nombre.
3. ¿Qué información guarda un inodo?
El tipo de archivo, los permisos, el usuario y el grupo dueños, el tamaño, las fechas, la cantidad de enlaces y la ubicación de los bloques de datos.
4. Un archivo de 16 bytes ocupa 4096 en el disco. ¿Por qué?
Porque el sistema de archivos asigna el espacio en bloques enteros, aquí de 4096 bytes. El resto del bloque queda sin usar.
5. Tras ln a.txt b.txt, ¿cuál es el original y cuál la copia?
Ninguno. Son dos nombres del mismo inodo, equivalentes en todo. No hay datos duplicados, y borrar cualquiera de los dos deja el archivo accesible por el otro.
6. ¿Qué hace realmente rm?
Elimina una entrada de directorio y descuenta uno del contador de enlaces del inodo. El archivo se libera solo si ese contador llega a cero y ningún proceso lo tiene abierto.
7. ¿Por qué un enlace duro no puede apuntar a un archivo de otra partición, y uno simbólico sí?
El enlace duro guarda un número de inodo, y esos números solo tienen sentido dentro de un sistema de archivos. El simbólico guarda una ruta, que puede llevar a cualquier lugar del árbol.
8. ¿Por qué cd es un comando interno de la shell?
Porque el directorio actual es un dato de cada proceso. Un programa aparte cambiaría su propio directorio y terminaría, sin efecto sobre la shell.
9. ¿Qué es un punto de montaje?
El directorio del árbol al que se asocia un sistema de archivos. Al entrar en él se ve el contenido de ese sistema de archivos, que puede estar en otro dispositivo o no estar en ninguno.
10. /proc/cpuinfo tiene tamaño cero y cat muestra texto. ¿Cómo?
Pertenece a un sistema de archivos virtual. No hay datos guardados: el kernel genera el contenido en el momento en que alguien lo lee.
11. ¿Dónde se guarda la posición de lectura de un archivo abierto?
En la entrada de archivo abierto que el kernel crea con cada open. No está en el descriptor, que es solo un índice, ni en el inodo, que es común a todas las aperturas.
12. Un proceso tiene abiertos los descriptores 0, 1, 2 y 4. ¿Qué número devuelve el siguiente open?
El 3, que es el libre más bajo.
13. En ls > lista.txt, ¿cómo sabe ls que debe escribir en el archivo?
No lo sabe. Escribe en el descriptor 1, como siempre. La shell, en el proceso hijo y antes de exec, abrió el archivo y lo colocó en el descriptor 1 con dup2.
14. Se borra un archivo de registro de 10 GB y df no muestra espacio recuperado. ¿Qué ocurre?
Algún proceso lo tiene abierto. Se eliminó el nombre, pero el inodo y sus bloques siguen ocupados hasta que ese proceso cierre el archivo o termine.
15. ¿Qué diferencia hay entre fflush y fsync?
fflush vacía el búfer de la biblioteca de C: hace que los datos lleguen al kernel con un write. fsync pide al kernel que los datos que tiene en su caché se escriban en el dispositivo.
Para practicar
- Ejecuta
statsobre un archivo, léelo concat, modifícalo conecho "x" >>y cámbiale los permisos conchmod, repitiendostatdespués de cada paso. ¿Cuál de las tres fechas cambia en cada caso? - Reproduce la sesión de enlaces de la sección 4. Luego crea un directorio y mira su contador de enlaces con
ls -ld. Crea dentro dos subdirectorios y vuelve a mirar. ¿Por qué aumenta? - Usa
infosobre/bin,/dev/tty, tu disco y un enlace simbólico. Cambialstatporstaty repite con el enlace. - Ejecuta
findmnty clasifica cada sistema de archivos montado en una de las cuatro clases de la sección 6. - Modifica
abrir.cpara que, en lugar del segundoopen, useb = dup(a). ¿Qué lee ahorab? Explícalo con la figura de la sección 8. - Añade un
forkaabrir.cdespués de abrir el archivo, y haz que el padre y el hijo lean tres bytes cada uno. ¿Comparten la posición? - Amplía
redirigir.cpara que redirija también la salida de errores al mismo archivo, como> archivo 2>&1. - Ejecuta
strace -f -e trace=openat,dup2,close,execve bash -c 'date > /tmp/f'y localiza las tres operaciones deredirigir.c. - Mira los descriptores de un proceso en una tubería: ejecuta
sleep 100 | sleep 100 &y examina conls -l /proc/PID/fdlos dos procesos. ¿Qué aparece en el descriptor 1 del primero y en el 0 del segundo? - Reproduce la sesión de la sección 11 con un archivo de 100 MB, creado con
head -c 100M /dev/zero. Comparadf -h .antes de borrar, después de borrar y después de finalizartail.