Semana 13 — RAID y la interfaz de archivos

Objetivos

  • Comparar los niveles RAID 0, 1, 4 y 5 en capacidad, confiabilidad y rendimiento.
  • Usar la API de archivos y directorios de Unix y entender inodos (inodes), enlaces duros (hard links) y simbólicos (symbolic links).

Sesión A — RAID

Lecturas:

Temas:

  • Interfaz y modelo de fallos (fail-stop).
  • RAID 0 (striping), RAID 1 (mirroring), RAID 4 (paridad), RAID 5 (paridad rotada).
  • El problema de la escritura pequeña (small-write problem).

Sesión B — Archivos y directorios

Lecturas:

Temas:

  • open, read, write, lseek, fsync, rename, stat, unlink.
  • Tabla de archivos abiertos (open file table), descriptores compartidos tras fork/dup.
  • Directorios, enlaces duros vs. simbólicos, contador de referencias.
  • Permisos, mkfs y mount.

Laboratorio — raid.py y la API de archivos

  1. raid.py: preguntas 1–5 del cap. 38.
  2. Tarea de código del cap. 39: implementar mystat, myls -l, mytail -n y una búsqueda recursiva tipo find.

Entregas

Preguntas de repaso

  1. ¿Cuántas E/S cuesta una escritura pequeña en RAID 4? ¿Por qué RAID 5 mejora el rendimiento de escrituras aleatorias?
  2. ¿Por qué se llama unlink y no delete?
  3. Si dos procesos abren el mismo archivo por separado, ¿comparten el desplazamiento (offset)? ¿Y un padre y un hijo después de fork?

Código de la semana

Compilar dentro del contenedor del curso con make o gcc -Wall.

Ejemplos (3)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

atomic.cDescargar atomic.c
// atomic.c — reemplazar el contenido de un archivo de forma atómica:
// escribir un temporal, fsync, rename, fsync del directorio.
//
// Uso: ./atomic ARCHIVO TEXTO          reemplaza ARCHIVO por TEXTO
//      ./atomic ARCHIVO TEXTO crash    se "cae" antes del rename
//
// Después de un fallo, ARCHIVO tiene el contenido viejo o el nuevo completo,
// nunca una mezcla ni un archivo vacío.
#include <fcntl.h>
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void die(const char *msg) { perror(msg); exit(1); }

int main(int argc, char *argv[]) {
    if (argc < 3) { fprintf(stderr, "uso: %s ARCHIVO TEXTO [crash]\n", argv[0]); return 1; }
    char tmp[4096], dir[4096];
    snprintf(tmp, sizeof tmp, "%s.tmp", argv[1]);
    snprintf(dir, sizeof dir, "%s", argv[1]);

    // 1. Escribir la versión nueva completa en un archivo temporal
    int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) die("open");
    size_t n = strlen(argv[2]);
    if (write(fd, argv[2], n) != (ssize_t)n || write(fd, "\n", 1) != 1)
        die("write");
    // 2. Forzarla al disco: sin esto, el rename podría
    //    llegar al disco antes que los datos
    if (fsync(fd) < 0) die("fsync");
    close(fd);
    printf("escrito y sincronizado: %s\n", tmp);

    if (argc > 3 && strcmp(argv[3], "crash") == 0) {
        printf("*** caída simulada antes de rename ***\n");
        fflush(stdout);
        _exit(1);                     // termina sin limpiar nada
    }

    // 3. rename es atómico: el nombre apunta al inodo
    //    viejo o al nuevo, nunca a una mezcla
    if (rename(tmp, argv[1]) < 0) die("rename");
    // 4. El cambio de nombre vive en el directorio:
    //    sincronizarlo también
    int dfd = open(dirname(dir), O_RDONLY | O_DIRECTORY);
    if (dfd < 0) die("open dir");
    if (fsync(dfd) < 0) die("fsync dir");
    close(dfd);
    printf("rename %s -> %s listo\n", tmp, argv[1]);
    return 0;
}
raid5.cDescargar raid5.c
// raid5.c — un arreglo RAID-5 de juguete en memoria: paridad XOR,
// falla de un disco, reconstrucción y escritura pequeña.
//
// Uso: ./raid5 [DISCO_QUE_FALLA]      (0..4, por defecto 2)
//
// Cinco "discos" de NSTRIPES bloques de BS bytes. Paridad rotada al estilo
// de OSTEP (fig. 38.7): en la franja s la paridad vive en el disco
// N-1 - (s % N) y los datos siguen a su derecha, dando la vuelta.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define N        5                   // discos
#define NSTRIPES 4                   // franjas (stripes)
#define BS       8                   // bytes por bloque
#define NDATA    ((N - 1) * NSTRIPES) // bloques lógicos de datos

static unsigned char disk[N][NSTRIPES][BS];
static int failed = -1;              // disco caído (-1: ninguno)
static int nreads, nwrites;          // E/S físicas contadas

static int parity_disk(int s) { return N - 1 - (s % N); }

// Bloque lógico a -> (disco, franja)
static void map(int a, int *d, int *s) {
    *s = a / (N - 1);
    *d = (parity_disk(*s) + 1 + a % (N - 1)) % N;
}

static void xor_into(unsigned char *dst, const unsigned char *src) {
    for (int i = 0; i < BS; i++)
        dst[i] ^= src[i];
}

static void read_blk(int d, int s, unsigned char *buf) {
    if (d == failed) { fprintf(stderr, "leer disco caído\n"); exit(1); }
    memcpy(buf, disk[d][s], BS);
    nreads++;
}

static void write_blk(int d, int s, const unsigned char *buf) {
    memcpy(disk[d][s], buf, BS);
    nwrites++;
}

// Paridad de la franja s = XOR de sus N-1 bloques de datos.
static void full_parity(int s, unsigned char *p) {
    memset(p, 0, BS);
    for (int d = 0; d < N; d++)
        if (d != parity_disk(s))
            xor_into(p, disk[d][s]);
}

static void print_bits(const char *label, unsigned char c) {
    printf("  %-7s", label);
    for (int b = 7; b >= 0; b--)
        putchar(c >> b & 1 ? '1' : '0');
    printf("  (0x%02x '%c')\n", c, c >= 32 && c < 127 ? c : '.');
}

int main(int argc, char *argv[]) {
    const char *text = "Si un disco falla, la paridad XOR reconstruye sus "
                       "bloques: basta leer los otros cuatro discos de la "
                       "franja y combinarlos con XOR";
    unsigned char saved[NSTRIPES][BS], p[BS];
    int fail = argc > 1 ? atoi(argv[1]) : 2;
    if (fail < 0 || fail >= N) { fprintf(stderr, "disco 0..%d\n", N - 1); return 1; }

    // 1. Escribir los datos franja por franja (escrituras de franja completa)
    for (int a = 0; a < NDATA; a++) {
        int d, s;
        map(a, &d, &s);
        size_t off = (size_t)a * BS;
        memset(disk[d][s], '.', BS);
        if (off < strlen(text)) {
            size_t n = strlen(text) - off;
            memcpy(disk[d][s], text + off, n < BS ? n : BS);
        }
    }
    for (int s = 0; s < NSTRIPES; s++) {
        full_parity(s, p);
        memcpy(disk[parity_disk(s)][s], p, BS);
    }

    printf("Distribución (bloque lógico o P = paridad):\n       ");
    for (int d = 0; d < N; d++) printf("  disco%d", d);
    for (int s = 0; s < NSTRIPES; s++) {
        printf("\n  f%d:  ", s);
        for (int d = 0; d < N; d++) {
            if (d == parity_disk(s)) { printf("      P%d", s); continue; }
            for (int a = 0; a < NDATA; a++) {
                int dd, ss;
                map(a, &dd, &ss);
                if (dd == d && ss == s) printf("  %6d", a);
            }
        }
    }

    // 2. Paridad con bits reales: primer byte de cada bloque de la franja 0
    printf("\n\nFranja 0, primer byte de cada bloque:\n");
    for (int d = 0; d < N; d++) {
        char label[16];
        snprintf(label, sizeof label, d == parity_disk(0) ? "P0" : "D%d", d);
        print_bits(label, disk[d][0][0]);
    }

    // 3. Falla un disco: se pierde todo su contenido
    for (int s = 0; s < NSTRIPES; s++)
        memcpy(saved[s], disk[fail][s], BS);
    memset(disk[fail], 0, sizeof disk[fail]);
    failed = fail;
    printf("\nFalla el disco %d. Reconstrucción:\n", fail);
    nreads = nwrites = 0;
    int ok = 0;
    for (int s = 0; s < NSTRIPES; s++) {
        unsigned char rec[BS] = {0}, tmp[BS];
        // XOR de los N-1 sobrevivientes
        for (int d = 0; d < N; d++) {
            if (d == fail) continue;
            read_blk(d, s, tmp);
            xor_into(rec, tmp);
        }
        int same = memcmp(rec, saved[s], BS) == 0;
        ok += same;
        if (fail == parity_disk(s)) {       // era paridad: se muestra en hex
            printf("  f%d: P%d =", s, s);
            for (int i = 0; i < BS; i++) printf(" %02x", rec[i]);
        } else {
            printf("  f%d: \"%.*s\"", s, BS, rec);
        }
        printf("  %s\n", same ? "== original" : "DISTINTO");
    }
    printf("  %d/%d bloques recuperados, %d lecturas\n", ok, NSTRIPES, nreads);
    for (int s = 0; s < NSTRIPES; s++)      // disco de reemplazo
        memcpy(disk[fail][s], saved[s], BS);
    failed = -1;

    // 4. Escritura pequeña (método sustractivo): bloque lógico 6
    int a = 6, d, s;
    unsigned char old[BS], newb[BS], pold[BS];
    map(a, &d, &s);
    memcpy(newb, "NUEVO!!!", BS);
    nreads = nwrites = 0;
    int pd = parity_disk(s);
    read_blk(d, s, old);      // dato viejo
    read_blk(pd, s, pold);    // paridad vieja
    // P_nueva = P_vieja ^ D_viejo ^ D_nuevo
    xor_into(pold, old);
    xor_into(pold, newb);
    write_blk(d, s, newb);    // dato nuevo
    write_blk(pd, s, pold);   // paridad nueva
    full_parity(s, p);
    printf("\nEscritura pequeña al bloque %d:\n", a);
    printf("  dato en disco %d, paridad en disco %d\n", d, pd);
    printf("  %d lecturas + %d escrituras = %d E/S\n",
           nreads, nwrites, nreads + nwrites);
    printf("  paridad sustractiva %s recalculada\n",
           memcmp(p, disk[pd][s], BS) == 0 ? "==" : "!=");
    return 0;
}
shareoff.cDescargar shareoff.c
// shareoff.c — ¿quién comparte el offset de un archivo abierto?
//
// Uso: ./shareoff ARCHIVO      (lo crea con 26 letras y lo sobrescribe)
//
// Tres casos:
//   fork(): padre e hijo comparten la entrada de la tabla de archivos abiertos.
//   dup():  dos descriptores del mismo proceso, una sola entrada.
//   open() dos veces: dos entradas, cada una con su propio offset.
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

static long where(int fd) { return (long)lseek(fd, 0, SEEK_CUR); }

static void read_n(int fd, int n, const char *who) {
    char buf[32];
    ssize_t r = read(fd, buf, n);
    printf("  %s lee %zd bytes: \"%.*s\"\n", who, r, (int)r, buf);
}

int main(int argc, char *argv[]) {
    if (argc != 2) { fprintf(stderr, "uso: %s ARCHIVO\n", argv[0]); return 1; }
    int fd = open(argv[1], O_CREAT | O_TRUNC | O_WRONLY, 0644);
    if (fd < 0) { perror("open"); return 1; }
    if (write(fd, "abcdefghijklmnopqrstuvwxyz", 26) != 26) { perror("write"); return 1; }
    close(fd);

    printf("1) fork():\n");
    fd = open(argv[1], O_RDONLY);
    fflush(stdout);           // no duplicar el buffer
    pid_t pid = fork();
    if (pid == 0) {           // hijo: lee 10 bytes
        read_n(fd, 10, "hijo ");
        exit(0);
    }
    waitpid(pid, NULL, 0);    // padre: espera al hijo
    printf("  offset del padre: %ld\n", where(fd));
    read_n(fd, 5, "padre");   // sigue donde quedó
    close(fd);

    printf("2) dup():\n");
    fd = open(argv[1], O_RDONLY);
    int fd2 = dup(fd);
    read_n(fd, 5, "fd   ");
    printf("  offset de fd=%d: %ld, de fd2=%d: %ld\n", fd, where(fd), fd2, where(fd2));
    close(fd);
    close(fd2);

    printf("3) open() dos veces:\n");
    int a = open(argv[1], O_RDONLY);
    int b = open(argv[1], O_RDONLY);
    read_n(a, 5, "a    ");
    printf("  offset de a=%d: %ld, de b=%d: %ld\n", a, where(a), b, where(b));
    read_n(b, 5, "b    ");
    close(a);
    close(b);
    return 0;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 13: RAID y la interfaz de archivos.
#   make         compila todo
#   make test    arma un árbol de prueba en /tmp/w13 y corre cada programa
#   make clean   borra los binarios
CFLAGS = -Wall -Wextra -g
PROGS  = raid5 shareoff atomic mystat myls mytail myfind
# Las soluciones aún no publicadas (code/.ocultos) no están en el repositorio:
# se omiten los programas sin fuente y sus líneas en las pruebas.
PROGS := $(foreach p,$(PROGS),$(if $(wildcard $(p).c),$(p)))
RUN    = $(if $(wildcard $(1).c),./$(1),@: sin $(1).c)
T      = /tmp/w13

all: $(PROGS)

$(PROGS): %: %.c
	$(CC) $(CFLAGS) -o $@ $<

# Árbol de prueba: archivo, enlace duro, enlace simbólico, enlace roto,
# subdirectorios y un archivo de 2000 líneas.
setup:
	rm -rf $(T)
	mkdir -p $(T)/docs/notas $(T)/src
	seq -f "línea %g" 1 2000 > $(T)/log.txt
	echo hola > $(T)/docs/a.txt
	ln $(T)/docs/a.txt $(T)/docs/b.txt
	ln -s a.txt $(T)/docs/sim
	ln -s nada.txt $(T)/docs/roto
	touch $(T)/src/main.c $(T)/src/util.c $(T)/docs/notas/clase.md
	chmod 600 $(T)/docs/notas/clase.md

test: $(PROGS) setup
	./raid5 2
	./shareoff $(T)/off.txt
	$(call RUN,mystat) $(T)/docs/a.txt $(T)/docs/sim
	$(call RUN,mystat) $(T)/docs
	$(call RUN,myls) $(T)/docs
	$(call RUN,myls) -l $(T)/docs
	$(call RUN,mytail) -3 $(T)/log.txt
	$(call RUN,myfind) $(T)
	$(call RUN,myfind) $(T) -name '*.c'
	$(call RUN,myfind) $(T) -type l
	./atomic $(T)/config.txt "version 1"
	./atomic $(T)/config.txt "version 2" crash || true
	cat $(T)/config.txt
	ls $(T)
	./atomic $(T)/config.txt "version 2"
	cat $(T)/config.txt

clean:
	rm -f $(PROGS)

.PHONY: all setup test clean