1. Definir qué significa guardar
En el tema 14 seguimos las páginas sucias hasta su writeback. El kernel puede aceptar modificaciones, agruparlas y propagarlas después. Ahora añadiremos un requisito de la aplicación: identificar un punto a partir del cual se puede comunicar que una actualización quedó confirmada dentro del contrato de almacenamiento elegido.
Durabilidad: propiedad por la que un cambio confirmado puede recuperarse después del tipo de fallo contemplado. Para razonar sobre ella hay que precisar qué se confirma, mediante qué operaciones y bajo qué supuestos del sistema de archivos y del dispositivo.
“Guardado” puede significar varias cosas: el texto salió de la interfaz, la biblioteca lo entregó al kernel, el archivo tiene datos sincronizados o el nombre que lo identifica también quedó confirmado. El programa debe elegir qué promesa hace antes de devolver éxito. Los mecanismos de este tema permiten construir esa promesa, pero no convierten cualquier secuencia de escrituras en una transacción.
| Propiedad | Pregunta concreta | Ejemplo |
|---|---|---|
| Visibilidad | ¿Qué ve otro lector ahora? | Una lectura obtiene el contenido nuevo desde caché. |
| Durabilidad | ¿Qué cambio confirmado se recupera tras el fallo previsto? | El programa espera una sincronización y comprueba su resultado. |
| Atomicidad | ¿La actualización se observa completa o puede verse a medias? | Se publica un archivo terminado reemplazando un nombre. |
| Orden | ¿Qué trabajo debe confirmarse antes de publicar el siguiente? | Primero se sincroniza el temporal; luego se publica su nombre. |
Un archivo modificado en el lugar puede quedar parcialmente actualizado ante un fallo, aunque el programa planease llamar a fsync() al final. Si el fallo ocurre antes de completar esa llamada, todavía no se alcanzó el punto de confirmación. Además, otros procesos podrían haber leído los cambios mientras se estaban realizando.
La atomicidad de operaciones complejas requiere un protocolo adicional. Como referencia de una aplicación real, la documentación de commit atómico de SQLite explica cómo combina pasos de escritura, sincronización y recuperación. Aquí estudiaremos un caso más pequeño: un archivo de datos y la publicación de un nombre dentro de un directorio.
2. fflush, cierre y capas de memoria
Si usamos FILE *, parte de la salida puede seguir en un búfer de la biblioteca. Mientras esos bytes no lleguen al descriptor, una sincronización del archivo no puede confirmar su contenido. El primer paso consiste en entregar correctamente la salida de usuario al kernel.
fwrite / fprintf
│
▼
búfer de stdio en el proceso
│ fflush: entrega la salida pendiente
▼
archivo y caché del kernel
│ fsync / fdatasync: sincronizan el archivo según su alcance
▼
almacenamiento y sus mecanismos de persistencia
entrada del directorio: vincula un nombre con un archivo
│ fsync del descriptor del directorio
▼
estado persistente del nombre, bajo el contrato del sistema de archivosfflush() sobre un flujo de salida entrega la salida pendiente de la biblioteca y conserva el flujo abierto. No sustituye la sincronización del archivo. Esta separación se detalla en fflush(3).
/* fp es un flujo de salida válido, controlado por este proceso. */
if (fflush(fp) == EOF) {
/* Falló la entrega: no informar confirmación. */
} else {
int fd = fileno(fp);
if (fd == -1) {
/* No se obtuvo el descriptor. */
} else {
int r;
do { r = fsync(fd); } while (r == -1 && errno == EINTR);
if (r == -1) {
/* No se confirmó la sincronización. */
} else {
/* Archivo sincronizado; revisar aparte el nombre si cambió. */
}
}
}
/* Todavía se debe comprobar fclose(fp); no reutilizar fp tras cerrarlo. */El fragmento muestra el orden, no una función completa de gestión de recursos. Un programa real conserva el estado de error y cierra el flujo también en las ramas de fracaso. No debe llamar a close(fileno(fp)) y después a fclose(fp): la biblioteca sigue siendo responsable de ese descriptor mientras el flujo esté abierto.
Un cierre correcto libera recursos, pero no equivale a una confirmación de persistencia. En Linux se comprueba el retorno de close() y no se reintenta ciegamente el mismo número: puede haber quedado disponible para otro descriptor. El manual de close(2) explica tanto el reporte de errores como ese riesgo.
Si hay cinco líneas en el búfer de stdio y llamas a fsync(fileno(fp)) antes de fflush(fp), esas líneas todavía no forman parte del trabajo que el kernel recibió. La secuencia debe corresponder a las capas que realmente utilizaste.
3. fsync frente a fdatasync
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
/* Ambas: 0 si tienen éxito; -1 con errno si fallan. */fsync() sincroniza los datos modificados del archivo y sus metadatos. fdatasync() puede omitir metadatos que no sean necesarios para recuperar los datos. Por ejemplo, el tamaño necesario para recuperar una ampliación del archivo sí importa; una marca de tiempo puede quedar fuera de esa obligación. Ambas esperan la finalización correspondiente y deben comprobarse.
La entrada del directorio no queda automáticamente confirmada al sincronizar el archivo. Estas diferencias de alcance y los posibles errores se documentan en fsync(2) y fdatasync(2).
| Necesidad | Elección a considerar | Lo que debe recordarse |
|---|---|---|
| Recuperar el contenido y su longitud | fdatasync(fd) | “Datos” incluye los metadatos necesarios para recuperarlos. |
| Confirmar también metadatos del archivo | fsync(fd) | El nombre del directorio requiere atención separada. |
| Publicar un nombre nuevo o reemplazado | Protocolo de archivo y directorio | El orden de las operaciones forma parte de la solución. |
Una diferencia semántica no garantiza una diferencia de velocidad
Si tu carga necesita modificar el tamaño, asignar bloques o completar otra información imprescindible, el trabajo exigido por ambas llamadas puede parecerse mucho. También puede ocurrir que la implementación use mecanismos similares para las dos. No hay una regla que permita declarar de antemano cuántos milisegundos ahorrará fdatasync().
Para elegir, primero escribe qué estado necesita recuperar la aplicación. Después mide políticas que cumplan ese requisito. Comparar únicamente el número más pequeño sin revisar el contrato puede enfrentar operaciones que confirman cosas distintas.
Los ejemplos de este tema son de un solo escritor. Si otro proceso o hilo modifica el archivo de forma concurrente, se necesita coordinación para definir qué conjunto de operaciones corresponde a cada confirmación. Una llamada de sincronización no reemplaza un bloqueo de aplicación ni crea una instantánea transaccional del archivo.
4. sync y syncfs: elegir el alcance
/* En glibc, habilitar las declaraciones antes de incluir cabeceras:
_DEFAULT_SOURCE para sync; _GNU_SOURCE para syncfs. */
#include <unistd.h>
void sync(void);
int syncfs(int fd);sync() actúa sobre los sistemas de archivos del sistema; su interfaz no devuelve un código de error. En Linux espera la finalización de la E/S correspondiente. El alcance permitido por POSIX no exige esa misma espera, de modo que la afirmación sobre la finalización debe identificarse como comportamiento de Linux.
syncfs(fd) limita la sincronización al sistema de archivos que contiene el archivo referido por el descriptor. Devuelve cero o -1 con errno; sus errores pueden referirse a otros archivos de ese sistema de archivos. La referencia de sync(2) describe el alcance y las diferencias de reporte entre versiones de Linux.
| Operación | Alcance de interés | Uso en este tema |
|---|---|---|
fsync / fdatasync | El archivo referido por el descriptor. | Confirmar y medir trabajo de los archivos propios. |
syncfs | El sistema de archivos que contiene el descriptor. | Conocer el alcance; no usarlo para aislar un archivo del ensayo. |
sync | Alcance global. | Conocer la interfaz; no introducir trabajo global en las mediciones. |
El comando de shell llamado sync y la función C son interfaces distintas. Un comando puede interpretar opciones y rutas según la herramienta instalada. Los programas del laboratorio llaman directamente a las funciones de archivo; no dependen de que una orden externa tenga una opción concreta.
Si una operación modifica un único archivo, una llamada global añade actividad y dificulta atribuir el costo. Además, el éxito aparente de una interfaz sin retorno de error no ofrece a la aplicación un diagnóstico específico de su actualización. Elegir un alcance adecuado ayuda tanto al diseño del protocolo como a la interpretación del experimento.
5. O_SYNC y O_DSYNC
También se puede solicitar una política de finalización al abrir el archivo. Con O_DSYNC, las escrituras tienen finalización sincronizada de integridad de datos; con O_SYNC, la obligación abarca integridad del archivo, incluidos sus metadatos correspondientes. El manual de open(2) describe estas banderas y su relación.
int datos = open("datos.nuevos",
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_DSYNC, 0600);
int completo = open("completo.nuevo",
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_SYNC, 0600);
/* Comprobar cada open; atender transferencias parciales;
comprobar cada write y cada close. Son dos archivos distintos. */El costo de confirmación aparece dentro de las escrituras. Si se mide solamente una llamada explícita al final, se puede pasar por alto la mayor parte de la espera que ya pagó el escritor. En cambio, con una política bufferizada y un único fsync final, buena parte del costo puede concentrarse en ese punto o avanzar previamente en segundo plano.
O_SYNC no vacía mágicamente el búfer de stdio: hasta que la biblioteca entregue los bytes, no hay escritura del descriptor que pueda beneficiarse de la bandera. Tampoco sustituye el protocolo de directorio de una publicación ni transforma varias escrituras en una operación indivisible.
O_DIRECT responde otra pregunta: cómo participa la caché de páginas. No se debe usar como sinónimo de confirmación durable. El tema 16 estudiará esa modalidad y sus requisitos; aquí conservaremos E/S bufferizada y compararemos cuándo se espera la sincronización.
Si la biblioteca agrupa muchos registros en una escritura, la política de la bandera se aplica a esa escritura. Si el programa divide una transferencia en varias llamadas, cambia la frecuencia de puntos de espera. Por eso el tamaño del bloque debe acompañar cualquier resultado de rendimiento.
6. Puntos de confirmación y lotes
Un registro de aplicación no es automáticamente una llamada al kernel. Para comparar políticas, fijaremos registros de 4096 bytes y un solo escritor. Después elegiremos si esperar por cada registro, por un conjunto de ocho o al completar toda la secuencia.
por registro: W S W S W S W S W S W S W S W S ...
por lote de 8: W W W W W W W W S W W W W W W W W S
al final: W W W W W W W W W W W W W W W W S
W = entregar un registro completo, resolviendo transferencias parciales
S = esperar la confirmación elegida y comprobar el resultadoUna política por lote puede compartir trabajo entre registros. A cambio, el programa no debe confirmar cada registro al recibirlo si todavía no alcanzó el punto de sincronización del lote. La respuesta al usuario o al cliente debe reflejar el momento real de confirmación.
Supongamos un servicio que recibió ocho solicitudes. Puede escribir sus registros, sincronizar una vez y responder después a esas ocho. Si responde antes de la sincronización y luego cae, habría prometido más de lo que confirmó. Si sincroniza pero cae antes de enviar la respuesta, el cliente puede no saber que la operación se completó; el protocolo necesita resolver reintentos y posibles duplicados.
Confirmación no implica que todos los cambios anteriores al fallo sean invisibles
Un registro no confirmado puede haber llegado ya al almacenamiento gracias al writeback. “Todavía no confirmado” significa que la aplicación no puede prometer su durabilidad mediante el punto esperado; no significa “seguro que desaparecerá”. La recuperación debe distinguir registros completos y válidos, no asumir que el sistema conserva exactamente el último número al que respondió.
Por eso un log real suele necesitar formato, longitudes, comprobaciones de integridad y reglas de recuperación. El benchmark que sigue escribe contenido determinista para medir puntos de espera; no implementa un formato transaccional ni una recuperación de registros interrumpidos.
El rendimiento agregado indica cuántos bytes o registros se completan por unidad de tiempo. La latencia de confirmación indica cuánto espera una operación concreta. Agrupar puede mejorar el primero mientras aumenta la espera de un registro que llega al principio de un lote.
7. Errores y estados de resultado
Hay que comprobar las escrituras y las sincronizaciones. Un error de almacenamiento puede comunicarse después de que una entrega anterior haya tenido éxito. No corresponde interpretar un fracaso como si el contenido hubiera vuelto automáticamente a su versión anterior.
| Situación | Decisión del programa | Estado posible |
|---|---|---|
| Escritura parcial positiva | Continuar con los bytes restantes. | Parte del contenido ya se aplicó. |
EINTR en la sincronización | Repetir la operación de sincronización del ejemplo. | No anunciar éxito hasta obtenerlo. |
| Fallo de E/S, espacio o cuota | Informar fracaso y conservar el contexto. | Puede haber cambios visibles o parcialmente persistidos. |
| Fallo después de publicar el nombre | Distinguirlo de un fallo anterior a publicar. | El nombre nuevo puede estar visible; su confirmación puede faltar. |
| Fallo al escribir el informe | Informar error del programa. | No deshacer datos que ya se confirmaron. |
El código usará una función de cierre que invalida la variable local antes de llamar a close(). Así evita reutilizar el número por accidente durante la limpieza. Un error se conserva como fracaso; no se ejecuta un segundo cierre del mismo número para intentar arreglarlo.
También hay que comprobar que el sistema de archivos admite las operaciones necesarias. Si la sincronización de directorio falla o no está soportada, un protocolo que la necesita no puede anunciar su garantía completa. La solución no consiste en ignorar el error para que el ejemplo termine con un mensaje optimista.
Una aplicación con varios escritores necesita definir quién controla los descriptores y las modificaciones durante la confirmación. Los ejemplos trabajan sobre archivos nuevos y directorios privados para mantener un alcance claro; no son un servicio de almacenamiento compartido ni una biblioteca de recuperación general.
8. Medir seis políticas en C
El experimento usa archivos nuevos con O_EXCL, entre 1 y 256 registros de 4096 bytes, y un máximo de 1 MiB por ejecución. El contenido de cada registro se prepara antes del cronómetro: A para el primero, B para el segundo, hasta repetir el alfabeto. No se usa compresión explícita ni se atribuyen los resultados a un ancho de banda físico.
| Modo | Durante la secuencia | Al final de todos los modos |
|---|---|---|
final | Escrituras bufferizadas sin puntos explícitos intermedios. | Se comprueba un fsync final del archivo, su tamaño y el cierre. El nombre del directorio queda fuera de esta medición. |
fsync | fsync después de cada registro. | |
fdatasync | fdatasync después de cada registro. | |
lote | fsync por cada ocho registros y para el resto final. | |
osync | Apertura con O_SYNC. | |
odsync | Apertura con O_DSYNC. |
El fsync final común fija un punto comparable al terminar el archivo. En algunos modos ya se confirmó gran parte del trabajo y esa llamada puede ser corta; se mide igualmente. En los modos de integridad de datos, la confirmación final también cubre metadatos del archivo que no se exigían en cada registro.
#ifndef DURABILIDAD_H
#define DURABILIDAD_H
#include <errno.h>
#include <stddef.h>
#include <unistd.h>
static int entregar(int fd, const unsigned char *buf, size_t n,
unsigned long *llamadas)
{
size_t hecho = 0;
while (hecho < n) {
ssize_t k;
if (llamadas) ++*llamadas;
k = write(fd, buf + hecho, n - hecho);
if (k > 0) { hecho += (size_t)k; continue; }
if (k == -1 && errno == EINTR) continue;
if (k == 0) errno = EIO;
return -1;
}
return 0;
}
static int asegurar(int fd, int solo_datos)
{
int r;
do { r = solo_datos ? fdatasync(fd) : fsync(fd); }
while (r == -1 && errno == EINTR);
return r;
}
static int cerrar(int *fd)
{
int anterior = *fd;
*fd = -1;
return anterior == -1 ? 0 : close(anterior);
}
#endif#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include "durabilidad.h"
enum { REGISTRO = 4096, LOTE = 8 };
static const char *modos[] = {"final", "fsync", "fdatasync", "lote", "osync", "odsync"};
static long cantidad(const char *s)
{
char *fin;
long n;
if (!s[0] || strspn(s, "0123456789") != strlen(s)) return -1;
errno = 0;
n = strtol(s, &fin, 10);
return errno || *fin || n < 1 || n > 256 ? -1 : n;
}
static double reloj(void)
{
struct timespec t;
if (clock_gettime(CLOCK_MONOTONIC, &t) == -1) {
perror("clock_gettime"); exit(EXIT_FAILURE);
}
return (double)t.tv_sec + (double)t.tv_nsec / 1e9;
}
int main(int argc, char **argv)
{
long registros;
int modo = -1, fd = -1, estado = EXIT_FAILURE, flags;
unsigned char *datos;
unsigned long llamadas = 0, puntos = 0;
struct stat st;
double inicio, t, dt, write_s = 0, puntos_s = 0, final_s, total_s, max_write = 0;
if (argc != 4 || (registros = cantidad(argv[3])) == -1) {
fprintf(stderr, "Uso: %s NUEVO modo registros(1..256)\n", argv[0]);
return EXIT_FAILURE;
}
for (int i = 0; i < 6; ++i) if (strcmp(argv[2], modos[i]) == 0) modo = i;
if (modo == -1) {
fprintf(stderr, "Modos: final fsync fdatasync lote osync odsync\n");
return EXIT_FAILURE;
}
datos = malloc((size_t)registros * REGISTRO);
if (!datos) { perror("malloc"); return EXIT_FAILURE; }
for (long i = 0; i < registros; ++i)
memset(datos + (size_t)i * REGISTRO, 'A' + (int)(i % 26), REGISTRO);
flags = O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC;
if (modo == 4) flags |= O_SYNC;
if (modo == 5) flags |= O_DSYNC;
fd = open(argv[1], flags, 0600);
if (fd == -1) { perror("crear nuevo"); goto salir; }
inicio = reloj();
for (long i = 0; i < registros; ++i) {
t = reloj();
if (entregar(fd, datos + (size_t)i * REGISTRO, REGISTRO, &llamadas) == -1) {
perror("write"); goto salir;
}
dt = reloj() - t;
write_s += dt;
if (dt > max_write) max_write = dt;
if (modo == 1 || modo == 2 ||
(modo == 3 && ((i + 1) % LOTE == 0 || i + 1 == registros))) {
t = reloj();
if (asegurar(fd, modo == 2) == -1) { perror("punto sync"); goto salir; }
puntos_s += reloj() - t;
++puntos;
}
}
t = reloj();
if (asegurar(fd, 0) == -1) { perror("fsync final"); goto salir; }
final_s = reloj() - t;
total_s = reloj() - inicio;
if (fstat(fd, &st) == -1) { perror("fstat"); goto salir; }
if (st.st_size != (off_t)registros * REGISTRO) {
fprintf(stderr, "Tamaño visible inesperado\n"); goto salir;
}
if (cerrar(&fd) == -1) { perror("close"); goto salir; }
if (printf("modo,bloques,bytes,writes,puntos,write_s,puntos_s,final_s,total_s,max_write_s\n"
"%s,%ld,%ld,%lu,%lu,%.9f,%.9f,%.9f,%.9f,%.9f\n",
modos[modo], registros, registros * REGISTRO, llamadas, puntos,
write_s, puntos_s, final_s, total_s, max_write) < 0 ||
fflush(stdout) == EOF) {
fprintf(stderr, "No se pudo escribir el CSV\n"); goto salir;
}
estado = EXIT_SUCCESS;
salir:
if (cerrar(&fd) == -1) { perror("close en limpieza"); estado = EXIT_FAILURE; }
free(datos);
return estado;
}Qué significan las columnas
write_s suma el tiempo de entregar registros completos, incluidos reintentos y esperas dentro de las escrituras. puntos_s suma las sincronizaciones explícitas intermedias. final_s mide la sincronización final común. total_s cubre desde antes del primer registro hasta después de esa sincronización, incluidos el control del bucle y las lecturas del reloj.
La creación del archivo, preparación de los datos, comprobación de tamaño, cierre y salida CSV quedan fuera de total_s. No es el tiempo completo del proceso ni un protocolo de publicación del nombre. El resultado CSV se emite después de comprobar el cierre para que una ejecución fallida no produzca una fila de éxito completa.
writes cuenta intentos reales de llamada: puede superar el número de registros por interrupciones o transferencias parciales. puntos cuenta confirmaciones explícitas intermedias exitosas, sin contar el fsync final. En osync y odsync puede ser cero aunque cada escritura haya pagado sincronización.
max_write_s es la mayor ventana de entrega de un registro, no el percentil 99 ni la máxima latencia total de confirmación de una solicitud de servicio. En los modos con puntos explícitos, esa columna no incluye la sincronización que viene después; en los modos con banderas, la espera correspondiente ya forma parte de la escritura.
9. Laboratorio y análisis de resultados
Preparar un espacio propio
Elige una ubicación local con espacio disponible. Crea el directorio de ensayo, entra y guarda durabilidad.h, medir-sync.c y, más adelante, publicar-config.c y analizar-sync.py. Las ejecuciones de medición solo crean archivos nuevos; no requieren privilegios ni modifican parámetros del sistema.
$ LAB15=$(mktemp -d ./tema15.XXXXXX)
$ cd "$LAB15"
Guarda aquí los archivos de código del tema antes de compilarlos.
$ uname -r
$ findmnt -T .
$ df -h .
$ gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror medir-sync.c -o medir-syncUn montaje en memoria permite estudiar interfaces, pero no constituye una prueba de persistencia ante pérdida de energía. En una VM o en WSL, registra también qué ruta estás usando. El montaje visible al invitado no describe por sí solo todas las cachés y garantías del anfitrión.
Dieciocho muestras con orden alternado
Usaremos tres rondas de seis modos y 32 registros por archivo. Son 128 KiB por ejecución y 2,25 MiB de datos finales entre todos los archivos. Se altera el orden de cada ronda para no ejecutar siempre la misma política en la misma posición; esto reduce una fuente de sesgo, pero no elimina la influencia del entorno.
$ for ronda in 1 2 3; do case "$ronda" in 1) modos='final fsync fdatasync lote osync odsync';; 2) modos='odsync osync lote fdatasync fsync final';; 3) modos='lote final odsync fsync osync fdatasync';; esac; for modo in $modos; do ./medir-sync "r${ronda}-${modo}.bin" "$modo" 32 > "r${ronda}-${modo}.csv" || exit 1; done; doneSi una ejecución falla, detén el análisis de esa serie. Los nombres existentes se rechazan para evitar sobrescrituras. Para repetir el barrido, crea otro directorio nuevo. No borres resultados para seleccionar únicamente una muestra favorable.
import csv
import math
from pathlib import Path
import statistics
import sys
MODOS = ("final", "fsync", "fdatasync", "lote", "osync", "odsync")
CAMPOS = ("modo", "bloques", "bytes", "writes", "puntos", "write_s",
"puntos_s", "final_s", "total_s", "max_write_s")
def analizar(carpeta):
esperados = {f"r{r}-{m}.csv" for r in range(1, 4) for m in MODOS}
encontrados = {p.name for p in carpeta.glob("r*-*.csv")}
if encontrados != esperados:
raise ValueError("se necesitan exactamente las 18 muestras r1..r3 de los seis modos")
grupos = {m: [] for m in MODOS}
tamanos = set()
for ronda in range(1, 4):
for modo in MODOS:
ruta = carpeta / f"r{ronda}-{modo}.csv"
with ruta.open(newline="", encoding="utf-8") as archivo:
lector = csv.DictReader(archivo)
if tuple(lector.fieldnames or ()) != CAMPOS:
raise ValueError(f"{ruta.name}: cabecera incorrecta")
filas = list(lector)
if len(filas) != 1 or filas[0]["modo"] != modo:
raise ValueError(f"{ruta.name}: se esperaba una fila de su modo")
f = filas[0]
n, b, w, p = (int(f[k]) for k in ("bloques", "bytes", "writes", "puntos"))
esperado = n if modo in ("fsync", "fdatasync") else (n + 7) // 8 if modo == "lote" else 0
if not 1 <= n <= 256 or b != n * 4096 or w < n or p != esperado:
raise ValueError(f"{ruta.name}: contadores inconsistentes")
tiempos = {k: float(f[k]) for k in CAMPOS[5:]}
if any(not math.isfinite(v) or v < 0 for v in tiempos.values()):
raise ValueError(f"{ruta.name}: tiempo inválido")
if tiempos["total_s"] <= 0:
raise ValueError(f"{ruta.name}: intervalo total no positivo")
suma = tiempos["write_s"] + tiempos["puntos_s"] + tiempos["final_s"]
if suma > tiempos["total_s"] + 1e-6 or tiempos["max_write_s"] > tiempos["write_s"] + 1e-6:
raise ValueError(f"{ruta.name}: ventanas incompatibles")
tamanos.add(n)
grupos[modo].append(tiempos)
if len(tamanos) != 1:
raise ValueError("las muestras no tienen la misma cantidad de registros")
n = tamanos.pop()
print(f"18 muestras completas; {n} registros de 4096 bytes por ejecución")
print("modo total mediana ms write ms puntos ms final ms rango total ms")
for modo, filas in grupos.items():
mediana = lambda k: statistics.median(f[k] for f in filas) * 1000
totales = [f["total_s"] * 1000 for f in filas]
print(f"{modo:10} {mediana('total_s'):16.4f} {mediana('write_s'):10.4f} "
f"{mediana('puntos_s'):11.4f} {mediana('final_s'):10.4f} "
f"{min(totales):.4f}..{max(totales):.4f}")
print("Son ventanas del mismo ensayo; las medianas de columnas no tienen por qué sumar.")
print("Tres muestras por modo describen este ensayo, no una distribución de colas de latencia.")
if len(sys.argv) != 2:
sys.exit("Uso: python3 analizar-sync.py DIRECTORIO")
try:
analizar(Path(sys.argv[1]))
except (OSError, ValueError, KeyError, TypeError, csv.Error) as error:
sys.exit(f"Serie inválida: {error}")$ python3 analizar-sync.py .
$ python3 -c 'from pathlib import Path; modos=("final","fsync","fdatasync","lote","osync","odsync"); esperado=b"".join(bytes([65+i%26])*4096 for i in range(32)); assert all(Path(f"r{r}-{m}.bin").read_bytes()==esperado for r in range(1,4) for m in modos); print("18 archivos con tamaño y contenido visibles correctos")'Cómo interpretar el costo medido
- Mira primero total_s. Todos los modos terminan en un punto final comparable del archivo; comparar solo write_s ocultaría las esperas explícitas.
- Localiza las esperas. En O_SYNC y O_DSYNC están dentro de write_s; en fsync, fdatasync y lote aparecen también en puntos_s.
- Revisa el rango. Una mediana con solo tres muestras resume ese barrido. Un resultado variable merece más contexto, no una conclusión universal.
- Compara contratos. Las obligaciones durante la secuencia son distintas, aunque haya una sincronización final común.
- Explica el alcance. Se mide una carga pequeña, creciente y de un escritor. No se mide el servicio bajo concurrencia ni una publicación durable de nombres.
No se incluyen tiempos numéricos prefabricados: el resultado medido es la tabla que produzca tu equipo. Es razonable investigar si agrupar reduce el costo, pero el ejercicio no exige que una política gane ni que fdatasync resulte siempre más rápido que fsync.
El archivo crece durante el ensayo. Reescribir un archivo ya asignado sería otro experimento: cambia las necesidades de asignación y de actualización del tamaño. Para comparar ambas cargas habría que preparar archivos equivalentes y separar esa preparación del cronómetro, sin mezclar resultados como si fueran la misma prueba.
Una finalización rápida o lenta no revela por sí sola cómo se implementó la persistencia. El montaje, las cachés del anfitrión y el cumplimiento del contrato del dispositivo siguen siendo parte de las condiciones del resultado.
10. Durabilidad del nombre y publicación
Un archivo tiene contenido y metadatos; un directorio mantiene vínculos entre nombres y archivos. Si se crea o reemplaza una entrada, confirmar el contenido del archivo no responde todavía si ese vínculo quedó confirmado. Esta diferencia aparece al guardar configuraciones, índices o documentos mediante un temporal.
Un protocolo en un mismo directorio
- Crear un temporal exclusivo. Se trabaja en un directorio privado y en el mismo sistema de archivos que el destino.
- Escribir su contenido completo. Se atienden las transferencias parciales y los errores. Si se usa stdio, primero se entrega su búfer.
- Sincronizar el temporal. Se comprueba el punto de confirmación del archivo antes de exponerlo como versión nueva.
- Reemplazar el nombre con rename. La operación publica el archivo terminado bajo el nombre elegido.
- Sincronizar el directorio. Se confirma el cambio de entrada antes de anunciar el resultado completo del protocolo.
rename() puede reemplazar un destino existente de forma atómica para las búsquedas del nombre: no introduce un momento intermedio en el que se haya quitado el destino y todavía falte poner el nuevo. Los descriptores que ya estaban abiertos siguen refiriéndose a su archivo anterior. Estas propiedades están descritas en rename(2). No equivalen por sí solas a confirmar el cambio frente a una caída.
config.txt ───────────────→ archivo anterior
.config.tmp ──────────────→ archivo nuevo, todavía en preparación
entregar todo → fsync(temporal) → comprobar cierre → renameat(...)
│
config.txt ─────────────────────────────────────────────┘→ archivo nuevo
│
fsync(directorio) → confirmar a la aplicaciónEl ejemplo mantiene ambos nombres en el mismo directorio para reducir el protocolo a un cambio de ese directorio. Mover entre directorios añade estados que considerar; mover entre sistemas de archivos puede fallar con EXDEV y no se resuelve reemplazando la operación por una copia y borrado sin revisar las garantías.
El protocolo parte de un directorio ya existente cuya ruta se considera estable y durable. Si acabas de crearlo y necesitas prometer también que sobrevivirá su aparición, debes tratar la entrada de ese directorio en su padre. No basta repetir “fsync del archivo” para confirmar todos los antecesores de una ruta nueva.
Reemplazar el archivo por un nuevo objeto tampoco conserva automáticamente propietario, permisos, ACL, atributos extendidos ni enlaces duros de la versión anterior. Una aplicación debe definir qué propiedades forman parte de la versión publicada y prepararlas antes del punto de confirmación correspondiente.
11. Programa de publicación controlada
publicar-config.c recibe un directorio privado existente y un texto de 1 a 4096 bytes. Sus nombres internos son fijos: .config.tmp y config.txt. El segundo es el destino que se reemplaza de manera intencional. Utiliza el descriptor del directorio con openat y renameat para que las operaciones relativas mantengan ese punto de referencia.
El directorio debe pertenecer al usuario efectivo y no tener permisos para grupo ni otros. Aun así, el ejemplo presupone que ningún otro proceso modifica ese espacio durante la ejecución; la comprobación no implementa un mecanismo de exclusión entre procesos del mismo usuario ni una defensa general ante cambios maliciosos.
#define _POSIX_C_SOURCE 200809L
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include "durabilidad.h"
int main(int argc, char **argv)
{
int dir = -1, fd = -1, publicado = 0, confirmado = 0, estado = 1;
size_t n;
struct stat st;
if (argc != 3 || (n = strlen(argv[2])) == 0 || n > 4096) {
fprintf(stderr, "Uso: %s DIRECTORIO_PRIVADO TEXTO(1..4096 bytes)\n", argv[0]);
return 1;
}
dir = open(argv[1], O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
if (dir == -1) { perror("abrir directorio"); goto salir; }
if (fstat(dir, &st) == -1) { perror("fstat directorio"); goto salir; }
if (st.st_uid != geteuid() || (st.st_mode & 077) != 0) {
fprintf(stderr, "Se exige directorio propio sin permisos de grupo u otros\n");
goto salir;
}
if (fstatat(dir, "config.txt", &st, AT_SYMLINK_NOFOLLOW) == 0) {
if (!S_ISREG(st.st_mode)) {
fprintf(stderr, "El destino existente debe ser un archivo regular\n"); goto salir;
}
} else if (errno != ENOENT) { perror("examinar destino"); goto salir; }
fd = openat(dir, ".config.tmp", O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
if (fd == -1) { perror("crear temporal exclusivo"); goto salir; }
if (entregar(fd, (const unsigned char *)argv[2], n, NULL) == -1) {
perror("escribir temporal"); goto salir;
}
if (asegurar(fd, 0) == -1) { perror("fsync temporal"); goto salir; }
if (cerrar(&fd) == -1) { perror("close temporal"); goto salir; }
if (renameat(dir, ".config.tmp", dir, "config.txt") == -1) {
perror("renameat"); goto salir;
}
publicado = 1;
if (asegurar(dir, 0) == -1) { perror("fsync directorio"); goto salir; }
confirmado = 1;
estado = 0;
salir:
if (cerrar(&fd) == -1) { perror("close temporal en limpieza"); estado = 1; }
if (cerrar(&dir) == -1) { perror("close directorio"); estado = 1; }
if (estado != 0) {
if (publicado) {
fprintf(stderr, "Nombre nuevo publicado; confirmación de directorio=%s. "
"No se intenta revertir.\n", confirmado ? "completada" : "no completada");
return 2;
}
fprintf(stderr, "No se publicó el nombre nuevo; si se creó el temporal, se conserva.\n");
return 1;
}
if (puts("Archivo y cambio de nombre confirmados bajo el contrato del montaje") == EOF ||
fflush(stdout) == EOF) {
fprintf(stderr, "Publicación confirmada, pero falló el informe de salida\n");
return 2;
}
return 0;
}Probar dos versiones en un directorio del laboratorio
$ gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror publicar-config.c -o publicar-config
$ mkdir -m 700 configuracion
$ ./publicar-config configuracion 'color=azul'
$ ./publicar-config configuracion 'color=verde'
$ python3 -c 'from pathlib import Path; p=Path("configuracion"); assert (p/"config.txt").read_bytes()==b"color=verde"; assert not (p/".config.tmp").exists(); print("Configuración visible correcta; temporal consumido por rename")'El directorio configuracion se crea para aislar el ensayo. El programa confirma su cambio interno, pero no sincroniza la entrada de ese directorio en su padre: el comando no pretende demostrar la publicación durable de una jerarquía recién creada. Para usar el protocolo en una aplicación, parte de un directorio durable o añade la preparación de su padre a la especificación.
Tres resultados que la aplicación debe distinguir
| Salida | Significado en este programa | Acción de diagnóstico |
|---|---|---|
0 | Se completaron archivo, reemplazo, directorio, cierres e informe. | Se alcanzó el compromiso declarado bajo los supuestos del montaje. |
1 | No se llegó a publicar el nombre nuevo. | Examinar el error y un eventual temporal. No se borra automáticamente. |
2 | Se publicó el nombre y después hubo un error. | Leer el diagnóstico: puede faltar confirmación o haber fallado solo el informe. |
Si existe .config.tmp, el programa se detiene en lugar de truncarlo o asumir que es suyo. Un servicio real necesita nombres temporales y reglas de recuperación adecuados a su concurrencia. La versión didáctica conserva un solo temporal reconocible para que se pueda inspeccionar el punto de fracaso.
Si falla fsync del directorio después del reemplazo, el nuevo contenido puede estar visible. Intentar restaurar el archivo anterior a ciegas introduce otra actualización cuyo resultado también habría que confirmar. Por eso el ejemplo comunica un estado posterior a la publicación y conserva la evidencia, sin prometer un rollback.
Los permisos del archivo nuevo se solicitan como 0600, sujetos a umask. No se copia la identidad ni los atributos del archivo anterior. Esto es una decisión explícita del ejemplo; un editor o un gestor de configuración tendría que definir la política de atributos que necesita conservar.
12. Límites del contrato y de la medición
El dispositivo también forma parte del contrato
Las solicitudes de sincronización atraviesan capas del sistema de archivos y del almacenamiento. Su valor depende de que esas capas comuniquen correctamente la finalización y respeten sus obligaciones. Cachés de controladores, dispositivos y anfitriones virtuales deben considerarse en el diseño de un sistema durable.
La documentación de fiabilidad de PostgreSQL explica cómo intervienen cachés del sistema operativo, controladores y unidades. La conclusión para nuestro ensayo es de alcance: medir el retorno de una llamada no verifica físicamente qué ocurriría ante todas las combinaciones de fallos del equipo.
Un fallo de proceso es distinto de perder el estado del sistema
Terminar el proceso no elimina la caché del kernel. Una prueba que mata al escritor y vuelve a leer puede encontrar datos que continuaron en memoria y se escribieron después. Para investigar recuperación se necesitan entornos controlados, puntos de corte definidos y un oráculo que distinga estados válidos; no basta un comando de terminación y una lectura posterior.
Los laboratorios de este tema verifican compilación, resultados visibles, puntos de espera y errores. No provocan cortes del equipo ni afirman certificar persistencia física. El simulador permite explorar un fallo idealizado sin modificar el almacenamiento real.
Decidir según el requisito de la aplicación
| Aplicación | Pregunta que orienta la política |
|---|---|
| Registro de eventos | ¿Cuándo se confirma cada evento y cómo se detecta el último registro completo? |
| Configuración reemplazable | ¿Se publica una versión terminada y se confirma su nombre? |
| Resultados regenerables | ¿Qué pérdidas se toleran y cuándo hace falta confirmar un punto de recuperación? |
| Servicio con clientes | ¿Qué respuesta se envía después del punto durable y cómo se reconocen reintentos? |
Reducir la cantidad de sincronizaciones es una decisión de protocolo, no una optimización mecánica. Antes de cambiar una confirmación por registro a una por lote, revisa qué respuestas se retrasan y qué información queda sin confirmar entre puntos. Después mide la carga que represente ese compromiso.
13. Simulador de capas y recuperación
El modelo parte de config.txt apuntando a un archivo anterior durable con contenido A. Ya hay un temporal nuevo vacío y abierto; su nombre solo aparece inicialmente en el directorio visible. Las escrituras añaden texto al temporal, incluso si posteriormente se lo publica. Se distinguen búfer de stdio, contenido del kernel, contenido persistente ideal y entradas de directorio.
Confirmación de contenido y publicación del nombre
El texto se elige para cada entrega. La bandera se aplica con Reiniciar. El modelo limita el contenido pendiente y escrito a 80 caracteres.
| Capa del modelo | Estado | Qué operación actúa aquí |
|---|
Historial de acciones
Secuencias para comprobar
- Sin entregar stdio. Reinicia, añade texto, sincroniza el archivo, publica, sincroniza el directorio y simula el corte. El texto que seguía en el búfer no llegó al archivo.
- Archivo confirmado, nombre pendiente. Reinicia, añade, entrega con fflush, sincroniza el archivo y publica. Sin sincronizar el directorio, el corte del modelo recupera el nombre anterior.
- Protocolo completo. Repite la secuencia anterior añadiendo fsync del directorio antes del corte. Se recupera el nombre con el contenido nuevo confirmado.
- fdatasync y metadatos. Añade texto, haz fflush y fdatasync. Compara contenido, tamaño y marca de modificación. Después haz fsync y observa la marca persistente del modelo.
- Una bandera no entrega stdio. Elige O_SYNC, reinicia y añade texto. No se propaga hasta fflush; entonces la entrega actualiza también el estado persistente del archivo.
- Directorio sin contenido confirmado. Sin bandera, añade, entrega, publica y sincroniza solo el directorio. El corte ilustra un nombre nuevo con contenido anterior del temporal.
Alcance: el simulador representa una ejecución idealizada, sin writeback de fondo ni errores. En un sistema real, trabajo no confirmado puede persistir, y el conjunto de estados de recuperación depende del sistema de archivos. El estado elegido por el modelo sirve para mostrar por qué una promesa no se deduce de la operación anterior; no predice qué bytes exactos recuperará Linux tras una caída.
La marca de modificación es un contador didáctico, no un timestamp real. El tamaño usa caracteres ASCII de las opciones, no páginas ni sectores. fdatasync conserva el tamaño necesario para el contenido y no cambia esa marca persistente; una implementación real puede propagar más metadatos. La copia del directorio se modela como una operación ideal completa. El directorio padre se considera durable desde el inicio.
14. Resumen
- Define primero qué actualización y qué tipo de fallo cubre una confirmación.
fflushentrega el búfer de la biblioteca; la sincronización del archivo actúa sobre trabajo que llegó al kernel.fsyncyfdatasyncdifieren en sus obligaciones sobre metadatos; el tamaño necesario para recuperar datos no se puede omitir.synctiene alcance global y no devuelve un error;syncfstiene alcance de sistema de archivos.O_SYNCyO_DSYNCtrasladan la espera correspondiente al interior de las escrituras del descriptor.- Confirma por bloque, por lote o al final según el protocolo. La frecuencia cambia rendimiento y latencia.
- Compara ventanas completas: write_s solo puede ocultar costos que otras políticas pagan en puntos separados.
- Un nombre nuevo o reemplazado requiere considerar el directorio, además del contenido del archivo.
- Un temporal completo, su sincronización, rename y la sincronización del directorio permiten ordenar una publicación controlada.
- Un error posterior a publicar es distinto de un error anterior; el fracaso no implica rollback automático.
- La atomicidad de visibilidad y la durabilidad responden preguntas diferentes. El cumplimiento de las capas de almacenamiento forma parte del contrato.
15. Preguntas de repaso y práctica
1. ¿Por qué hay que definir qué significa guardar?
Porque entregar salida a una biblioteca, hacerla visible y confirmar su recuperación frente a una caída son compromisos distintos. El programa debe comunicar éxito después del punto que cumple la promesa elegida.
2. ¿fflush equivale a fsync?
No. fflush entrega la salida pendiente de stdio mediante su función de escritura. La sincronización del archivo atiende el trabajo recibido por el kernel. Si hay búfer de usuario, el orden necesario comienza por entregarlo.
3. ¿Qué sucede al sincronizar antes de fflush?
Los bytes que siguen en la biblioteca no se incluyen por el simple hecho de sincronizar el descriptor. Puede confirmarse un estado anterior mientras la nueva salida permanece en el proceso.
4. ¿Qué metadatos puede omitir fdatasync?
Los que no sean necesarios para recuperar los datos. No puede omitir el tamaño indispensable para recuperar una ampliación. Puede no exigir la misma confirmación de marcas de tiempo que fsync.
5. ¿fdatasync siempre tarda menos que fsync?
No. La diferencia es semántica. La carga y la implementación pueden exigir trabajo parecido; hay que medir sin asumir una ventaja numérica universal.
6. ¿Qué confirma fsync del archivo sobre su nombre nuevo?
No confirma automáticamente la entrada del directorio. Cuando se crea o reemplaza un nombre, el protocolo debe considerar también esa modificación del directorio.
7. ¿Qué diferencia hay entre sync y syncfs?
sync tiene alcance global y una interfaz sin retorno de error. syncfs limita el alcance al sistema de archivos del descriptor y permite reportar errores, que pueden corresponder a otros archivos de ese sistema de archivos.
8. ¿Cómo debe describirse la espera de sync en Linux?
Como comportamiento de Linux, que espera la finalización correspondiente. No debe generalizarse esa obligación a todo sistema POSIX ni confundirse la función con las opciones del comando de shell.
9. ¿Dónde aparece el costo de O_SYNC?
Dentro de las escrituras reales del descriptor. Si una medición solo cronometra una sincronización explícita final, puede ocultar las esperas que ya se pagaron al escribir.
10. ¿O_SYNC elimina el búfer de stdio?
No. La biblioteca puede retener salida hasta entregarla. La bandera solo puede actuar cuando se efectúa la escritura del descriptor correspondiente.
11. ¿O_DIRECT es otra forma de decir durable?
No. Se relaciona con la participación de la caché de páginas, no constituye por sí solo el protocolo de confirmación de este tema. Sus restricciones se estudian en el siguiente.
12. ¿Cuándo puede responder un servicio que confirma por lote?
Después del punto de sincronización que incluye las operaciones de ese lote y de comprobar su éxito. Responder antes prometería una confirmación que todavía no se alcanzó.
13. ¿Por qué el benchmark añade fsync final en todos los modos?
Para cerrar la medición con un punto comparable del archivo. La frecuencia de confirmaciones durante la secuencia sigue siendo distinta; el nombre del directorio queda fuera del ensayo.
14. ¿Qué indica puntos igual a cero en osync?
Que no hubo puntos explícitos intermedios contados por el programa. No significa ausencia de sincronización: la política de apertura actúa dentro de las escrituras y existe además la comprobación final común.
15. ¿rename hace durable la publicación por sí solo?
No. Proporciona propiedades de cambio de nombre y de visibilidad, pero confirmar el contenido y el directorio requiere el protocolo correspondiente. Los descriptores antiguos tampoco cambian mágicamente de archivo.
16. ¿Qué ocurre si falla la sincronización de directorio después de rename?
El nombre nuevo puede estar visible aunque no se haya confirmado su persistencia. Hay que informar un estado posterior a publicar; no suponer que el destino anterior quedó intacto ni intentar un rollback automático sin protocolo.
17. ¿El ejemplo confirma también un directorio recién creado en su padre?
No. Supone que el directorio de trabajo es un punto durable. Si su propia creación forma parte del compromiso, hay que incluir la confirmación de su entrada en el padre y revisar el alcance de la ruta.
18. ¿La lectura correcta posterior demuestra resistencia a un corte?
No. Comprueba el estado visible durante la ejecución. La caché puede conservar cambios y las capas de almacenamiento pueden tener condiciones adicionales. La prueba de recuperación requiere un entorno y un protocolo de fallos definidos.
Para practicar
- Escribe una promesa concreta de confirmación para un editor de configuración y otra para un registro de eventos. Identifica qué datos y nombres debe cubrir cada una.
- Ejecuta el barrido de 18 muestras y conserva el montaje y el kernel junto a la tabla. Explica dónde aparece el costo de cada política.
- En un directorio nuevo, prueba 1, 8, 9 y 17 registros con lote. Predice y comprueba la cantidad de puntos intermedios.
- Prueba un nombre existente, cero registros, 257 registros y un modo desconocido. Verifica el fracaso sin alterar archivos anteriores.
- En copias de la serie, elimina una muestra, duplica su fila y cambia su modo. Comprueba que el analizador rechaza cada conjunto incorrecto.
- Explica por qué sumar las medianas de write_s, puntos_s y final_s no garantiza obtener la mediana de total_s.
- Publica dos versiones dentro del directorio privado del ejemplo. Conserva un descriptor lector de la primera en una extensión del programa y estudia qué objeto sigue leyendo después del reemplazo.
- Crea manualmente el temporal del ejemplo antes de ejecutar el publicador en un directorio de prueba. Comprueba que se rechaza en lugar de truncarse.
- Dibuja los estados de fracaso antes y después de rename. Describe qué puede informar la aplicación en cada uno.
- En el simulador, compara fsync antes y después de fflush. Repite con O_SYNC y explica qué capa todavía puede retener datos.
- En el simulador, confirma contenido sin nombre y nombre sin contenido. Compara ambos cortes con el protocolo completo.
- Diseña un protocolo de respuesta por lotes que contemple una caída después de sincronizar y antes de contestar al cliente. Indica cómo evitarías duplicar una solicitud reintentada.
En el tema 16 estudiaremos O_DIRECT y la E/S sin caché: qué recorrido cambia, qué alineaciones hay que respetar y por qué esa elección debe distinguirse de las garantías de sincronización.