Bài 18.1026 phút đọc
Ghi an toàn và xử lý lỗi
Sau bài này bạn sẽ làm được
- Cài mẫu ghi ra tệp tạm rồi đổi tên và giải thích vì sao nó an toàn
- Phân biệt fflush với fsync và biết cái nào chạm tới đĩa
- Xử lý đủ các lỗi đĩa đầy, không có quyền và tệp đang bị khóa
- Viết hàm lưu trả về mã lỗi rõ ràng thay vì im lặng
Người dùng bấm Lưu, đèn ổ cứng nhấp nháy, rồi mất điện. Chương trình viết cẩn thận thì họ mất công việc của năm phút vừa rồi. Chương trình viết ẩu thì họ mất tất cả. Khác biệt nằm ở ba dòng mã.
#Cửa sổ nguy hiểm
FILE *f = fopen("du-lieu.dat", "wb"); /* tep cu bi xoa NGAY tai day */
for (size_t i = 0; i < n; ++i)
ghi_ban_ghi(f, &ds[i]); /* mat vai giay voi du lieu lon */
fclose(f);terminal
./thu-cua-so
truoc khi ghi de truc tiep: dl2.txt: [du lieu cu rat quan trong] (25 byte) ngay sau fopen che do "wb", chua ghi byte nao: dl2.txt: 0 byte
#Ghi ra tệp tạm rồi đổi tên
Thao tác nguyên tử
Thao tác mà người quan sát chỉ thấy được hai trạng thái: chưa làm, và đã làm xong. Không có trạng thái ở giữa. Lệnh
rename trong cùng một hệ tệp là thao tác nguyên tử trên POSIX.luu-an-toan.c
#include <errno.h>
#include <stdio.h>
#include <string.h>
int luu_an_toan(const char *ten, const SinhVien *ds, size_t n) {
char tam[512];
int k = snprintf(tam, sizeof tam, "%s.tmp", ten);
if (k < 0 || (size_t)k >= sizeof tam) return -1; /* ten qua dai */
FILE *f = fopen(tam, "wb");
if (!f) { perror(tam); return -1; }
/* Tu day tro di, moi that bai deu phai XOA TEP TAM */
for (size_t i = 0; i < n; ++i)
if (ghi_ban_ghi(f, &ds[i]) != 0) goto hong;
if (fflush(f) != 0) goto hong;
if (fclose(f) != 0) { /* fclose that bai: dung goto vi
f da khong con hop le */
remove(tam);
return -1;
}
/* Tren Windows, rename khong ghi de duoc tep da ton tai */
remove(ten);
if (rename(tam, ten) != 0) {
perror("rename");
remove(tam);
return -1;
}
return 0;
hong:
fclose(f);
remove(tam);
return -1;
}terminal
./thu-luu-an-toan
sau lan luu dau: dl.txt: [phien ban 1] (11 byte) dl.txt.tmp: KHONG TON TAI sau lan luu hai: dl.txt: [phien ban 2, dai hon nhieu] (26 byte) dl.txt.tmp: KHONG TON TAI
Ghi toàn bộ ra tệp tạm
Tệp đích chưa hề bị đụng tới. Nếu bước này thất bại vì bất cứ lý do gì, bạn xóa tệp tạm và tệp cũ vẫn nguyên vẹn.
Xả bộ đệm và đóng
Sau
fclosethành công, mọi byte đã ở trong tay hệ điều hành. Đây cũng là nơi lỗi đĩa đầy lộ ra, xem lại Bài 18.2.Đổi tên
Một lời gọi duy nhất. Trước nó, tệp đích là bản cũ. Sau nó, là bản mới. Không có khoảnh khắc nào nó là bản dở dang.
#fflush chưa phải là đã nằm trên đĩa
| Sau lời gọi | Sống sót khi chương trình sập | Sống sót khi mất điện | Chi phí |
|---|---|---|---|
| Chỉ fwrite | Không | Không | Gần bằng không |
| fflush | Có | Không | Một lời gọi hệ thống |
| fflush rồi fsync | Có | Có | Chờ đĩa, có thể vài mili giây |
| fsync cả thư mục | Có | Có, kể cả tên tệp mới | Thêm một lần chờ đĩa nữa |
Bản đầy đủ trên POSIX
#include <unistd.h> /* fsync */
#include <fcntl.h> /* open */
int luu_that_an_toan(const char *ten, const SinhVien *ds, size_t n) {
char tam[512];
snprintf(tam, sizeof tam, "%s.tmp", ten);
FILE *f = fopen(tam, "wb");
if (!f) return -1;
for (size_t i = 0; i < n; ++i)
if (ghi_ban_ghi(f, &ds[i]) != 0) { fclose(f); remove(tam); return -1; }
/* 1. Bo dem thu vien -> nhan he dieu hanh */
if (fflush(f) != 0) { fclose(f); remove(tam); return -1; }
/* 2. Bo dem nhan -> mat dia */
if (fsync(fileno(f)) != 0) { fclose(f); remove(tam); return -1; }
if (fclose(f) != 0) { remove(tam); return -1; }
/* 3. Doi ten */
if (rename(tam, ten) != 0) { remove(tam); return -1; }
/* 4. Ban ghi ve TEN TEP nam trong thu muc, cung can dong bo */
int td = open(thu_muc_cua(ten), O_RDONLY);
if (td >= 0) { fsync(td); close(td); }
return 0;
}#Mã lỗi thay cho thông báo
Một mã lỗi duy nhất cho mọi chuyện
int luu(const char *ten) {
FILE *f = fopen(ten, "wb");
if (!f) {
printf("Loi!\n"); /* nguoi dung biet gi? khong biet gi ca */
return -1;
}
/* ... */
}
/* Nguoi goi nhan -1 va cung khong biet gi hon:
tep khong mo duoc?
dia day?
ten khong hop le?
Ho khong the hien thi thong bao huu ich, khong the tu quyet dinh
nen thu lai hay bo cuoc. */Kiểu enum có mô tả
/* trong quanly.h */
typedef enum {
QL_OK = 0,
QL_LOI_MO_TEP,
QL_LOI_GHI,
QL_LOI_DOC,
QL_LOI_BO_NHO,
QL_TEP_HONG,
QL_SAI_PHIEN_BAN,
QL_TEN_QUA_DAI,
} MaLoi;
const char *ql_mo_ta_loi(MaLoi m);
/* trong quanly.c */
const char *ql_mo_ta_loi(MaLoi m) {
switch (m) {
case QL_OK: return "thanh cong";
case QL_LOI_MO_TEP: return "khong mo duoc tep";
case QL_LOI_GHI: return "loi khi ghi, dia co the day";
case QL_LOI_DOC: return "loi khi doc";
case QL_LOI_BO_NHO: return "khong du bo nho";
case QL_TEP_HONG: return "tep hong hoac sai dinh dang";
case QL_SAI_PHIEN_BAN: return "tep do phien ban moi hon tao ra";
case QL_TEN_QUA_DAI: return "duong dan qua dai";
}
return "loi khong ro";
}Dùng ở tầng giao diện
MaLoi m = ql_luu(&ql, ten);
if (m != QL_OK) {
fprintf(stderr, "Khong luu duoc %s: %s\n", ten, ql_mo_ta_loi(m));
if (m == QL_LOI_GHI)
fprintf(stderr, "Hay giai phong bot dung luong roi thu lai.\n");
return 1;
}
printf("Da luu %zu sinh vien vao %s\n", ql.n, ten);#Danh sách lỗi phải xử lý
| Lỗi | Xảy ra khi | Xử lý thế nào |
|---|---|---|
| fopen trả NULL | Không có tệp, không có quyền, hết số hiệu tệp | Báo tên tệp cùng strerror(errno), dừng thao tác |
| fwrite trả về ít hơn n | Đĩa đầy, hạn ngạch, ổ bị rút | Xóa tệp tạm, giữ nguyên tệp cũ, báo lỗi ghi |
| fclose trả khác 0 | Xả bộ đệm thất bại | Coi như ghi thất bại, xóa tệp tạm |
| malloc trả NULL | Hết bộ nhớ, hoặc số bản ghi trong tệp bị hỏng | Giải phóng những gì đã cấp, trả QL_LOI_BO_NHO |
| Số nhận dạng không khớp | Người dùng chọn nhầm tệp | Từ chối ngay, không đọc thêm byte nào |
| Mã kiểm tra không khớp | Tệp hỏng, chép dở, sửa tay | Từ chối, gợi ý dùng bản .bak |
| Tệp cụt giữa bản ghi | Lần ghi trước bị ngắt | Báo đọc được bao nhiêu trong bao nhiêu, hỏi có nạp phần đó không |
| rename thất bại | Tệp đang mở, khác phân vùng, không có quyền | Xóa tệp tạm, báo lỗi, tệp cũ vẫn còn |
Tự làm thử
- Viết chương trình mở một tệp có sẵn bằng
"wb"rồi dừng lại, và kiểm tra kích thước tệp từ một cửa sổ khác. - Cài
luu_an_toanđầy đủ và xác nhận không có tệp.tmpnào còn lại sau khi chạy. - Cố ý làm
ghi_ban_ghithất bại ở bản ghi thứ mười, rồi kiểm tra tệp cũ còn nguyên vẹn. - Thêm bản sao lưu
.bakvà kiểm tra nó chứa đúng nội dung của lần lưu trước. - Cắt cụt một tệp dữ liệu bằng
truncatevà xác nhận chương trình báo lỗi rõ ràng thay vì nạp dữ liệu rác.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Mở tệp đích bằng
"w"xóa nó ngay lập tức, tạo ra một cửa sổ nguy hiểm kéo dài suốt quá trình ghi. - Ghi ra tệp tạm cùng thư mục rồi
renamelên tệp đích, nhờ đó luôn tồn tại một bản đầy đủ. fflushchống được chương trình sập, cònfsyncmới chống được mất điện. Gọifsyncmột lần ở cuối, không gọi trong vòng lặp.- Trả về mã lỗi kiểu
enumcó hàm mô tả, để tầng giao diện quyết định cách nói với người dùng. - Mọi con số đọc từ tệp đều là dữ liệu không đáng tin. Kiểm tra trước khi nhân nó với kích thước bản ghi.