Bayangkan kita sedang menulis file konfigurasi penting—dan tiba-tiba listrik padam. Bila kita menulis langsung ke file target (fopen("config.json", "w")), file akan terpotong di tengah jalan: program yang lain membaca akan melihat partial content yang korup. Untuk mencegah skenario ini, kita pakai pola atomic file update: tulis ke file sementara, sync ke disk, lalu rename ke target. Operasi rename POSIX bersifat atomik—begitu selesai, file target berisi seluruh data baru; begitu gagal, file target tetap berisi data lama. Pada chapter ini kita akan mempraktikkan pola ini dan memahami kenapa fsync adalah bagian krusial yang sering dilupakan.
Masalah: Penulisan Tidak Atomik #
Cara naif menulis file adalah:
FILE *f = fopen("config.json", "w"); /* truncate file ke 0 byte */
fprintf(f, "{...}\n");
fclose(f);Pendekatan ini punya tiga masalah:
- Window partial write — saat
fopenmengembalikan pointer, file sudah terpotong menjadi 0 byte. Bila program lain membaca file pada saat ini, mereka akan melihat file kosong. - Kehilangan data saat crash — bila program crash (atau listrik mati) sebelum
fclose, file akan tetap kosong atau berisi partial content. Data lama sudah hilang. - Race dengan reader — reader yang sedang membuka file bisa mendapat versi yang tidak konsisten.
Pola Atomik: Write-Temp-Sync-Rename #
Pola yang benar adalah:
- Tulis ke file temp dengan nama unik (misalnya
target.tmp.XXXXXX). fsyncfile temp untuk memastikan data benar-benar di disk (bukan hanya di page cache kernel).rename(temp, target)— operasi atomik yang menggantikan file target dengan file temp.fsyncdirektori (opsional, untuk durability penuh) — agar rename tercatat di disk.
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
/* Tulis file secara atomik: tulis ke .tmp, fsync, rename ke target */
int atomic_write(const char *target, const char *data, size_t len) {
/* 1. Bangun nama temp dengan suffix .tmp */
char tmpname[4096];
int n = snprintf(tmpname, sizeof(tmpname), "%s.tmp.XXXXXX", target);
if (n < 0 || (size_t)n >= sizeof(tmpname)) {
return -1;
}
/* 2. Buat file temp dengan mkstemp */
int fd = mkstemp(tmpname);
if (fd < 0) {
perror("mkstemp");
return -1;
}
/* 3. Tulis data dengan loop write(2) */
size_t written = 0;
while (written < len) {
ssize_t w = write(fd, data + written, len - written);
if (w < 0) {
perror("write");
close(fd);
unlink(tmpname);
return -1;
}
written += (size_t)w;
}
/* 4. fsync untuk memastikan data benar-benar di disk */
if (fsync(fd) < 0) {
perror("fsync");
close(fd);
unlink(tmpname);
return -1;
}
close(fd);
/* 5. Rename ke target - operasi atomik */
if (rename(tmpname, target) < 0) {
perror("rename");
unlink(tmpname);
return -1;
}
return 0;
}
int main(void) {
/* Buat file awal */
atomic_write("/tmp/test_atomic.txt", "versi awal\n", 11);
printf("Versi awal ditulis\n");
/* Baca dan tampilkan */
FILE *f = fopen("/tmp/test_atomic.txt", "r");
if (f) {
char buf[64] = {0};
fread(buf, 1, sizeof(buf)-1, f);
printf("Isi sekarang: %s", buf);
fclose(f);
}
/* Update ke versi baru */
atomic_write("/tmp/test_atomic.txt", "versi baru setelah update\n", 27);
printf("\nUpdate ke versi baru selesai\n");
/* Baca kembali */
f = fopen("/tmp/test_atomic.txt", "r");
if (f) {
char buf[64] = {0};
fread(buf, 1, sizeof(buf)-1, f);
printf("Isi sekarang: %s", buf);
fclose(f);
}
/* Pastikan tidak ada file temp yang tersisa */
if (access("/tmp/test_atomic.txt.tmp.XXXXXX", F_OK) != 0) {
printf("\nTidak ada file temp tersisa (sudah direname)\n");
}
return 0;
}Output yang Dihasilkan:
Versi awal ditulis
Isi sekarang: versi awal
Update ke versi baru selesai
Isi sekarang: versi baru setelah update
Tidak ada file temp tersisa (sudah direname)Perhatikan output di atas. File test_atomic.txt awalnya berisi "versi awal\n". Setelah pemanggilan atomic_write kedua, file berisi "versi baru setelah update\n"—sepenuhnya diganti. Tidak ada momen di mana reader akan melihat file kosong atau partial content; rename POSIX bersifat atomik.
Mari kita telaah baris-baris kode di atas:
- Baris 9 (
char tmpname[4096];): buffer untuk nama file temp. Kita pakaisnprintfuntuk membangun nama<target>.tmp.XXXXXXdengan aman. - Baris 14 (
int fd = mkstemp(tmpname);): membuat file temp dengan nama acak.mkstempmengganti 6 karakterXXXXXXdengan karakter acak dan membuka file denganO_CREAT | O_EXCL—mencegah race condition dengan penyerang yang mungkin membuat symlink dengan nama yang sama. - Baris 21-30 (
while (written < len)): loop write sampai semua byte ditulis.write(2)bisa mengembalikan kurang dari yang diminta—kita harus loop sampaiwritten == len. - Baris 33 (
fsync(fd)): memaksa page cache kernel untuk mem-flush data file ke disk fisik. Tanpafsync, data mungkin masih di RAM—bila listrik mati setelahrenametetapi sebelum kernel sempat flush, file target akan kosong. - Baris 42 (
rename(tmpname, target)): operasi atomik POSIX. Pada filesystem yang sama,renamemenggantikan file target dengan file temp dalam satu langkah. Pembaca yang membuka file target sebelumrenameakan melihat versi lama; setelahrenameakan melihat versi baru. Tidak ada momen di mana file target kosong atau korup. - Baris 56 (
access("/tmp/test_atomic.txt.tmp.XXXXXX", F_OK)): memeriksa apakah masih ada file temp tersisa. Bilarenameberhasil, file temp (dengan nama asli yang berisi karakter acak, bukan literalXXXXXX) sudah tidak ada.
Mengapa fsync Diperlukan #
Tanpa fsync, urutan operasi sebenarnya adalah:
write(fd, data, len)— data masuk ke page cache kernel (RAM), belum ke disk.close(fd)— menutup file descriptor, tetapi data mungkin masih di page cache.rename(temp, target)— metadata file berganti (atomik), tetapi directory entry baru mungkin juga masih di page cache.- Listrik mati. Saat sistem booting lagi, file target mungkin:
- Tidak ada (rename belum sempat di-flush).
- Ada tetapi kosong (rename sempat di-flush, tetapi data belum).
- Ada dengan data lama (rename belum sempat di-flush).
Dengan fsync(fd) sebelum rename, kita memaksa kernel menulis isi file ke disk sebelum mengganti nama. Untuk durability penuh (rename juga harus tercatat di disk), kita perlu juga fsync direktori induk:
int dir_fd = open(dirname((char*)target), O_RDONLY);
if (dir_fd >= 0) {
fsync(dir_fd);
close(dir_fd);
}Demonstrasi: Update File Konfigurasi #
Mari kita lihat skenario realistis: program membaca konfigurasi JSON, mengubah sebuah field, lalu menulis ulang secara atomik.
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int atomic_write(const char *target, const char *data, size_t len) {
char tmpname[4096];
snprintf(tmpname, sizeof(tmpname), "%s.tmp.XXXXXX", target);
int fd = mkstemp(tmpname);
if (fd < 0) return -1;
size_t written = 0;
while (written < len) {
ssize_t w = write(fd, data + written, len - written);
if (w < 0) { close(fd); unlink(tmpname); return -1; }
written += (size_t)w;
}
fsync(fd);
close(fd);
if (rename(tmpname, target) < 0) {
unlink(tmpname);
return -1;
}
return 0;
}
int main(void) {
/* Tulis konfigurasi awal */
const char *config_v1 = "{\"version\": 1, \"port\": 8080}\n";
atomic_write("/tmp/test_config.json", config_v1, strlen(config_v1));
printf("Config v1 ditulis\n");
/* Update: ganti port ke 9090, tambahkan host */
const char *config_v2 = "{\"version\": 2, \"port\": 9090, \"host\": \"0.0.0.0\"}\n";
atomic_write("/tmp/test_config.json", config_v2, strlen(config_v2));
printf("Config v2 ditulis (update atomik)\n");
/* Baca untuk verifikasi */
FILE *f = fopen("/tmp/test_config.json", "r");
if (f) {
char buf[256] = {0};
fread(buf, 1, sizeof(buf)-1, f);
printf("Isi final: %s", buf);
fclose(f);
}
/* Daftar isi /tmp untuk konfirmasi tidak ada file temp tersisa */
printf("\n(gunakan 'ls /tmp/test_config*' untuk verifikasi)\n");
return 0;
}Output yang Dihasilkan:
Config v1 ditulis
Config v2 ditulis (update atomik)
Isi final: {"version": 2, "port": 9090, "host": "0.0.0.0"}
(gunakan 'ls /tmp/test_config*' untuk verifikasi)Output di atas menunjukkan bahwa setelah dua kali pemanggilan atomic_write, file test_config.json berisi konfigurasi terbaru (version: 2). Tidak ada partial write, tidak ada file temp tersisa. Program lain yang membaca test_config.json pada saat atomic_write berjalan akan melihat salah satu dari dua versi (lama atau baru), tidak pernah file kosong atau korup.
Edge Cases dan Pertimbangan #
Beberapa hal yang perlu diperhatikan saat menerapkan pola ini:
- Filesystem yang sama —
renamehanya atomik bilatempdantargetberada di filesystem yang sama. Bila berbeda,renamebisa gagal atau melakukan copy + delete yang tidak atomik. Selalu buat file temp di direktori yang sama dengan target. - Izin file —
mkstempmembuat file dengan mode0600(hanya pemilik). Bila target harus punya izin berbeda (misalnya0644), gunakanfchmod(fd, 0644)sebelumclose. - Owner file —
mkstempmembuat file dengan owner adalah proses pemanggil. Bila target harus punya owner berbeda (misalnya file sistem),renameakan gagal kecuali proses berjalan sebagai root. - Quota — bila disk penuh,
writeakan gagal. Pastikan kita membersihkan file temp di jalur error. - Network filesystem — pada NFS,
renametidak selalu atomik. Untuk NFS, gunakan file locking atau protokol tingkat aplikasi yang lebih kuat.