← Fundamentos de Sistemas Operativos Bloque 7 · Visión general de los servicios · Tema 28 de 34

Tema 28

Archivos: visión general

La memoria de un proceso desaparece con él. Lo que debe durar se guarda en archivos, y la misma interfaz con que se los lee sirve para hablar con una terminal, con un disco o con el propio kernel. En este tema vemos qué es un archivo por dentro, cómo se organiza el árbol de directorios y qué son esos números pequeños, los descriptores, que venimos usando desde el primer programa.

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:

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:

Un directorio contiene nombres asociados a números de inodo. El inodo guarda el tipo, los permisos, el dueño, el tamaño, las fechas, la cantidad de enlaces y la ubicación de los datos. Los datos están en bloques aparte Directorio notas.txt 1840200 mapa.c 1840315 fotos 1841007 Inodo 1840200 tipo y permisos dueño y grupo tamaño y fechas cantidad de enlaces ubicación de los datos Bloques datos datos datos
El nombre está en el directorio; todo lo demás, en el inodo. El inodo no sabe cómo se llama el archivo.
ComponenteContieneDónde está
La entrada de directorioUn nombre y un número de inodo. Nada más.En el directorio que contiene al archivo.
El inodoTodo 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 datosEl 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:

terminal
$ 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
CampoSignificado
SizeEl tamaño en bytes: los quince caracteres y el salto de línea.
BlocksEl espacio ocupado en el disco, en unidades de 512 bytes. Son 4096 bytes.
DeviceEl dispositivo donde está el sistema de archivos.
InodeEl número de inodo.
LinksCuántos nombres apuntan a este inodo.
Access, Uid, GidLos permisos, y el usuario y el grupo dueños. Los vemos en el tema 30.
Access, Modify, ChangeCuá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:

info.c
#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;
}
terminal
$ 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:

terminal
$ 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:

terminal
$ 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 duroEnlace simbólico
EsOtro nombre para el mismo inodoUn archivo que contiene una ruta
InodoEl mismo que el originalUno propio
Si se borra el originalEl archivo sigue existiendoQueda roto
Puede apuntar a un directorioNoSí
Puede cruzar a otro sistema de archivosNo: los números de inodo son propios de cada unoSí
Se usa paraCopias de seguridad que no duplican datosAtajos, 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 absolutaRuta relativa
EmpiezaCon /Sin /
Se interpreta desdeEl directorio raízEl directorio actual del proceso
Ejemplo/home/diego/so/notas.txtnotas.txt, ../tema1/hola.c
Designa siempre lo mismoSí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:

DirectorioContenidoLo usamos en
/bin, /usr/binLos programas ejecutables.Todo el curso
/lib, /usr/libLas bibliotecas compartidas y los módulos del kernel.Temas 9, 11 y 15
/etcLa configuración del sistema, en archivos de texto.Temas 21 y 22
/homeUn directorio para los archivos de cada usuario.—
/rootEl directorio del administrador.—
/bootEl kernel, el initramfs y el cargador de arranque.Temas 11, 20 y 21
/varDatos que cambian durante el funcionamiento: registros, colas, bases de datos.Tema 22
/tmpArchivos temporales, que cualquiera puede crear.—
/devLos archivos que representan dispositivos.Temas 11 y 20; el 29
/proc, /sysInformación del kernel, presentada como archivos.Casi todos; el 31
/runDatos de los servicios en marcha. Se vacía en cada arranque.Tema 22
/mnt, /mediaLugares 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.

terminal
$ 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.

TipoDónde están los datosEjemplos
En discoEn una partición de un dispositivo.ext4, XFS y Btrfs, los habituales de Linux; vfat, para compatibilidad; NTFS, el de Windows.
En memoriaEn la memoria principal. Se pierden al apagar.tmpfs, usado en /run; devtmpfs, en /dev.
VirtualesEn ningún lado: el kernel genera el contenido cuando alguien lo lee.proc, en /proc; sysfs, en /sys.
En redEn 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:

terminal
$ 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ácterTipoQué hay detrás
-Archivo regularBytes guardados en un sistema de archivos.
dDirectorioUna tabla de nombres e inodos.
lEnlace simbólicoUna ruta.
cDispositivo de caracteresUn driver que entrega o recibe una corriente de bytes: una terminal, /dev/null.
bDispositivo de bloquesUn driver de un dispositivo de almacenamiento: un disco, una partición.
pTubería con nombreUn canal entre dos procesos.
sSocketUn 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:

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:

Tres tablas encadenadas: la tabla de descriptores del proceso, con los números 0, 1, 2 y 3; la tabla de archivos abiertos del kernel, donde se guardan el modo y la posición; y los inodos. Los descriptores 0, 1 y 2 llevan al mismo archivo abierto, la terminal; el 3, a otro, notas.txt Descriptores por proceso Archivos abiertos en el kernel Inodos 0 1 2 3 lectura y escritura posición: no aplica solo lectura posición: 5 /dev/pts/0 notas.txt
El descriptor es solo un índice. El modo de apertura y la posición de lectura están en el nivel intermedio.
NivelHay una porContiene
Tabla de descriptoresProcesoPara cada número, a qué archivo abierto se refiere.
Archivo abiertoLlamada a openEl modo en que se abrió y la posición: el byte por el que va la lectura o la escritura.
InodoArchivoLo 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:

terminal
$ 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:

abrir.c
#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;
}
terminal
$ 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:

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:

redirigir.c
#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;
}
terminal
$ 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 escribeLa shell, en el hijo, antes de exec…
programa > archivoAbre el archivo para escritura, vaciándolo, y lo pone en el descriptor 1.
programa >> archivoLo mismo, pero abriéndolo para añadir al final.
programa < archivoAbre el archivo para lectura y lo pone en el descriptor 0.
programa 2> archivoLo pone en el descriptor 2.
programa 2>&1Hace que el descriptor 2 se refiera a lo mismo que el 1.
uno | otroCrea 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:

terminal
$ 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:

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.

  1. El programa llama a open con la ruta. La biblioteca hace la llamada al sistema openat. cambio de modo
  2. El kernel recorre la ruta, un componente por vez. Parte del inodo raíz, busca home en su tabla, obtiene un inodo; busca diego en la de ese, y así hasta el final. En cada directorio comprueba que el proceso tenga permiso para atravesarlo. modo kernel
  3. Comprueba los permisos del archivo contra la identidad del proceso, según el modo de apertura pedido. modo kernel
  4. 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
  5. El programa llama a read con 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
  6. Busca esos bloques en la caché de páginas. Si están, salta al paso 8. modo kernel
  7. 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
  8. 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

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

  1. Ejecuta stat sobre un archivo, léelo con cat, modifícalo con echo "x" >> y cámbiale los permisos con chmod, repitiendo stat después de cada paso. ¿Cuál de las tres fechas cambia en cada caso?
  2. 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?
  3. Usa info sobre /bin, /dev/tty, tu disco y un enlace simbólico. Cambia lstat por stat y repite con el enlace.
  4. Ejecuta findmnt y clasifica cada sistema de archivos montado en una de las cuatro clases de la sección 6.
  5. Modifica abrir.c para que, en lugar del segundo open, use b = dup(a). ¿Qué lee ahora b? Explícalo con la figura de la sección 8.
  6. Añade un fork a abrir.c después de abrir el archivo, y haz que el padre y el hijo lean tres bytes cada uno. ¿Comparten la posición?
  7. Amplía redirigir.c para que redirija también la salida de errores al mismo archivo, como > archivo 2>&1.
  8. Ejecuta strace -f -e trace=openat,dup2,close,execve bash -c 'date > /tmp/f' y localiza las tres operaciones de redirigir.c.
  9. Mira los descriptores de un proceso en una tubería: ejecuta sleep 100 | sleep 100 & y examina con ls -l /proc/PID/fd los dos procesos. ¿Qué aparece en el descriptor 1 del primero y en el 0 del segundo?
  10. Reproduce la sesión de la sección 11 con un archivo de 100 MB, creado con head -c 100M /dev/zero. Compara df -h . antes de borrar, después de borrar y después de finalizar tail.