Apuntes · Capítulo 13Persistencia

RAID y la interfaz de archivos

RAID y la interfaz de archivos

La semana pasada vimos que un disco duro es lento en acceso aleatorio (unas 76 lecturas de 4 KB por segundo en nuestro disco de escritorio), tiene una capacidad limitada y, tarde o temprano, falla. Este capítulo empieza con la respuesta clásica a esos tres problemas: juntar varios discos y presentarlos como uno solo. Eso es un RAID.

En la segunda mitad subimos un piso y miramos lo que ve un programa: archivos y directorios. Estudiamos la interfaz de Unix para manejarlos y las estructuras del kernel que hay detrás, en particular la tabla de archivos abiertos (open file table). La semana 14 abrirá la caja y veremos cómo se implementa un sistema de archivos (file system) sobre el disco.

Ruta de lectura.

  • Antes de clase (15–20 min): «Muchos discos como uno: RAID», «RAID 0: striping», «RAID 1: espejo», «Archivos y directorios» y «Crear, leer y escribir».
  • En clase: sesión A, «RAID 4: paridad», «RAID 5: paridad rotada» y «Comparación con números»; sesión B, de «El offset y lseek» a «Permisos, mkfs y mount».
  • Después de clase: repasar el «Resumen», leer «Herramientas propias: mytail y myfind» y resolver los «Ejercicios». Para ampliar: OSTEP cap. 38 y 39, y «RAID hoy».

Lectura. Este capítulo acompaña a OSTEP cap. 38 Redundant Arrays of Inexpensive Disks (RAIDs) y cap. 39 Interlude: Files and Directories. El código está en code/week-13; make test arma un árbol de prueba en /tmp/w13 y corre todos los programas dentro del contenedor del curso.

Muchos discos como uno: RAID

Un RAID (Redundant Array of Inexpensive Disks, Patterson, Gibson y Katz, 1988) es una caja con varios discos, un procesador, memoria y, muchas veces, NVRAM respaldada por batería. Hacia afuera tiene la interfaz de un disco: un arreglo de bloques que se leen y escriben. Por dentro, cada pedido lógico se traduce a uno o varios pedidos físicos. Esa transparencia explica su éxito: el sistema operativo y los programas no cambian.

Para razonar sobre fallas usamos el modelo fail-stop: un disco funciona bien o falló por completo, y cuando falla el RAID lo sabe de inmediato. Deja fuera la corrupción silenciosa y los sectores dañados sueltos (OSTEP cap. 45).

Compararemos cada diseño en capacidad (con N discos de B bloques), confiabilidad (cuántas fallas aguanta) y rendimiento. Para el rendimiento usamos dos cargas de trabajo (workloads): secuencial, donde un disco entrega S MB/s, y aleatoria, donde entrega R MB/s. Con el disco de escritorio de la semana 12 (7 200 RPM, 9 ms de búsqueda, 105 MB/s):

Carga Pedido Tiempo por pedido Tasa
Secuencial 100 MB 9 + 4,17 + 952,4 = 965,5 ms S = 103,6 MB/s
Aleatoria 4 KB 9 + 4,17 + 0,038 = 13,20 ms R = 0,303 MB/s (76 E/S por segundo)

Llamaremos T a la latencia de un pedido chico a un solo disco.

RAID 0: striping

El diseño más simple reparte los bloques entre los discos en ronda. Cada fila es una franja (stripe):

Cuatro discos en columnas con los bloques 0 a 15 repartidos en ronda: la fila 0 tiene los bloques 0 a 3, la fila 1 del 4 al 7, y así. La primera fila está marcada como una franja y el bloque 14 aparece resaltado en el disco 2, fila 3Cuatro discos en columnas con los bloques 0 a 15 repartidos en ronda: la fila 0 tiene los bloques 0 a 3, la fila 1 del 4 al 7, y así. La primera fila está marcada como una franja y el bloque 14 aparece resaltado en el disco 2, fila 3
RAID 0 con 4 discos. Cada fila es una franja; el bloque 14 está en el disco 14 mod 4 = 2, fila 14 div 4 = 3.

El bloque lógico A está en el disco A mod N, fila A div N: el bloque 14 de un arreglo de 4 discos está en el disco 2, fila 3.

En la práctica se pone un chunk de varios bloques por disco antes de pasar al siguiente. Un chunk chico reparte cada archivo entre muchos discos, pero cada pedido espera al disco más lento en posicionarse; uno grande evita eso, pero el paralelismo tiene que venir de muchos pedidos distintos. Los valores usuales van de 64 KB a 512 KB.

RAID 0 es la cota superior: capacidad N·B, N·S en secuencial y N·R en aleatorio. Y su confiabilidad es cero: si falla un disco, se pierde un pedazo de casi todos los archivos.

RAID 1: espejo

El espejo (mirroring) guarda cada bloque dos veces, en discos distintos. Lo común es combinarlo con striping (RAID 10): pares de espejos y, sobre ellos, franjas. Para leer, el RAID elige cualquier copia; para escribir, actualiza las dos en paralelo y espera a la más lenta.

  • Capacidad N·B / 2. Aguanta una falla seguro, y hasta N/2 con suerte.
  • Aleatorio: lectura N·R, escritura N/2·R.
  • Secuencial: N/2·S en ambos sentidos. La lectura sorprende: si a un disco le tocan los bloques 0, 4, 8…, tiene que dejar pasar bajo el cabezal los que no le tocan, y mientras tanto no entrega datos útiles.

Idea clave. Todo RAID que actualiza varios discos en una operación tiene el problema de la actualización consistente: si se va la luz entre las dos escrituras, las copias quedan distintas. Los RAID por hardware anotan primero lo que van a hacer en una NVRAM (un pequeño log) y lo repiten al volver la luz.

RAID 4: paridad

El espejo gasta la mitad del espacio. La paridad protege contra una falla gastando un solo disco: por cada franja, un bloque extra en un disco aparte con el XOR bit a bit de los bloques de datos. El XOR de varios bits es 1 si hay un número impar de unos; dicho de otra forma, en cada posición de bit, contando la paridad, el número de unos es par. Ese es el invariante.

raid5.c guarda un texto en cinco discos de juguete. Estos son los primeros bytes de la franja 0, tal como los imprime:

  D0     01010011  (0x53 'S')
  D1     01110011  (0x73 's')
  D2     01100001  (0x61 'a')
  D3     01110010  (0x72 'r')
  P0     00110011  (0x33 '3')

El bit 6 tiene cuatro unos (par): paridad 0. El bit 5 tiene tres (impar): paridad 1. Y así con los ocho bits y los 4 096 bytes de un bloque real.

Ahora falla el disco 2. Para el bit 5, los sobrevivientes tienen 0, 1, 1 y 1; para que el total sea par, el que falta tiene que ser 1. En general:

D2 = D0 ⊕ D1 ⊕ D3 ⊕ P0 = 01100001 = 'a'

Reconstruir es lo mismo que calcular la paridad. Pero reconstruir un disco completo obliga a leer todos los demás, y si fallan dos no hay ecuación que alcance.

Escribir con paridad

Si se escribe una franja entera, el RAID calcula la paridad y escribe todo en paralelo (escritura de franja completa). Así RAID 4 logra capacidad (N−1)·B, secuencial (N−1)·S y lectura aleatoria (N−1)·R.

El problema es escribir un solo bloque. Hay dos formas de actualizar la paridad:

  • Aditiva: leer los otros N−2 bloques de la franja, recalcular y escribir dato y paridad. El costo crece con N.
  • Sustractiva: leer el dato viejo y la paridad vieja y aplicar P_nueva = (D_viejo ⊕ D_nuevo) ⊕ P_vieja: donde el dato cambió, la paridad se invierte. Siempre 2 lecturas y 2 escrituras.

Reemplazar ‘s’ por ‘N’ en el bloque 1:

Animación: la controladora RAID recibe el dato nuevo N para el bloque 1; lee el dato viejo s del disco 1 y la paridad vieja del disco 4; marca las columnas de bits donde el dato cambió, invierte ahí la paridad y obtiene 00001110; luego escribe el dato y la paridad nuevos. El contador llega a 4 E/SAnimación: la controladora RAID recibe el dato nuevo N para el bloque 1; lee el dato viejo s del disco 1 y la paridad vieja del disco 4; marca las columnas de bits donde el dato cambió, invierte ahí la paridad y obtiene 00001110; luego escribe el dato y la paridad nuevos. El contador llega a 4 E/S
Escritura pequeña sustractiva en la franja 0. D viejo 01110011 ('s'), D nuevo 01001110 ('N'), P vieja 00110011, P nueva 00001110: la paridad se invierte justo en las columnas donde cambió el dato.

Pueden comprobar que ‘S’ ⊕ ‘N’ ⊕ ‘a’ ⊕ ‘r’ también da 00001110.

El problema de la escritura pequeña

Cada escritura pequeña cuesta 4 E/S y tarda unos 2T. Peor aún: dos escrituras simultáneas a los bloques 4 y 13 tienen sus datos en discos distintos, pero las dos leen y escriben paridad en el mismo disco. El disco de paridad hace 2 E/S por cada escritura lógica, así que todo el arreglo queda en R/2, sin importar cuántos discos se agreguen. Es el problema de la escritura pequeña (small-write problem).

RAID 5: paridad rotada

RAID 5 pone la paridad de cada franja en un disco distinto. Esta es la distribución de raid5.c (la de OSTEP): la franja s guarda su paridad en el disco N − 1 − (s mod N).

Dos arreglos de cinco discos lado a lado. En RAID 4, la paridad P0 a P3 está toda en el disco 4, y las escrituras a los bloques 4 y 13 necesitan ese disco las dos. En RAID 5, la paridad rota: P0 en el disco 4, P1 en el 3, P2 en el 2 y P3 en el 1, y las escrituras a los bloques 1 y 10 usan los discos 1 y 4, y 0 y 2Dos arreglos de cinco discos lado a lado. En RAID 4, la paridad P0 a P3 está toda en el disco 4, y las escrituras a los bloques 4 y 13 necesitan ese disco las dos. En RAID 5, la paridad rota: P0 en el disco 4, P1 en el 3, P2 en el 2 y P3 en el 1, y las escrituras a los bloques 1 y 10 usan los discos 1 y 4, y 0 y 2
A la izquierda, RAID 4; a la derecha, la distribución de raid5.c. Cada color es una escritura pequeña: su bloque de datos y su bloque de paridad.

Escribir el bloque 1 usa los discos 1 y 4; escribir el 10, los discos 0 y 2: pueden ir en paralelo. Con muchos pedidos trabajan todos los discos; cada escritura sigue costando 4 E/S, y la escritura aleatoria llega a N/4·R. La lectura aleatoria sube a N·R. Por eso RAID 5 reemplazó casi por completo a RAID 4.

raid5.c hace caer un disco, reconstruye cada bloque con los otros cuatro y lo compara con el original; luego hace una escritura pequeña sustractiva y la verifica contra la paridad recalculada desde cero:

$ ./raid5 2
...
Falla el disco 2. Reconstrucción:
  f0: "a, la pa"  == original
  f1: "s: basta"  == original
  f2: P2 = 59 4a 08 0a 01 00 5b 4e  == original
  f3: " la fran"  == original
  4/4 bloques recuperados, 16 lecturas

Escritura pequeña al bloque 6:
  dato en disco 1, paridad en disco 3
  2 lecturas + 2 escrituras = 4 E/S
  paridad sustractiva == recalculada

Comparación con números

RAID 0 RAID 1 RAID 4 RAID 5
Capacidad N·B N·B/2 (N−1)·B (N−1)·B
Fallas que aguanta 0 1 (hasta N/2) 1 1
Secuencial (lectura y escritura) N·S N/2·S (N−1)·S (N−1)·S
Lectura aleatoria N·R N·R (N−1)·R N·R
Escritura aleatoria N·R N/2·R 1/2·R N/4·R
Latencia lectura / escritura T / T T / T T / 2T T / 2T

Con 8 discos de 4 TB y nuestro disco de escritorio:

Capacidad Secuencial Lectura aleatoria Escritura aleatoria
RAID 0 32 TB 829 MB/s 606 E/S/s 606 E/S/s
RAID 1 16 TB 414 MB/s 606 E/S/s 303 E/S/s
RAID 4 28 TB 725 MB/s 530 E/S/s 38 E/S/s
RAID 5 28 TB 725 MB/s 606 E/S/s 151 E/S/s

Idea clave. Solo rendimiento: RAID 0. Rendimiento en escrituras aleatorias con confiabilidad: RAID 1, pagando la mitad del espacio. Capacidad con confiabilidad: RAID 5, sabiendo que cada escritura pequeña cuesta 4 E/S.

RAID hoy

Reconstruir un disco exige leer todos los demás y escribir el reemplazo completo; la cota inferior es tamaño / velocidad. Un disco de 20 TB a 250 MB/s necesita unas 22 horas, y más si el arreglo sigue atendiendo pedidos. Durante ese tiempo un RAID 5 no tiene redundancia, y los discos que quedan son del mismo lote y la misma edad que el que falló. Por eso con discos grandes se usa RAID 6: dos bloques de redundancia por franja, P (XOR) y Q (un código de Reed–Solomon). Aguanta dos fallas, su capacidad es (N−2)·B y una escritura pequeña cuesta 6 E/S.

El RAID puede vivir en una tarjeta controladora o en el kernel: en Linux, el driver md (que se administra con mdadm) hace RAID 0, 1, 4, 5, 6 y 10 por software. ZFS junta el sistema de archivos con el RAID (RAID-Z1, Z2 y Z3). Los dos enfrentan el hueco de escritura (write hole): si la luz se va entre la escritura del dato y la de la paridad, la franja queda inconsistente sin que nadie lo note. md ofrece un bitmap de intención de escritura y, para RAID 5, un journal o un log de paridad parcial (PPL). ZFS nunca sobrescribe en su lugar (copy-on-write) y guarda checksums, con lo que también detecta la corrupción silenciosa que el modelo fail-stop ignora.

Un gabinete de almacenamiento en un rack: arriba la controladora y debajo cuatro bandejas con hasta 12 discos cada una, varios de ellos con su etiqueta de capacidad
Un arreglo de discos de almacenamiento (HP EVA4400): la controladora arriba y cuatro bandejas de discos debajo. Foto: Redline, CC BY-SA 3.0, Wikimedia Commons.

Y una advertencia: RAID no es un backup. Si alguien borra un archivo, el RAID borra todas sus copias.

Archivos y directorios

Un archivo es un arreglo de bytes; al sistema de archivos no le interesa qué hay adentro. Su nombre de bajo nivel es el número de inodo (inode number), y el inodo guarda sus metadatos: tamaño, owner, permisos, fechas y dónde están sus bloques.

Un directorio también tiene inodo, y su contenido es una lista de pares (nombre, número de inodo). Directorios dentro de directorios forman el árbol. Para abrir /home/ana/notas.txt se busca home en /, ana en home y notas.txt en ana. El nombre de un archivo no está en su inodo: está en el directorio que lo contiene.

Crear, leer y escribir

int fd = open("foo", O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);

O_CREAT crea el archivo si no existe, O_TRUNC lo vacía si existe y el tercer argumento da los permisos. El resultado es un descriptor de archivo (file descriptor): un entero chico, privado del proceso, que funciona como una capability. Cada proceso empieza con los descriptores 0, 1 y 2 abiertos, y open devuelve el número libre más bajo: casi siempre 3.

strace muestra lo que hace cat (sin la carga de bibliotecas):

$ strace -e trace=openat,read,write,close cat foo
...
openat(AT_FDCWD, "foo", O_RDONLY)       = 3
read(3, "hola\n", 131072)               = 5
write(1, "hola\n", 5hola
)                   = 5
read(3, "", 131072)                     = 0
close(3)                                = 0

cat pide hasta 128 KB, recibe 5 bytes y los escribe en el descriptor 1. Cuando read devuelve 0, llegó al fin del archivo.

El offset y lseek

Cada archivo abierto tiene un offset: dónde empieza la próxima lectura o escritura. read y write lo avanzan; lseek(fd, off, whence) lo cambia (SEEK_SET desde el inicio, SEEK_CUR desde donde está, SEEK_END desde el final). Con un archivo de 300 bytes:

Llamada Retorna offset después
fd = open("f", O_RDONLY) 3 0
read(fd, buf, 100) 100 100
read(fd, buf, 100) 100 200
lseek(fd, 250, SEEK_SET) 250 250
read(fd, buf, 100) 50 300
read(fd, buf, 100) 0 300

A pesar del nombre, lseek no mueve el disco: solo cambia un número en la memoria del kernel.

La tabla de archivos abiertos

El offset no vive en el descriptor ni en el inodo, sino en un nivel intermedio:

  1. Cada proceso tiene un arreglo de descriptores; el descriptor es un índice.
  2. Cada posición ocupada apunta a una entrada de la tabla de archivos abiertos, una sola para todo el sistema, con el offset, el modo, un contador de referencias y un puntero al inodo.
  3. El inodo en memoria representa al archivo.

Cada open crea una entrada nueva: dos open del mismo archivo (en uno o en dos procesos) dan offsets independientes. Dos llamadas hacen, en cambio, que dos descriptores compartan una entrada:

  • fork() copia el arreglo de descriptores, no las entradas. Si el hijo lee 10 bytes, el offset del padre también pasa a 10. Así un padre y sus hijos escriben en la misma salida sin pisarse.
  • dup(fd) da otro descriptor del mismo proceso que apunta a la misma entrada. La shell hace cmd > out 2>&1 abriendo out en el descriptor 1 y luego dup2(1, 2).
Tres casos. Con fork, el descriptor 3 del padre y el del hijo apuntan a una sola entrada con offset 10 y ref 2. Con dup, los descriptores 3 y 4 de un proceso apuntan a una entrada con offset 5 y ref 2. Con dos open, los descriptores 3 y 4 tienen entradas distintas, con offsets 5 y 0. Todas las entradas apuntan al mismo inodo de off.txtTres casos. Con fork, el descriptor 3 del padre y el del hijo apuntan a una sola entrada con offset 10 y ref 2. Con dup, los descriptores 3 y 4 de un proceso apuntan a una entrada con offset 5 y ref 2. Con dos open, los descriptores 3 y 4 tienen entradas distintas, con offsets 5 y 0. Todas las entradas apuntan al mismo inodo de off.txt
El offset vive en la entrada de la tabla de archivos abiertos, no en el descriptor ni en el inodo. Los números son los de shareoff.c.

shareoff.c prueba los tres casos sobre un archivo con el abecedario (el offset se consulta con lseek(fd, 0, SEEK_CUR)):

$ ./shareoff /tmp/w13/off.txt
1) fork():
  hijo  lee 10 bytes: "abcdefghij"
  offset del padre: 10
  padre lee 5 bytes: "klmno"
2) dup():
  fd    lee 5 bytes: "abcde"
  offset de fd=3: 5, de fd2=4: 5
3) open() dos veces:
  a     lee 5 bytes: "abcde"
  offset de a=3: 5, de b=4: 0
  b     lee 5 bytes: "abcde"

Antes de fork el programa hace fflush(stdout): si no, con la salida en un pipe, el texto pendiente en el buffer de stdio se copiaría al hijo y aparecería dos veces (el misterio de la semana 2).

En xv6. La entrada de la tabla es struct file (kernel/file.h), con type, ref, readable, writable, el inodo ip y el offset off. La tabla del sistema es ftable (kernel/file.c): NFILE = 100 entradas bajo un spinlock, libres si ref == 0. La tabla de descriptores es struct file *ofile[NOFILE] en struct proc (kernel/proc.h), con NOFILE = 16. sys_open pide siempre una entrada nueva con filealloc() y le pone off = 0. kfork() (kernel/proc.c) llama a filedup() por cada descriptor abierto, y sys_dup también: ambos copian el puntero y hacen f->ref++. fileclose() resta uno y solo con ref en 0 libera la entrada y suelta el inodo con iput(). xv6 no tiene lseek.

Escribir de verdad: fsync

Cuando write retorna, los datos casi nunca están en el disco: están en la caché de páginas del kernel, marcados como sucios. En Linux, por defecto, cada 5 segundos se escriben los bloques sucios que llevan más de 30. Si la luz se va antes, se pierden. Un programa que necesita durabilidad llama a fsync(fd): el kernel escribe los datos y metadatos sucios del archivo, le pide al disco que vacíe su propia caché y recién entonces retorna (fdatasync omite los metadatos que no hacen falta para leerlo).

Un archivo recién creado también vive en su directorio: para que su nombre sobreviva a una caída, hay que hacer fsync también del directorio.

Reemplazar un archivo de forma atómica: rename

Reescribir un archivo en su lugar es peligroso: una caída a la mitad deja un archivo vacío o una mezcla. rename(viejo, nuevo) cambia la entrada del directorio en un solo paso, también frente a caídas: el nombre apunta al inodo viejo o al nuevo, nunca a algo intermedio. El patrón, en atomic.c: escribir la versión nueva en un temporal, fsync del temporal, rename sobre el nombre final y fsync del directorio. Con la opción crash, el programa termina justo antes del rename:

$ cd /tmp/w13
$ /work/atomic c.txt v1
escrito y sincronizado: c.txt.tmp
rename c.txt.tmp -> c.txt listo
$ /work/atomic c.txt v2 crash
escrito y sincronizado: c.txt.tmp
*** caída simulada antes de rename ***
$ cat c.txt
v1
Dos estados del directorio. Antes del rename, la entrada c.txt apunta al inodo viejo con v1 y la entrada c.txt.tmp al inodo nuevo con v2, que ya está en disco. Después del rename queda solo la entrada c.txt, que apunta al inodo nuevo, y el inodo viejo queda con nlink 0 y se libera. Una caída antes deja c.txt con v1; una caída después, con v2Dos estados del directorio. Antes del rename, la entrada c.txt apunta al inodo viejo con v1 y la entrada c.txt.tmp al inodo nuevo con v2, que ya está en disco. Después del rename queda solo la entrada c.txt, que apunta al inodo nuevo, y el inodo viejo queda con nlink 0 y se libera. Una caída antes deja c.txt con v1; una caída después, con v2
El patrón temporal + rename. El rename cambia una sola entrada del directorio: una caída deja el estado de la izquierda o el de la derecha, nunca una mezcla.

Metadatos: stat

stat devuelve el inodo en un struct stat: dispositivo e inodo, tipo y permisos (st_mode), enlaces (st_nlink), owner, grupo, tamaño, bloques y tres fechas: acceso, modificación del contenido y cambio del inodo. lstat describe el enlace simbólico en vez de su destino; fstat recibe un descriptor. mystat.c es un stat mínimo:

$ ./mystat /tmp/w13/docs/a.txt
/tmp/w13/docs/a.txt
  tipo      archivo regular
  inodo     35442243  (dispositivo 0:108)
  enlaces   2
  tamaño    5 bytes, 8 bloques de 512
  permisos  0644 rw-r--r--  owner root  grupo root
  ...

5 bytes ocupan 8 bloques de 512: el sistema de archivos reserva bloques de 4 KB. ¿Y por qué 2 enlaces? Lo vemos enseguida.

Directorios

mkdir crea un directorio con dos entradas: . (él mismo) y .. (el padre); rmdir solo borra uno vacío. Se leen con opendir, readdir y closedir: cada struct dirent trae nombre e inodo, y el resto sale de un lstat por entrada. Así funciona ls -l, y así myls.c:

$ ./myls -l /tmp/w13/docs
35442243 -rw-r--r--  2 root  root       5 Nov  4 14:35 a.txt
35442243 -rw-r--r--  2 root  root       5 Nov  4 14:35 b.txt
35442240 drwxr-xr-x  2 root  root    4096 Nov  4 14:35 notas
35442245 lrwxrwxrwx  1 root  root       8 Nov  4 14:35 roto -> nada.txt
35442244 lrwxrwxrwx  1 root  root       5 Nov  4 14:35 sim -> a.txt

Lo que no se puede es escribir un directorio con write: solo el kernel lo modifica, vía open con O_CREAT, mkdir, link, unlink o rename. Un directorio tiene 2 + (subdirectorios) enlaces: su entrada en el padre, su . y el .. de cada hijo. docs tiene un subdirectorio, y mystat /tmp/w13/docs muestra enlaces 3.

En la salida de myls, a.txt y b.txt tienen el mismo inodo: son dos nombres del mismo archivo. Eso es un enlace duro (hard link), y se crea con link() o con ln:

$ echo hola > file
$ ln file file2
$ ls -li file file2
34297533 -rw-r--r-- 2 root root 5 Oct 29 09:22 file
34297533 -rw-r--r-- 2 root root 5 Oct 29 09:22 file2
$ rm file
$ ls -li file2
34297533 -rw-r--r-- 1 root root 5 Oct 29 09:22 file2

ln no copia nada: agrega una entrada con el mismo número de inodo y sube el contador nlink. Ninguno de los nombres es el “original”. rm file borra una entrada y baja nlink a 1.

Por eso la llamada que usa rm se llama unlink y no delete: solo quita un enlace. El archivo desaparece cuando nlink llega a 0 y ningún proceso lo tiene abierto:

$ exec 3< file2      # la shell lo abre en el descriptor 3
$ rm file2           # nlink = 0
$ cat <&3            # todavía se puede leer
hola

Es un truco útil: crear un temporal, abrirlo y hacerle unlink enseguida. Nadie más lo ve, y el kernel lo borra cuando el programa lo cierra o termina.

En xv6. nlink es un campo de struct dinode (kernel/fs.h): vive en el disco. sys_link (kernel/sysfile.c) hace ip->nlink++, iupdate(ip) y agrega la entrada con dirlink; no acepta directorios. sys_unlink llena de ceros la entrada con writei y hace ip->nlink--. Los bloques se liberan en iput() (kernel/fs.c), que calcula last = (ip->ref == 1 && ip->valid && ip->nlink == 0): solo si nadie más tiene el inodo en memoria y nadie lo nombra en el disco, itrunc libera los bloques e ifree el inodo. nlink cuenta nombres; ref, referencias.

Enlaces simbólicos

Un enlace duro no puede apuntar a un directorio (habría ciclos) ni a otro sistema de archivos (los números de inodo solo valen dentro de uno). El enlace simbólico (symbolic link) sí: ln -s a.txt sim crea un archivo nuevo, de otro tipo, cuyo contenido es la ruta del destino (por eso sim mide 5 bytes). No suma al nlink del destino, y si el destino se borra queda colgando (dangling):

$ rm a.txt
$ cat sim
cat: sim: No such file or directory
$ ls -l sim
lrwxrwxrwx 1 root root 5 Oct 29 09:22 sim -> a.txt
El directorio docs tiene tres entradas: a.txt y b.txt con el inodo 35442243, y sim con el inodo 35442244. El primer inodo es un archivo regular con nlink 2 y datos hola; el segundo es un enlace simbólico con nlink 1 cuyo contenido es la ruta a.txt, y una flecha vuelve desde esa ruta al directorioEl directorio docs tiene tres entradas: a.txt y b.txt con el inodo 35442243, y sim con el inodo 35442244. El primer inodo es un archivo regular con nlink 2 y datos hola; el segundo es un enlace simbólico con nlink 1 cuyo contenido es la ruta a.txt, y una flecha vuelve desde esa ruta al directorio
Un enlace duro es otra entrada con el mismo número de inodo; un enlace simbólico es un inodo aparte que guarda una ruta y se resuelve buscando ese nombre otra vez.

Permisos, mkfs y mount

Los nueve bits bajos de st_mode dan lectura, escritura y ejecución para el owner, el grupo y los demás: -rw-r--r-- es 0644, y chmod 600 f deja el archivo solo para su owner. En un directorio, r permite listarlo, w crear y borrar entradas, y x atravesarlo. root pasa por encima de todo. Una ACL (access control list) da permisos a usuarios y grupos con nombre: setfacl -m u:nobody:r f agrega una entrada user:nobody:r--, y ls -l muestra un + junto a los permisos.

Para armar el árbol, mkfs escribe un sistema de archivos vacío en un dispositivo o en un archivo imagen (mkfs.ext4 disk.img en el contenedor crea uno de 16 384 bloques de 4 KB con su journal), y mount -t ext4 /dev/sdb1 /home/users lo pega en un punto del árbol: desde ese momento, /home/users/ana es /ana del otro disco. Un solo árbol reúne así ext4, tmpfs, proc y otros. (Montar pide privilegios que el contenedor no tiene.)

Herramientas propias: mytail y myfind

mytail.c imprime las últimas n líneas sin leer todo el archivo: fstat da el tamaño, lseek salta cerca del final y lee bloques de 4 KB hacia atrás contando saltos de línea:

$ ./mytail -3 /tmp/w13/log.txt
línea 1998
línea 1999
línea 2000
[mytail: 4096 de 22893 bytes examinados]

myfind.c recorre el árbol con recursión, scandir y lstat, con filtros -name y -type. Con lstat no sigue enlaces simbólicos, así que no cae en ciclos: ./myfind /tmp/w13 -type l encuentra docs/roto y docs/sim.

En xv6. La interfaz de archivos de xv6 es pequeña (ver user/usys.pl): no tiene lseek, rename, fsync ni enlaces simbólicos, aunque sí un sync que espera a que se confirme el log (sys_sync en kernel/log.c). El proyecto P5 (lab fs del MIT) agrega archivos grandes, con un bloque doblemente indirecto (semana 14), y enlaces simbólicos: la llamada symlink(target, path), el tipo T_SYMLINK y la opción O_NOFOLLOW. open debe seguir el enlace, también si apunta a otro enlace, con un límite de profundidad contra los ciclos.

Resumen

  • Un RAID presenta varios discos como uno. Con el modelo fail-stop, se compara por capacidad, confiabilidad y rendimiento (N, B, S, R, T).
  • RAID 0 reparte: rápido y sin protección. RAID 1 copia: aguanta una falla y gasta la mitad. RAID 4/5 guardan la paridad XOR, que reconstruye cualquier disco que falle.
  • Una escritura pequeña con paridad cuesta 4 E/S. En RAID 4 el disco de paridad la limita a R/2; RAID 5 rota la paridad y llega a N/4·R. Con discos grandes se usa RAID 6.
  • Descriptor → entrada de la tabla de archivos abiertos (offset, referencias) → inodo. fork y dup comparten la entrada; dos open, no.
  • write no es durable sin fsync. Reemplazo atómico: temporal, fsync, rename.
  • Un directorio guarda pares (nombre, inodo). unlink quita un nombre; el inodo se libera con nlink == 0 y sin referencias abiertas. Un enlace simbólico guarda una ruta y puede quedar colgando.

Ejercicios

  1. Un RAID 4 tiene en una franja los bytes 0x0F, 0xA0, 0x33 y 0x5C. Calculen la paridad. Cambien 0xA0 por 0xFF con el método sustractivo y comprueben que coincide con recalcularla desde cero.
  2. ¿Cuántas E/S cuesta una escritura pequeña en RAID 4? ¿Para qué N conviene el método aditivo? ¿Por qué RAID 5 mejora las escrituras aleatorias y no las secuenciales?
  3. Repitan la tabla de 8 discos con N = 4. ¿Cómo queda la escritura aleatoria de RAID 5 frente a un solo disco?
  4. Con raid.py (homework del cap. 38), comparen -L 5 -5 LS y -L 5 -5 LA. ¿Cuál de las dos distribuciones usa raid5.c?
  5. ¿Por qué se llama unlink? Escriban un programa que cree un archivo, lo abra, le haga unlink, escriba 1 MB y lo vuelva a leer. ¿Cuándo se libera el espacio?
  6. Modifiquen shareoff.c para que padre e hijo escriban en el mismo descriptor, y luego en dos descriptores abiertos por separado. Comparen el contenido final del archivo.
  7. Agreguen a myfind la opción -L para seguir enlaces simbólicos y creen un enlace ciclo -> ... ¿Qué pasa? Detecten el ciclo con st_dev y st_ino.