1. Por qué truncar el destino expone estados incompletos
Los temas 33 y 34 separaron recuperación del formato, versiones e integridad. Ahora resolveremos una tarea de aplicación: reemplazar un archivo de configuración completo sin que una apertura ordinaria del nombre publicado encuentre una versión a medio escribir.
open("estado.txt", O_WRONLY | O_TRUNC)
write(primera_parte)
write(segunda_parte)
close()
Entre ambas escrituras otro proceso puede abrir estado.txt.
Si la segunda falla, el archivo ya perdió su contenido anterior.O_TRUNC reduce el archivo a longitud cero al abrirlo con el acceso adecuado. Los lectores que ya tenían el mismo objeto abierto también pueden observar las modificaciones. Ni el journal ni COW convierten esta secuencia en una transacción de aplicación.
Publicación por reemplazo: preparar un archivo distinto y cambiar el nombre público cuando el contenido está completo. El punto de publicación se sitúa en la sustitución del nombre, en lugar de repartirse entre las escrituras de los datos.
2. Atomicidad, durabilidad y aislamiento
| Propiedad | Pregunta concreta | Mecanismo del tema |
|---|---|---|
| Atomicidad de publicación | ¿Una apertura del nombre encuentra una versión completa? | Preparar otro archivo y sustituir el nombre con rename. |
| Durabilidad | ¿Se confirmó la nueva versión frente al fallo considerado? | Sincronizar archivo y directorio en el orden requerido. |
| Aislamiento entre escritores | ¿Un escritor puede invalidar el trabajo de otro? | Necesita coordinación adicional; rename no basta. |
Una publicación puede ser visible y todavía no tener confirmada su persistencia. También puede ser atómica y perder una actualización de otro escritor. Por eso «guardar de forma atómica» necesita una definición de alcance.
Trabajaremos con Linux, archivos regulares, un sistema de archivos local que admita las operaciones de sincronización necesarias y almacenamiento que respete sus confirmaciones. No extrapolaremos automáticamente el comportamiento a protocolos remotos, servicios de objetos ni todos los proveedores virtuales.
3. El protocolo de publicación
Antes de rename, estado.txt conserva la versión anterior. Después de rename, las aperturas nuevas encuentran el archivo preparado. Se informa confirmación completa después de sincronizar el directorio. La versión publicada no vuelve a modificarse en este diseño: cada actualización crea otro temporal.
Para analizar un corte suponemos que el estado inicial ya estaba confirmado y que ningún otro escritor cambia el destino. Si el protocolo se interrumpe antes de su última confirmación, no se inventa un resultado universal de recuperación: depende del punto alcanzado, del formato y de sus garantías.
4. Crear el temporal en el directorio correcto
El temporal debe estar en el mismo sistema de archivos que el destino para que el reemplazo mediante rename sea posible. Crearlo en /tmp no lo garantiza: /tmp puede ser otro montaje. Usar el propio directorio final simplifica la elección y permite confirmar una sola unidad de nombres.
mkstemp crea y abre un nombre exclusivo a partir de una plantilla modificable terminada en XXXXXX. No se consulta primero si el nombre está libre para abrirlo después: esa separación introduce una carrera. Consulta mkstemp(3).
char temporal[] = ".publicar35.XXXXXX";
int fd = mkstemp(temporal);
/* Comprobar fd antes de usarlo. temporal contiene el nombre elegido. */El programa completo abre el directorio una vez y cambia a él con fchdir antes de llamar a mkstemp. Es una herramienta de un solo hilo que termina al completar el trabajo; cambiar el directorio actual no sería una buena interfaz de biblioteca para una aplicación con varios hilos. Las operaciones posteriores de nombres usan el descriptor con renameat y unlinkat.
La práctica exige un directorio propio sin actores que cambien sus entradas durante la operación. Un descriptor del directorio estabiliza la referencia al directorio abierto, pero no impide que otro actor autorizado manipule sus nombres.
5. Escrituras parciales y buffers de biblioteca
write puede aceptar menos bytes que los solicitados. El bucle debe avanzar por el progreso ya realizado, reintentar EINTR cuando no hubo progreso informado y detenerse ante errores reales. Cero con bytes pendientes se trata como falta de progreso. Véase write(2).
pendientes = cantidad;
while (pendientes > 0) {
n = write(fd, posicion, pendientes);
si n > 0: avanzar posicion y reducir pendientes;
si n == -1 y errno == EINTR: reintentar;
en otro caso: abortar antes de publicar;
}Con FILE y fprintf existe además el buffer de la biblioteca. Antes de fsync sobre fileno se requiere vaciar ese buffer con fflush y comprobar su retorno. fflush no confirma por sí mismo persistencia del dispositivo; fsync tampoco puede sincronizar bytes que siguen exclusivamente en memoria de la biblioteca.
El ejemplo usa read y write directamente para que esas capas queden explícitas. Su origen debe ser un regular estable durante la copia. Copiar hasta EOF no constituye una instantánea si otro proceso modifica el origen al mismo tiempo.
6. Confirmar los datos antes de publicar
Después de completar el contenido se aplican los metadatos que la nueva versión deba tener y se sincroniza el archivo. Si ese paso falla, no se publica el temporal. El ejemplo fija su modo a 0600 antes de confirmarlo.
fsync puede informar errores diferidos que no aparecieron en write. Hay que respetar esos errores aunque todos los bytes parecieran haber sido aceptados. fdatasync es otra interfaz con un contrato distinto para ciertos metadatos; aquí usamos fsync para presentar una secuencia uniforme.
El programa comprueba close del temporal antes de rename. Un error en ese cierre también detiene la publicación. En Linux no se repite close a ciegas: el descriptor puede haberse liberado y reutilizado. Véase close(2).
7. Qué cambia rename y qué conservan los lectores
rename sustituye atómicamente una entrada existente dentro de las condiciones de su contrato: las aperturas del nombre no necesitan atravesar un intervalo en que lo hayamos borrado. Hacer unlink del destino antes de rename perdería esa ventaja.
Un descriptor que apuntaba al archivo anterior sigue apuntando a ese objeto. Otro open del mismo nombre puede obtener la versión nueva. Véase open(2). El reemplazo no cambia retroactivamente los descriptores de los lectores ni los otros hard links del objeto anterior.
Antes:
estado.txt → objeto anterior ← descriptor del lector
temporal → objeto nuevo
Después de rename:
estado.txt → objeto nuevo
descriptor del lector → objeto anteriorUn lector que necesita una versión coherente abre una vez y lee mediante ese descriptor. Si abre el nombre por separado para cada fragmento puede combinar versiones de aperturas distintas. La estabilidad requiere además que nadie modifique en el lugar el objeto que está leyendo.
8. Confirmar el nombre en el directorio
El contenido del archivo y su entrada de directorio son responsabilidades diferentes. fsync del archivo no necesariamente confirma la entrada que permite encontrarlo. El protocolo sincroniza el directorio después de rename; véase fsync(2).
Si ese fsync falla, la nueva versión ya puede ser visible. No se debe responder «no cambió nada» ni deshacerla borrando estado.txt. El resultado es publicación realizada con persistencia sin confirmar. Repetir la operación requiere considerar qué versión está presente y qué otros actores pudieron actuar.
El directorio de práctica ya existe y no cambia de ubicación. Si el protocolo también creara una jerarquía nueva, habría que analizar la confirmación de sus entradas en los directorios padres. Mover entre directorios distintos introduce asimismo más relaciones a confirmar; el ejemplo evita ese caso.
9. Errores antes y después de la publicación
| Punto | Nombre público en ejecución normal | Respuesta de la aplicación |
|---|---|---|
| Falla crear o escribir temporal | Versión anterior. | No publicar; informar el error y retirar el temporal propio si existe. |
| Falla fsync o close del temporal | Versión anterior. | Abortar la publicación aunque la escritura pareciera completa. |
| Falla rename en el ensayo local | No se publicó la versión nueva. | Informar el fallo y limpiar el temporal propio. |
| Rename tuvo éxito; falla fsync del directorio | Versión nueva visible. | Informar publicación con persistencia sin confirmar. |
| Fsync del directorio tuvo éxito | Versión nueva confirmada según las hipótesis. | Informar éxito del protocolo. |
El programa añade dos paradas didácticas para observar estados: una antes y otra después de rename. Son salidas deliberadas, no cortes de energía ni errores reales de fsync. El kernel sigue funcionando y puede persistir escrituras después de que termine el proceso.
Un temporal residual no debe elegirse automáticamente como versión ganadora solo por su nombre o su fecha. La recuperación necesita validar formato, completitud y pertenencia a una operación. En sistemas remotos también puede haber resultados ambiguos por fallos de comunicación; el alcance de esta tabla es local.
10. Permisos, enlaces y política de reemplazo
Reemplazar una entrada publica un objeto nuevo. No conserva automáticamente propietario, ACL, atributos extendidos, etiquetas, fechas ni todos los permisos del anterior. Una aplicación real decide cuáles copiar y cómo validarlos antes de sincronizar el temporal.
El ejemplo produce deliberadamente un archivo regular con modo 0600. Si estado.txt era un enlace simbólico, rename reemplaza esa entrada, sin escribir a través de su destino. Si tenía otros hard links, estos continúan referenciando el objeto anterior. Estas diferencias pueden ser importantes para un editor o un gestor de configuración.
La política es reemplazar el nombre fijo estado.txt, incluso si otro escritor lo actualizó mientras se preparaba el temporal. Para exigir que el nombre no exista se necesita otra operación, por ejemplo renameat2 con RENAME_NOREPLACE en entornos compatibles; una comprobación previa con access no evita carreras. Los contratos y restricciones se describen en rename(2).
11. Varios escritores y varios archivos
Si A y B leen saldo 100, A prepara 110 y B prepara 120, ambos pueden publicar archivos completos. Si B publica al final, el saldo queda en 120 aunque el efecto combinado pretendido fuera 130. No hay bytes mezclados, pero sí una actualización perdida.
Un bloqueo cooperativo debe abarcar lectura, cálculo y publicación. Conviene que el objeto de bloqueo sea estable y distinto del archivo reemplazado: bloquear el inodo que luego se sustituye puede dejar a procesos posteriores coordinándose sobre objetos diferentes. Todos los escritores deben respetar el mismo mecanismo.
Dos renames sobre archivos diferentes no forman una transacción conjunta. Un diseño posible prepara una generación completa en una ubicación propia y publica una única referencia a esa generación. Sus lectores deben resolver esa referencia una vez y conservar la generación elegida. Una base de datos añade protocolos de log, recuperación y concurrencia que este publicador de un archivo no implementa.
12. Programa en C: publicar una versión completa
Guarda publicar-atomico.c. Recibe un directorio propio, un archivo origen y un modo opcional: completo, antes-rename o despues-rename. El destino fijo es estado.txt. No modifica el origen y no requiere privilegios. El origen se abre antes de cambiar el directorio actual, por lo que también admite una ruta relativa.
#define _POSIX_C_SOURCE 200809L
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int sincronizar(int fd)
{
int r;
do { r = fsync(fd); } while (r == -1 && errno == EINTR);
return r;
}
static int escribir_todo(int fd, const char *p, size_t cantidad)
{
while (cantidad > 0) {
ssize_t n = write(fd, p, cantidad);
if (n > 0) { p += n; cantidad -= (size_t)n; }
else if (n == -1 && errno == EINTR) continue;
else { if (n == 0) errno = EIO; return -1; }
}
return 0;
}
static int copiar(int origen, int destino)
{
char buffer[16384];
for (;;) {
ssize_t n = read(origen, buffer, sizeof buffer);
if (n == 0) return 0;
if (n == -1) {
if (errno == EINTR) continue;
return -1;
}
if (escribir_todo(destino, buffer, (size_t)n) == -1) return -1;
}
}
int main(int argc, char **argv)
{
if (argc < 3 || argc > 4) {
fprintf(stderr, "Uso: %s directorio origen [completo|antes-rename|despues-rename]\n", argv[0]);
return 2;
}
const char *modo = argc == 4 ? argv[3] : "completo";
if (strcmp(modo, "completo") && strcmp(modo, "antes-rename") &&
strcmp(modo, "despues-rename")) {
fprintf(stderr, "Modo desconocido.\n"); return 2;
}
int origen = -1, dir = -1, temporal = -1;
int existe_temporal = 0, conservar = 0, estado = 1;
char nombre[] = ".publicar35.XXXXXX";
struct stat st;
origen = open(argv[2], O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (origen == -1) { perror("open origen"); goto salir; }
if (fstat(origen, &st) == -1) { perror("fstat origen"); goto salir; }
if (!S_ISREG(st.st_mode)) {
fprintf(stderr, "El origen debe ser regular.\n"); goto salir;
}
dir = open(argv[1], O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
if (dir == -1) { perror("open directorio"); goto salir; }
if (fchdir(dir) == -1) { perror("fchdir"); goto salir; }
temporal = mkstemp(nombre);
if (temporal == -1) { perror("mkstemp"); goto salir; }
existe_temporal = 1;
if (fcntl(temporal, F_SETFD, FD_CLOEXEC) == -1) {
perror("fcntl temporal"); goto salir;
}
if (copiar(origen, temporal) == -1) { perror("copiar"); goto salir; }
if (close(origen) == -1) {
origen = -1; perror("close origen"); goto salir;
}
origen = -1;
if (fchmod(temporal, 0600) == -1) { perror("fchmod"); goto salir; }
if (sincronizar(temporal) == -1) { perror("fsync temporal"); goto salir; }
if (close(temporal) == -1) {
temporal = -1; perror("close temporal"); goto salir;
}
temporal = -1;
if (!strcmp(modo, "antes-rename")) {
conservar = 1; estado = 4;
fprintf(stderr, "Parada antes de publicar. Temporal: %s\n", nombre);
goto salir;
}
if (renameat(dir, nombre, dir, "estado.txt") == -1) {
perror("renameat"); goto salir;
}
existe_temporal = 0;
estado = 3; /* La publicación ocurrió; aún no está confirmada. */
if (!strcmp(modo, "despues-rename")) {
fprintf(stderr, "Parada tras publicar; persistencia sin confirmar.\n");
estado = 5; goto salir;
}
if (sincronizar(dir) == -1) {
perror("fsync directorio: publicado, persistencia sin confirmar");
goto salir;
}
estado = 0;
puts("Version publicada y confirmada.");
salir:
if (temporal != -1 && close(temporal) == -1) perror("close limpieza temporal");
if (origen != -1 && close(origen) == -1) perror("close limpieza origen");
if (existe_temporal && !conservar && unlinkat(dir, nombre, 0) == -1)
perror("unlinkat temporal residual");
if (dir != -1 && close(dir) == -1) {
perror("close directorio");
if (estado == 0) estado = 6;
}
return estado;
}| Estado | Significado en esta herramienta |
|---|---|
| 0 | Publicación y sincronizaciones completadas. |
| 1 | Error antes de publicar; consultar también posibles errores de limpieza. |
| 2 | Uso incorrecto. |
| 3 | Publicado; falló confirmar el directorio. |
| 4 | Parada didáctica antes de rename; temporal conservado. |
| 5 | Parada didáctica después de rename; sin solicitar fsync del directorio. |
| 6 | Publicación confirmada, con error posterior al cerrar el descriptor del directorio. |
Los estados 3 y 5 no autorizan a tratar el archivo como si siguiera intacta la versión anterior. El 6 conserva la información de que la confirmación ya tuvo éxito. Estos valores son una convención del ejemplo, no códigos generales de las llamadas del sistema.
O_NOFOLLOW solo controla el componente final de cada open usado aquí. La herramienta no se presenta como un publicador para rutas hostiles ni conserva todos los metadatos. La limpieza por nombre presupone el directorio propio sin cambios adversarios declarado al comienzo.
13. Programa en C: observar dos aperturas
Guarda observar-version.c. Abre estado.txt, espera Enter y entonces abre otra vez el mismo nombre. Muestra como máximo los primeros 64 bytes de cada descriptor en hexadecimal, por lo que también sirve con archivos binarios.
#define _POSIX_C_SOURCE 200809L
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
static int mostrar(const char *etiqueta, int fd)
{
unsigned char datos[64];
size_t usados = 0;
while (usados < sizeof datos) {
ssize_t n = read(fd, datos + usados, sizeof datos - usados);
if (n > 0) usados += (size_t)n;
else if (n == 0) break;
else if (errno != EINTR) { perror("read"); return -1; }
}
printf("%s:", etiqueta);
for (size_t i = 0; i < usados; i++) printf(" %02x", (unsigned)datos[i]);
putchar('\n');
return 0;
}
int main(int argc, char **argv)
{
if (argc != 2) { fprintf(stderr, "Uso: %s archivo\n", argv[0]); return 2; }
int anterior = open(argv[1], O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (anterior == -1) { perror("open anterior"); return 1; }
puts("Descriptor anterior abierto. Publica otra version y pulsa Enter.");
fflush(stdout);
if (getchar() == EOF) {
fprintf(stderr, "No se recibio Enter.\n"); close(anterior); return 1;
}
int nuevo = open(argv[1], O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (nuevo == -1) { perror("open nuevo"); close(anterior); return 1; }
int estado = 0;
if (mostrar("ANTERIOR", anterior) == -1) estado = 1;
if (mostrar("NUEVO", nuevo) == -1) estado = 1;
if (close(anterior) == -1) { perror("close anterior"); estado = 1; }
if (close(nuevo) == -1) { perror("close nuevo"); estado = 1; }
return estado;
}Úsalo sobre regulares del laboratorio. No modifica ninguno y limita la cantidad leída. La demostración compara objetos elegidos por dos aperturas; no mide persistencia ni impide escrituras en el lugar realizadas por otros programas.
14. Laboratorio sin privilegios
Usa una sesión Bash en Linux con compilador C, Python 3 y un sistema local que admita fsync de directorios. No se necesitan montajes ni sudo. Guarda ambos fuentes en el directorio actual y detén la secuencia si aparece un fallo no previsto.
$ cc -std=c11 -Wall -Wextra -Wpedantic -O2 publicar-atomico.c -o publicar-atomico
$ cc -std=c11 -Wall -Wextra -Wpedantic -O2 observar-version.c -o observar-version
$ BIN35="$PWD"
$ LAB35=$(mktemp -d "$HOME/atomico35.XXXXXX")
$ mkdir -- "$LAB35/destino"
$ printf 'version anterior\n' > "$LAB35/anterior.txt"
$ printf 'version nueva\n' > "$LAB35/nueva.txt"
$ "$BIN35/publicar-atomico" "$LAB35/destino" "$LAB35/anterior.txt"
$ cat -- "$LAB35/destino/estado.txt"Observar el punto anterior a rename
$ if "$BIN35/publicar-atomico" "$LAB35/destino" "$LAB35/nueva.txt" antes-rename; then RC35=0; else RC35=$?; fi
$ printf 'Estado esperado 4: %s\n' "$RC35"
$ cmp -- "$LAB35/destino/estado.txt" "$LAB35/anterior.txt"
$ ls -la -- "$LAB35/destino"Debe seguir publicada la versión anterior y quedar un temporal cuyo nombre informa el programa. Sus datos se sincronizaron, pero no se solicitó confirmar la entrada del temporal en el directorio. No se promete que ese nombre sobreviva a un corte.
Observar un descriptor anterior y una apertura nueva
En la primera terminal ejecuta el lector y déjalo esperando. En la segunda usa los valores de BIN35 y LAB35 de tu ensayo; las variables de una terminal no se transfieren automáticamente a otra.
$ "$BIN35/observar-version" "$LAB35/destino/estado.txt"$ "$BIN35/publicar-atomico" "$LAB35/destino" "$LAB35/nueva.txt"
$ cmp -- "$LAB35/destino/estado.txt" "$LAB35/nueva.txt"Vuelve a la primera y pulsa Enter. ANTERIOR muestra los bytes de version anterior y NUEVO los de version nueva. No hay que matar procesos ni reiniciar el equipo para observar la diferencia.
Observar publicación sin confirmación solicitada
$ if "$BIN35/publicar-atomico" "$LAB35/destino" "$LAB35/anterior.txt" despues-rename; then RC35=0; else RC35=$?; fi
$ printf 'Estado esperado 5: %s\n' "$RC35"
$ cmp -- "$LAB35/destino/estado.txt" "$LAB35/anterior.txt"
$ "$BIN35/publicar-atomico" "$LAB35/destino" "$LAB35/nueva.txt"La comparación puede tener éxito con estado 5: visibilidad y confirmación son propiedades distintas. La última orden vuelve a ejecutar el protocolo completo. El ensayo conserva sus archivos y el temporal de la parada anterior para inspección.
15. Verificador reproducible y observación de llamadas
Este verificador crea otro directorio exclusivo dentro del laboratorio, genera dos contenidos de 256 KiB y mantiene un descriptor antiguo abierto durante el reemplazo. Comprueba resultados exactos y los códigos de ambas paradas. No reproduce pérdida de energía.
$ python3 - "$BIN35/publicar-atomico" "$LAB35" <<'PY'
from pathlib import Path
import os
import subprocess
import sys
import tempfile
programa = str(Path(sys.argv[1]).resolve())
base = Path(tempfile.mkdtemp(prefix='verificacion-', dir=sys.argv[2]))
destino = base / 'destino'
destino.mkdir()
a, b = base / 'a.bin', base / 'b.bin'
contenido_a, contenido_b = b'A' * 262144, b'B' * 262144
a.write_bytes(contenido_a)
b.write_bytes(contenido_b)
publico = destino / 'estado.txt'
def ejecutar(origen, modo='completo', esperado=0):
r = subprocess.run([programa, str(destino), str(origen), modo],
capture_output=True, text=True)
if r.returncode != esperado:
raise SystemExit(f'Estado {r.returncode}, esperado {esperado}: {r.stderr}')
def comprobar(condicion, mensaje):
if not condicion:
raise SystemExit(mensaje)
ejecutar(a)
with publico.open('rb') as anterior:
ejecutar(b)
comprobar(anterior.read() == contenido_a, 'Cambio el objeto anterior')
comprobar(publico.read_bytes() == contenido_b, 'Publicacion incompleta')
ejecutar(a, 'antes-rename', 4)
comprobar(publico.read_bytes() == contenido_b, 'Se publico antes de rename')
temporales = list(destino.glob('.publicar35.*'))
comprobar(len(temporales) == 1, 'Temporal residual inesperado')
comprobar(temporales[0].read_bytes() == contenido_a, 'Temporal incompleto')
ejecutar(a, 'despues-rename', 5)
comprobar(publico.read_bytes() == contenido_a, 'No se observo la publicacion')
ejecutar(b)
comprobar(publico.read_bytes() == contenido_b, 'Resultado final incorrecto')
comprobar((publico.stat().st_mode & 0o777) == 0o600, 'Modo inesperado')
comprobar(a.read_bytes() == contenido_a, 'Se modifico el origen A')
comprobar(b.read_bytes() == contenido_b, 'Se modifico el origen B')
print('OK: contenidos, descriptor anterior, paradas, permisos y origenes')
print('Ensayo conservado:', base)
PYSi dispones de strace, observa una ejecución completa. El programa también sincroniza su temporal durante las paradas, por lo que hay que mirar el orden y los argumentos de cada llamada.
$ strace -o "$LAB35/traza.txt" -e trace=openat,read,write,fchmod,fsync,close,renameat,unlinkat,fchdir "$BIN35/publicar-atomico" "$LAB35/destino" "$LAB35/nueva.txt"
$ cat -- "$LAB35/traza.txt"En la traza debe aparecer fsync del temporal antes de renameat y fsync del directorio después. Una traza verifica llamadas y retornos; no demuestra por sí sola que un controlador defectuoso respete la persistencia. Tampoco el éxito del verificador prueba todos los fallos posibles.
16. Simuladores de publicación y escritores
Nombre visible y confirmación del protocolo
Elige un punto de una secuencia ordenada con una versión anterior ya confirmada. El modelo describe lo que se observa antes de un fallo y qué confirmación puede afirmarse. No elige un estado arbitrario tras un apagado ni reproduce ext4, Btrfs o ZFS.
Preparar, publicar, confirmar
Dos escritores con o sin coordinación
El saldo inicial es 100. A suma 10 y B suma 20. Sin coordinación, ambos leen 100 antes de publicar. Con coordinación, el segundo vuelve a leer después de la publicación confirmada del primero y mantiene el bloqueo durante su propia actualización. Cada publicación sustituye un archivo completo.
Atomicidad y actualización perdida
| Escritor | Saldo leído | Incremento | Saldo publicado |
|---|
17. Diagnóstico y resumen
| Observación | Interpretación a comprobar |
|---|---|
| El archivo quedó vacío tras un fallo. | Se pudo truncar directamente el destino antes de completar la escritura. |
| Rename devuelve EXDEV. | Temporal y destino pueden pertenecer a montajes distintos. |
| El lector abierto sigue viendo lo anterior. | Su descriptor conserva el objeto anterior al reemplazo. |
| La versión nueva es visible, pero fsync del directorio falló. | Se publicó sin confirmar persistencia; no asumir que no hubo cambios. |
| El archivo nuevo tiene otros permisos. | Se publicó un inodo nuevo; revisar la política de metadatos. |
| Dos escritores producen un saldo incorrecto sin bytes mezclados. | Puede haberse perdido una actualización por falta de aislamiento. |
| Quedan temporales después de una interrupción. | Requieren validación y política de recuperación o limpieza. |
- Preparar otro archivo evita modificar progresivamente la versión publicada.
- Hay que comprobar lectura, escritura parcial, sincronización y cierre.
- El temporal se crea exclusivamente dentro del directorio final.
- La publicación ocurre en rename; la confirmación del nombre requiere sincronizar el directorio.
- Un descriptor anterior puede seguir leyendo el objeto reemplazado.
- Un error después de publicar necesita un estado diferente al de un aborto previo.
- Reemplazar el archivo exige decidir qué hacer con sus metadatos y enlaces.
- El patrón no proporciona por sí solo aislamiento entre escritores ni transacciones de varios archivos.
18. Preguntas de repaso y práctica
1. ¿Qué riesgo introduce O_TRUNC sobre el nombre publicado?
El contenido anterior se descarta antes de completar la nueva versión y otros lectores pueden ver un estado incompleto.
2. ¿Qué operación marca la publicación del ejemplo?
El renameat exitoso que sustituye estado.txt por el archivo temporal preparado.
3. ¿Publicación atómica implica persistencia confirmada?
No. El protocolo debe confirmar también archivo y directorio en el orden correspondiente.
4. ¿Por qué no crear siempre el temporal en /tmp?
Puede pertenecer a otro sistema de archivos y hacer imposible el reemplazo mediante rename.
5. ¿Qué carrera evita mkstemp?
Evita separar la elección de un nombre libre de su creación exclusiva y apertura.
6. ¿Una escritura positiva siempre completa el buffer?
No. Hay que avanzar por los bytes aceptados y continuar con los pendientes.
7. ¿Qué añade fflush al usar FILE?
Entrega al sistema los bytes pendientes en el buffer de biblioteca; no reemplaza la confirmación mediante fsync.
8. ¿Qué hace el programa si falla fsync del temporal?
Aborta antes de publicar e intenta retirar su temporal.
9. ¿Por qué no repetir close a ciegas en Linux?
El número de descriptor puede haberse liberado y reutilizado, de modo que se cerraría otro recurso.
10. ¿Hay que borrar estado.txt antes de rename?
No. Eso introduciría un intervalo sin el nombre publicado y perdería la sustitución atómica.
11. ¿Qué ve un descriptor abierto antes de reemplazar?
Continúa referenciando el objeto anterior, mientras nuevas aperturas pueden obtener el nuevo.
12. ¿Se conservan automáticamente las ACL y el propietario anterior?
No. Se publica otro objeto y la aplicación debe decidir y aplicar su política de metadatos.
13. ¿Qué significa el estado 3 del ejemplo?
Rename tuvo éxito, pero falló la confirmación del directorio. La publicación ya ocurrió.
14. ¿La parada despues-rename simula un corte de energía?
No. El kernel sigue funcionando y puede persistir los cambios pendientes.
15. ¿Por qué no basta con bloquear el archivo que se reemplaza?
El reemplazo cambia el objeto asociado al nombre y otros procesos pueden coordinarse sobre otro inodo.
16. ¿Dos escritores atómicos pueden perder una actualización?
Sí. Si ambos calculan desde el mismo estado anterior, el último puede reemplazar el resultado del primero.
17. ¿Dos renames forman una transacción conjunta?
No. Cada sustitución tiene su propio punto de publicación.
18. ¿El temporal más reciente siempre es recuperable?
No. Se debe validar su contenido y relación con la operación; una fecha no acredita completitud.
19. ¿Qué demuestra la traza de llamadas?
Permite observar el orden solicitado y los retornos, sin certificar el comportamiento físico del almacenamiento.
20. ¿Cuándo se informa el éxito completo del protocolo?
Después de preparar y sincronizar el archivo, publicar el nombre y sincronizar el directorio, comprobando los resultados.
Para practicar
- Dibuja las referencias del nombre público y del descriptor lector antes y después de rename.
- Compila ambos programas y ejecuta el verificador sobre un directorio propio.
- Publica un archivo vacío y comprueba que se trata como una versión completa de tamaño cero.
- Prueba un origen inexistente y verifica que estado.txt conserva su contenido.
- Usa un directorio como origen y comprueba el rechazo antes de publicar.
- Compara los estados 4 y 5 con el contenido observado y los temporales presentes.
- Comprueba que el archivo publicado tiene modo 0600 aunque el anterior tuviera otro modo.
- Crea un hard link de la versión anterior dentro del ensayo y observa que conserva ese objeto después del reemplazo.
- Identifica en strace las dos sincronizaciones y el renameat intermedio.
- Calcula los resultados de los escritores A y B en ambos órdenes, con y sin coordinación.
- Diseña una política explícita para preservar los metadatos necesarios de un archivo de configuración.
- Propón un formato con número de generación y validación para decidir qué temporales pueden recuperarse.
En el tema 36 comenzaremos el estudio de formatos concretos con FAT: tabla de asignación, cadenas de clústeres, variantes y lectura de una cadena desde C.