WriteFile trả thành công. Vậy dữ liệu đang ở đâu?
Đáp án, gần như chắc, chưa nằm trên đĩa. Chỉ sao chép vào cache trên bộ nhớ. Vì vậy “đã lưu, mất điện mở lại thì mất” xảy ra; vì vậy benchmark sao chép tệp đập tốc độ vật lý không thể có; vì vậy cơ sở dữ liệu gọi fsync một cách đàng hoàng.
Kỳ 4 của loạt “Đáy I/O Windows” là Cache Manager đứng giữa. Kỳ 2 đã viết “nằm cache thì I/O bất đồng bộ cũng hoàn tất đồng bộ”, kỳ 1 để bài tập “đường tắt không tạo IRP (Fast I/O)”. Lần này thu hết phục tuyến đó.
1. Kết luận trước
- Cache tệp Windows là kiểu write-back. Đọc trước từ system file cache, ghi cũng trước vào cache. Phản ánh ra đĩa OS làm sau.1
- Thực thể cache là file mapping. Cache Manager map đoạn 256KB của tệp vào không gian địa chỉ hệ thống; đọc ghi thành “sao chép bộ nhớ với view đó” (Chương 2).1
- Ghi được lazy writer mỗi giây đuổi theo phản ánh. Ứng dụng sập thì dữ liệu không mất, nhưng mất điện hay OS sập thì cache dirty mất (Chương 4).1
- Công cụ “ghi chắc” có ba.
FlushFileBuffers(=Flush(true)của .NET),FILE_FLAG_WRITE_THROUGH,FILE_FLAG_NO_BUFFERING. Flush mỗi lần ghi thường xuyên thì không hiệu quả; tài liệu chính thức nêu kết hợp NO_BUFFERING+WRITE_THROUGH (Chương 5).21 - NO_BUFFERING có yêu cầu căn chỉnh. Kích thước, offset là bội số nguyên kích thước sector; địa chỉ buffer cũng căn biên sector vật lý. Và NO_BUFFERING rồi metadata vẫn được cache (mục 5.3).31
- Map view và cache chia sẻ cùng dữ liệu. Memory-mapped file và I/O cache thường nhất quán; bền map là hai tầng
FlushViewOfFile+FlushFileBuffers(Chương 6).45 - Đọc ghi đồng bộ nằm cache đôi khi không tạo cả IRP. Đường tắt Fast I/O đi thẳng Cache Manager ── đáp bài tập kỳ 1 (Chương 7).6
Bản đồ tri thức của bài viết này
WriteFile thành công không có nghĩa đã bền trên đĩa; cache tệp Windows trước hết sao chép vào view cache hệ thống, phản ánh ra đĩa do lazy writer đuổi theo mỗi giây. Mất điện hay OS sập thì trang dirty chưa ghi sẽ mất, nên dữ liệu muốn ghi chắc cần dùng tách FlushFileBuffers, FILE_FLAG_WRITE_THROUGH, FILE_FLAG_NO_BUFFERING.
flowchart LR
accTitle: Bản đồ tri thức Cache Manager và write-behind
accDescr: Sơ đồ cho thấy quan hệ Cache Manager, cache write-back, view cache hệ thống 256KB, đọc trước, ghi trễ bằng lazy writer, mất trang dirty khi mất điện, ghi chắc bằng FlushFileBuffers, FILE_FLAG_WRITE_THROUGH, FILE_FLAG_NO_BUFFERING, yêu cầu căn chỉnh, nhất quán với memory-mapped file, Fast I/O
cache_manager["Cache Manager"]
write_behind_caching["Cache write-back (kiểu ghi trễ)"]
memory_mapped_file["File mapping (memory-mapped file)"]
system_cache_view["View cache hệ thống (khe 256KB)"]
file_object["File object"]
lazy_writer["lazy writer (luồng ghi trễ)"]
unflushed_write_loss["Mất dữ liệu chưa phản ánh"]
temp_file_attribute["FILE_ATTRIBUTE_TEMPORARY"]
dirty_page["Trang dirty"]
power_loss["Mất điện / OS sập"]
read_ahead["Đọc trước (read-ahead)"]
sequential_scan_hint["FILE_FLAG_SEQUENTIAL_SCAN"]
random_access_hint["FILE_FLAG_RANDOM_ACCESS"]
flushfilebuffers["FlushFileBuffers"]
write_through["FILE_FLAG_WRITE_THROUGH"]
no_buffering["FILE_FLAG_NO_BUFFERING"]
sector_alignment_requirement["Yêu cầu căn chỉnh sector"]
invalid_parameter_error["ERROR_INVALID_PARAMETER (87)"]
frequent_durable_write["Bền chắc khi ghi thường xuyên"]
flushviewoffile["FlushViewOfFile"]
fast_io["Fast I/O"]
procmon["Process Monitor (procmon.exe)"]
irp["IRP (gói yêu cầu I/O)"]
synchronous_io["I/O đồng bộ"]
cache_manager -->|"triển khai"| write_behind_caching
write_behind_caching -->|"sử dụng"| memory_mapped_file
cache_manager -->|"sử dụng"| system_cache_view
system_cache_view -->|"sử dụng"| memory_mapped_file
cache_manager -->|"sử dụng"| file_object
cache_manager -->|"sử dụng"| lazy_writer
lazy_writer -->|"tự động hóa"| write_behind_caching
lazy_writer -->|"giảm thiểu"| unflushed_write_loss
temp_file_attribute -.->|"không tương thích"| lazy_writer
write_behind_caching -->|"có thể gây"| dirty_page
dirty_page -->|"lưu trong"| system_cache_view
power_loss -.->|"có thể gây"| unflushed_write_loss
cache_manager -.->|"ngăn chặn"| unflushed_write_loss
cache_manager -->|"sử dụng"| read_ahead
read_ahead -.->|"cấu hình bằng"| sequential_scan_hint
read_ahead -.->|"cấu hình bằng"| random_access_hint
flushfilebuffers -->|"ngăn chặn"| unflushed_write_loss
write_through -->|"ngăn chặn"| unflushed_write_loss
no_buffering -.->|"giảm thiểu"| unflushed_write_loss
no_buffering -->|"yêu cầu"| sector_alignment_requirement
sector_alignment_requirement -->|"ngăn chặn"| invalid_parameter_error
no_buffering -->|"có thể gây"| invalid_parameter_error
flushfilebuffers -->|"không khuyến nghị"| frequent_durable_write
no_buffering -->|"khuyến nghị cho"| frequent_durable_write
write_through -->|"khuyến nghị cho"| frequent_durable_write
no_buffering -->|"không tương thích"| system_cache_view
flushviewoffile -->|"yêu cầu"| memory_mapped_file
flushviewoffile -->|"nên làm trước"| flushfilebuffers
fast_io -->|"xác minh bằng"| procmon
fast_io -->|"yêu cầu"| system_cache_view
fast_io -->|"không tương thích"| irp
fast_io -->|"yêu cầu"| synchronous_io
Trong sơ đồ, đường liền nét biểu thị quan hệ luôn đúng và đường nét đứt biểu thị quan hệ có điều kiện (điều kiện nằm trong phần giải thích từng quan hệ trên trang chi tiết). Danh sách đầy đủ các quan hệ (tổng 32, kèm bằng chứng và mức chắc chắn) cùng định nghĩa các khái niệm chính được tập hợp tại trang chi tiết bản đồ tri thức (bằng tiếng Nhật). Dữ liệu: JSON-LD / Turtle
2. Bản chất cache ── tệp được map vào bộ nhớ
2.1. Khe 256KB và sao chép bộ nhớ
Nghĩ cache tệp Windows là “hộp khối đĩa” thì nhiều hành vi không giải thích được. Hình đúng là thế này ── Cache Manager map đoạn 256KB của tệp vào “khe” không gian địa chỉ hệ thống; đọc ghi bật cache chạy như sao chép bộ nhớ giữa khe đó và buffer ứng dụng.1
flowchart TB
subgraph U["Ứng dụng (user mode)"]
BUF["Buffer ứng dụng<br/>(vùng đưa cho ReadFile/WriteFile)"]
end
subgraph S["Không gian địa chỉ hệ thống"]
SLOT["System file cache<br/>khe map đoạn 256KB của tệp"]
end
DISK[("Tệp trên đĩa")]
BUF <-->|"ReadFile/WriteFile =<br/>sao chép bộ nhớ với khe"| SLOT
SLOT <-->|"Đọc lúc truy cập lần đầu và<br/>ghi lại sau theo trang"| DISK
Hình 1: Thực ảnh I/O bật cache. “Đọc ghi tệp” phía ứng dụng, nhiều khi chỉ là sao chép bộ nhớ
Một chỗ dễ hiểu nhầm. 256KB là độ hạt của view (map), không có nghĩa I/O đĩa luôn theo đơn vị 256KB. Trang trong khe được đọc khi cần; lượng I/O thật bay ra đĩa đổi theo kích thước yêu cầu và mẫu truy cập. Đoạn đọc lần đầu thì I/O đĩa xảy ra để lấp (IRP kỳ 1 bay xuống storage stack phía dưới). Đã nằm cache thì đọc xong chỉ bằng sao chép. “Hit cache thì phát bất đồng bộ cũng hoàn tất đồng bộ” Chương 5 kỳ 2 đã xem, chính là hiện thân chuyển động “yêu cầu trả lời được thì hoàn tất tại chỗ”. Ngược lại, bật cache mà trang chưa có trong bộ nhớ, xử lý page fault không có cơ chế bất đồng bộ nên đọc bất đồng bộ đôi khi bị xử lý đồng bộ ── bẫy kỳ 2 cũng đã xem.7
2.2. Bản chất “bộ nhớ trống giảm”
Dùng cache hay không và trạng thái đọc trước được quản theo cách mở (đơn vị file object),1 nhưng thân dữ liệu đã cache được chia sẻ theo tệp (stream). Mở cùng tệp nhiều lần không tạo cache riêng; mọi handle thấy cùng nội dung cache (nền nhất quán Chương 6). Cache chạy suốt lúc Windows chạy, dưới chỉ huy Cache Manager.1 Sao chép tệp lớn hay đọc ghi nhiều thì bộ nhớ vật lý đang trống lần lượt bị chuyển sang cache. Task Manager thấy bộ nhớ trống giảm, phần lớn là “bộ nhớ đang chờ, dùng có giá trị, ứng dụng đòi thì nhường nhanh”. Để không nhầm phân biệt này lúc chẩn đoán thiếu bộ nhớ, thực tế quan sát cũng xử trong “.NET: phân biệt chờ GC và rò bộ nhớ”.
Chuyển động này xem được trên màn hình. Mở Task Manager > Hiệu năng > Bộ nhớ, thanh “cấu thành bộ nhớ” phía dưới chia In use / Modified / Standby / Free. Phần lớn cache tệp vào Standby, danh sách bên phải tổng là “Cached”. Xem kỹ hơn thì tab Bộ nhớ của Resource Monitor xếp cùng phân loại kèm dung lượng. Sao chép một tệp vài GB rồi xem lại, Standby tăng, Free giảm, mà “In use” gần như không đổi ── tức không phải “bộ nhớ bị ăn hết” mà “bộ nhớ đang trống được dùng làm cache”, mắt thấy được.
3. Đọc trước ── đầu cơ phía đọc
Cache Manager, từ mẫu truy cập quá khứ, đọc trước đoạn sắp bị đọc (read-ahead). Tệp đọc tuần tự thì dữ liệu tiếp đã nằm cache trước khi ứng dụng yêu cầu ── đó là bí mật tốc độ đọc tuần tự. Lượng đọc trước không cố định; đổi theo mẫu phát hiện và kích thước yêu cầu.
flowchart LR
A["Lịch sử yêu cầu đọc của ứng dụng<br/>đang đọc từ đầu theo thứ tự"]
D{"Cache Manager<br/>phát hiện mẫu"}
R["Đọc trước: đoạn tiếp<br/>đọc sẵn trước khi bị yêu cầu<br/>(lượng đổi theo mẫu và kích thước yêu cầu)"]
H1["Gợi ý FILE_FLAG_SEQUENTIAL_SCAN<br/>= đọc trước tích cực"]
H2["Gợi ý FILE_FLAG_RANDOM_ACCESS<br/>= đọc trước thành lãng phí nên kìm"]
A --> D
D --> R
H1 -.-> D
H2 -.-> D
Hình 2: Đọc trước. Ngoài phát hiện mẫu truy cập, còn đưa gợi ý bằng cờ CreateFile
FileOptions.SequentialScan / RandomAccess trên bảng tương ứng kỳ 1 là gợi ý cho engine đọc trước này. Xử lý hàng loạt “liếm hết” thì cái trước; truy cập kiểu lần chỉ mục thì cái sau ── nghĩ là cờ để dạy OS tương lai chỉ ứng dụng biết thì chỗ dùng rõ.
4. Ghi trễ ── nghĩa “thành công” của WriteFile
4.1. lazy writer đến mỗi giây
Phía ghi là cache write-back. WriteFile trả thành công lúc sao chép dữ liệu vào khe; phản ánh ra đĩa để sau. Chính sách “hoãn rồi ghi” này là ghi trễ (lazy writing).1
Người gánh phản ánh là lazy writer mà Cache Manager khởi mỗi giây. Chất 1/8 trang gần đây chưa flush vào hàng đợi rồi ghi ra; dữ liệu phải ghi nhiều thì chất thêm. Tệp tạm tạo với thuộc tính FILE_ATTRIBUTE_TEMPORARY bị loại khỏi đối tượng flush của lazy writer ── ghi thứ sắp xóa ngay chỉ phí.1 Tuy nhiên đây là gợi ý theo thuộc tính; bộ nhớ siết thì vẫn có thể ghi lại, và tệp “tên trông tạm” không được áp.
sequenceDiagram
participant App as Ứng dụng
participant C as System cache
participant LW as lazy writer (khởi mỗi giây)
participant D as Đĩa
App->>C: WriteFile (dữ liệu)
Note over C: Sao chép vào khe<br/>đánh dấu trang dirty (chưa ghi)
C-->>App: TRUE trả ngay
Note over App,C: Từ đây đến ghi lại là "cửa sổ nguy hiểm"<br/>mất điện, OS sập thì dữ liệu này mất
LW->>C: Chọn 1/8 trang dirty
LW->>D: Ghi lại gộp
Note over D: Lần đầu bền ở đây
Hình 3: Ghi trễ. WriteFile thành công là “đã giao OS”, không phải “đã bền”
4.2. Chuyện gì xảy ra thì mất đến đâu
Làm đúng nghĩa “cửa sổ nguy hiểm”. Loại sự cố quyết số phận.
flowchart TB
W["Dữ liệu ngay sau WriteFile thành công<br/>(trang dirty trên cache)"]
Q{"Chuyện gì xảy ra"}
A1["Tiến trình ứng dụng<br/>sập / bị buộc kết thúc"]
A2["Dừng cả OS<br/>(mất điện, màn hình xanh)"]
S["Dữ liệu còn<br/>cache thuộc OS nên<br/>lazy writer ghi lại đúng lịch"]
L["Trang dirty mất<br/>chỉ phần đã đến đĩa còn"]
W --> Q
Q --> A1
Q --> A2
A1 --> S
A2 --> L
Hình 4: Loại sự cố và rẽ nhánh sống còn. Cache không phải “của tiến trình” mà “của OS”
- Ứng dụng chết, dữ liệu không mất. Lúc sao chép vào cache xong, chủ dữ liệu là OS. “Vừa lưu ứng dụng sập mà tệp vẫn lành” nhờ đây.
- OS chết cả thì phần dirty mất. Tần suất flush được điều theo đánh đổi hiệu năng và tin cậy; tài liệu cũng ghi rõ “sự cố hệ thống đột ngột như mất điện thì dữ liệu cache chưa ghi sẽ mất”.1
Tức câu hỏi thiết kế ứng dụng nghiệp vụ là “dữ liệu này, khoảnh khắc mất điện, mất được không”. Vài giây log có thể cho. Bản ghi xác nhận đơn hàng thì không. Chỉ thứ không cho mới dùng công cụ chương sau.
5. Hộp công cụ làm “đã ghi chắc”
5.1. FlushFileBuffers ── ghi hết ngay
FlushFileBuffers ghi hết dữ liệu đã đệm của tệp chỉ định ra thiết bị. Metadata hệ thống tệp luôn được cache, nên đưa chắc cả metadata thì cần flush (hoặc WRITE_THROUGH) ── cũng là điểm nắm.12 .NET thì FileStream.Flush(true) tương đương (Flush() thôi chỉ đưa buffer trong .NET cho OS, cache OS nguyên).8
Tài liệu chính thức đóng đinh rõ ── gọi mỗi lần ghi thì không hiệu quả. Nhiều lần ghi mà mỗi lần cần bền thì nên dùng NO_BUFFERING+WRITE_THROUGH nói sau.2
5.2. FILE_FLAG_WRITE_THROUGH ── chỉ bỏ độ trễ
Mở FILE_FLAG_WRITE_THROUGH thì ghi vừa vào cache, vừa ghi ngay ra đĩa không chờ lazy writer.1 Điểm then chốt: đọc vẫn hưởng cache; câu trả lời thẳng cho “đọc vẫn nhanh, chỉ muốn hết độ trễ ghi”.
5.3. FILE_FLAG_NO_BUFFERING ── không đi cache
FILE_FLAG_NO_BUFFERING gỡ luôn system cache khỏi đọc ghi. Mọi đọc ghi không đi cache, mỗi lần thành I/O tới thiết bị đĩa.1 Tuy nhiên chỉ vòng được đến system cache Windows; như Hình 5, cache ghi trong thiết bị là tầng khác. Cần chịu mất điện thì vẫn cần kết hợp WRITE_THROUGH hay FlushFileBuffers. Công cụ cho truyền khối lượng lớn một lượt, hay engine cơ sở dữ liệu tự quản buffer, nhưng kèm cam kết chặt.3
- Kích thước đọc ghi và offset tệp là bội số nguyên kích thước sector volume (sector 512 byte thì 512, 1024, 1536…).
- Địa chỉ buffer cũng căn theo kích thước sector vật lý (cần tính đến đĩa “Advanced Format” sector vật lý 4096 byte).
- Dù vậy metadata vẫn được cache, nên bền hoàn toàn cần kết hợp WRITE_THROUGH hoặc
FlushFileBuffers.12
“Cam kết” này là chỗ người tưởng chỉ thêm cờ là xong ngã đầu. Đọc ghi không giữ căn chỉnh thì thất bại ERROR_INVALID_PARAMETER (87). Ba điểm phải giữ, sắp như sau.3
| Thứ phải thẳng | Điều kiện | Làm sao thỏa |
|---|---|---|
| Kích thước đọc ghi | Bội số nguyên kích thước sector volume | Lấy lpBytesPerSector của GetDiskFreeSpace rồi làm tròn bội số đó |
| Offset tệp | Như trên (chỉ định Offset của OVERLAPPED cũng vậy) |
Tiến từng bội số kích thước sector |
| Địa chỉ buffer | Căn kích thước sector vật lý | Cấp bằng VirtualAlloc (trả vùng căn biên trang, thường 4096 byte) |
Cái thứ ba hay bị sót nhất. Địa chỉ malloc, new, mảng C# trả không bảo đảm căn biên sector. Dùng VirtualAlloc cấp trên biên trang thì đồng thời thỏa yêu cầu đĩa “Advanced Format” sector vật lý 4096 byte. Dạng tối thiểu như sau.
// C++ / Win32. Xử lý lỗi giữ ở mức tối thiểu
DWORD sectorsPerCluster = 0, bytesPerSector = 0, freeClusters = 0, totalClusters = 0;
if (!GetDiskFreeSpaceW(L"C:\\", §orsPerCluster, &bytesPerSector,
&freeClusters, &totalClusters))
{
return GetLastError();
}
// Làm đơn vị đọc ghi thành bội số nguyên kích thước sector (ở đây tương đương 1MiB)
const DWORD chunk = (1024 * 1024 / bytesPerSector) * bytesPerSector;
// buffer lấy vùng căn biên trang (malloc/new không bảo đảm)
BYTE* buffer = static_cast<BYTE*>(
VirtualAlloc(nullptr, chunk, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
if (buffer == nullptr) { return GetLastError(); }
HANDLE h = CreateFileW(L"C:\\temp\\big.bin", GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, nullptr);
if (h == INVALID_HANDLE_VALUE)
{
// GetLastError trả kết quả của «lời gọi Win32 ngay trước». Gọi VirtualFree
// trước thì lý do CreateFileW thất bại (từ chối truy cập, không có đường dẫn, v.v.)
// bị kết quả dọn dẹp ghi đè, chỉ trả mã không rõ nguyên nhân
const DWORD err = GetLastError();
VirtualFree(buffer, 0, MEM_RELEASE);
return err;
}
// Mỗi lần tiến theo đơn vị chunk byte nên kích thước và offset đều giữ căn chỉnh
DWORD read = 0;
while (ReadFile(h, buffer, chunk, &read, nullptr) && read > 0)
{
// Xử lý read byte đầu của buffer
// (cuối tệp thì read < chunk. Đây là bình thường)
}
CloseHandle(h);
VirtualFree(buffer, 0, MEM_RELEASE);
Ngoài ra FileOptions của .NET không có giá trị tương ứng FILE_FLAG_NO_BUFFERING. Thật sự cần thì gọi CreateFile trực tiếp, lúc đó vẫn phải tự giữ yêu cầu căn chỉnh trên. Hãy xét theo thứ tự “NO_BUFFERING vì tự quản buffer”, không phải “NO_BUFFERING vì muốn nhanh”.
5.4. Sắp xếp cách dùng
flowchart TB
A["Buffer ứng dụng"]
B["System file cache<br/>(trang dirty)"]
C["Cache trong thiết bị đĩa"]
D[("Môi trường ghi không bay hơi")]
A -->|"WriteFile mặc định: đến đây thì trả thành công"| B
B -->|"lazy writer (mỗi giây) / WRITE_THROUGH (ngay)"| C
C -->|"Nhịp thiết bị /<br/>FlushFileBuffers đòi ghi hết"| D
A -.->|"NO_BUFFERING nhảy cache, đi thẳng"| C
Hình 5: Các tầng dữ liệu và từng công cụ đẩy đến đâu. Chú ý cả tầng cuối “cache trong thiết bị đĩa”
| Cách | Chuyện gì xảy ra | Chỗ hợp |
|---|---|---|
| Mặc định (bật cache) | Xong lúc sao chép cache. Phản ánh do lazy writer | Hầu hết I/O tệp |
FlushFileBuffers / Flush(true) |
Ghi hết dữ liệu + metadata tại thời điểm đó | Xác nhận ở mốc (commit giao dịch, v.v.) |
FILE_FLAG_WRITE_THROUGH |
Mỗi lần ghi ra đĩa ngay (đọc vẫn cache) | Log, journal ghi liên tục không được mất |
FILE_FLAG_NO_BUFFERING (+WRITE_THROUGH) |
Không đi cache. Có yêu cầu căn chỉnh | Tự quản buffer, I/O khối lượng lớn một lượt |
Thứ tự chọn là hai tầng: trước hết quyết “mất điện được mất mấy bản”, rồi xác nhận “vì thế chậm thêm được bao nhiêu”. Đừng nhìn bảng từ trên xuống; đi nhánh này.
flowchart TB
S["Đang định ghi dữ liệu này"]
Q1{"Khoảnh khắc mất điện, màn hình xanh<br/>mất được không"}
A0["Giữ mặc định (bật cache)<br/>nhanh nhất. Hầu hết I/O ở đây"]
Q2{"Thứ không được mất là<br/>'mốc' hay 'từng bản'"}
A1["Mốc thì FlushFileBuffers<br/>.NET thì Flush(true)<br/>chi phí: chỉ chờ ở mốc"]
Q3{"Tự quản buffer và<br/>thỏa yêu cầu căn chỉnh 5.3 không"}
A2["FILE_FLAG_WRITE_THROUGH<br/>mỗi lần ghi ra đĩa ngay<br/>đọc vẫn nhanh nhờ cache"]
A3["FILE_FLAG_NO_BUFFERING<br/>+ FILE_FLAG_WRITE_THROUGH<br/>dạng 'bền thường xuyên' tài liệu chính thức nêu"]
S --> Q1
Q1 -->|"Được<br/>(vài giây log gần nhất, v.v.)"| A0
Q1 -->|"Không được"| Q2
Q2 -->|"Mốc<br/>(xác nhận giao dịch, v.v.)"| A1
Q2 -->|"Từng bản"| Q3
Q3 -->|"Không (ứng dụng thường)"| A2
Q3 -->|"Có (engine DB, v.v.)"| A3
Hình 6: Cách chọn công cụ. Nhánh tầng 1 là yêu cầu tin cậy, tầng 2 là chi phí chịu được. Không có đường “mỗi bản FlushFileBuffers” vì, như 5.1, tài liệu chính thức coi đó không hiệu quả
Ba mẫu thực tế cũng chỉ nêu.
- “Ghi tệp tạm → flush → đổi tên” là lối quen không để lại tệp dở. Ghi hết nội dung rồi xác nhận bằng tên ── bàn giao nguyên tử này xử chi tiết trong “Kiến thức nền kiểm soát loại trừ khi liên kết tệp”.
- Giao cơ sở dữ liệu cũng là thiết kế đứng đắn. Chuyện SQLite xây độ bền bằng WAL và flush xem “Dùng SQLite từ C# trong ứng dụng nghiệp vụ”. Lựa chọn “không tự viết chiến lược flush” luôn có trên bàn.
- Benchmark thì nghi cache. Đo “đọc nhanh quá” thường đang đo hit cache từ lần hai trở đi. Phép đo đúng gom trong “Cách so sánh đúng tốc độ theo phiên bản chương trình trên Windows”.
Và đừng quên tầng cuối Hình 5 ── cache trong thiết bị đĩa. FlushFileBuffers đòi ghi hết gồm tầng đó, nhưng USB hay đĩa ngoài thì chính sách cache ghi phía thiết bị (“Tháo nhanh” và “Hiệu năng cao”) xen vào. Xử lý thiết bị tháo được xem thêm “Cách xử lý thiết bị USB từ ứng dụng Windows”.
6. Nhất quán với memory-mapped file
Kỳ 1 nghe “thực thể cache là file mapping”, hẳn có người nghĩ ── vậy view mình MapViewOfFile và cache của ReadFile/WriteFile có đánh nhau không?
Không. Vì ngồi trên cùng cơ chế. Đối tượng file mapping được chống lưng bằng tệp; đuổi trang thực hiện như ghi lại vào tệp. Nhiều tiến trình tạo view của cùng tệp cục bộ, nội dung thấy vẫn coherent (nhất quán).4
flowchart TB
subgraph P1["Không gian địa chỉ tiến trình A"]
V1["View của MapViewOfFile"]
end
subgraph SYS["Không gian địa chỉ hệ thống"]
SC["View của Cache Manager<br/>(khe ReadFile/WriteFile dùng)"]
end
PAGES["Cùng nhóm trang vật lý<br/>(bộ nhớ chống lưng bằng tệp)"]
DISK[("Tệp trên đĩa")]
V1 --> PAGES
SC --> PAGES
PAGES --> DISK
NB["I/O FILE_FLAG_NO_BUFFERING<br/>nằm ngoài khung chia sẻ này (thẳng ra đĩa)"]
NB -.-> DISK
Hình 7: Cả map view lẫn cache đều nhìn cùng “trang chống lưng bằng tệp”. Ngoài khung chỉ có NO_BUFFERING
Hai điểm chú ý.
- I/O
FILE_FLAG_NO_BUFFERINGnằm ngoài khung nhất quán này. Đọc ghi không đi cache không được đối chiếu với nội dung qua map view/cache. Trộn thì phải tự giữ khớp. - Bền map view là hai tầng.
FlushViewOfFilebắt đầu ghi ra trang dirty trong phạm vi, nhưng không ghi metadata, cũng không chờ ghi vật lý từ cache thiết bị đĩa. Đưa chắc thì sauFlushViewOfFilegọiFlushFileBuffers.5
Thực tế file mapping như bộ nhớ chia sẻ (chia sẻ có tên, đồng bộ, mẫu tai nạn) xử trong “Bẫy bộ nhớ chia sẻ và thực hành tốt nhất”.
7. Fast I/O ── thu bài tập kỳ 1
Hai dòng trước. Fast I/O là đường tắt chuẩn bị cho đọc ghi đồng bộ tới tệp đang nằm cache, trao đổi dữ liệu trực tiếp với cache mà không dựng IRP (gói yêu cầu I/O kernel đưa driver). Cột Operation của Procmon lẫn IRP_MJ_READ và FASTIO_READ vì cùng “đọc” đi đường thường hay đường tắt.
Mục 5.2 kỳ 1 viết “không phải mọi I/O đều thành IRP”. Đối đáp.
Đọc ghi tệp đang nằm cache, không cần dựng IRP chảy device stack, sao chép bộ nhớ với cache là xong. Vì vậy Windows chuẩn bị đường tắt Fast I/O cho I/O đồng bộ tới tệp đã cache ── không tạo IRP, gọi thẳng “điểm vào Fast I/O” của hệ thống tệp, sao chép trực tiếp từ Cache Manager.6 Không xử lý được bằng Fast I/O (không nằm cache, dính khóa, filter cắt vào, v.v.) thì gập về đường IRP thường. Đây là đường nhanh cho yêu cầu đồng bộ, không phải “hit cache = luôn Fast I/O”. Thao tác handle bất đồng bộ (FILE_FLAG_OVERLAPPED), dù hoàn tất tại chỗ từ cache (Chương 5 kỳ 2), đôi khi vẫn xử lý đường IRP.
flowchart TB
REQ["Đọc ghi đồng bộ tới handle bật cache"]
Q{"Xử lý được bằng Fast I/O không<br/>(đang nằm cache, v.v.)"}
FAST["Fast I/O<br/>không tạo IRP, sao chép thẳng với cache<br/>Procmon hiện FASTIO_"]
IRP["Đường thường<br/>dựng IRP rồi xuống device stack<br/>(thế giới Hình 6 kỳ 1)"]
REQ --> Q
Q -->|được| FAST
Q -->|không được| IRP
Hình 8: Nhánh Fast I/O. Procmon thấy lẫn FASTIO_READ và IRP_MJ_READ vì vậy
Lý do quan sát Procmon Chương 7 kỳ 1 lẫn dòng FASTIO_ giải thích được. Đọc hit cache, IRP còn là xa xỉ. Sự tồn tại đường này còn ảnh hưởng filter driver kỳ 6 (minifilter cắt được cả Fast I/O).
8. Tóm tắt
- Cache tệp Windows là write-back, thực thể là map đoạn 256KB của tệp. Đọc ghi bật cache thành sao chép bộ nhớ với khe.1
- Đọc thì đọc trước đầu cơ;
SequentialScan/RandomAccesslà gợi ý đó.1 - Ghi thì lazy writer mỗi giây đuổi theo. Ứng dụng chết dữ liệu còn; OS chết cả thì chỉ phần dirty mất. Câu hỏi thiết kế là “dữ liệu này, khoảnh khắc mất điện, mất được không”.1
- Công cụ ghi chắc là
FlushFileBuffers(xác nhận mốc) /WRITE_THROUGH(mỗi lần ghi) /NO_BUFFERING(không đi cache + yêu cầu căn chỉnh). Flush mỗi lần không hiệu quả; bền thường xuyên thì tài liệu chính thức khuyến nghị kết hợp NO_BUFFERING+WRITE_THROUGH. Chú ý metadata luôn được cache.231 - Map view và cache chia sẻ cùng trang, nhất quán. Ngoài khung chỉ NO_BUFFERING. Bền map là hai tầng
FlushViewOfFile+FlushFileBuffers.45 - Đọc ghi đồng bộ hit cache bỏ luôn IRP bằng Fast I/O. Đó là bản chất
FASTIO_thấy trên Procmon kỳ 1.6
Tiếp là kỳ 5 “Cấu trúc trong của NTFS ── hiểu hệ thống tệp từ MFT”. Đến kỳ này tệp được xử như “offset và dãy byte”; phía sau NTFS bố trí dữ liệu thế nào ── MFT, nhiều luồng dữ liệu, journal, hard link ── xuống cấu trúc tĩnh trên đĩa.
Bài viết liên quan
- Đáy I/O Windows (kỳ 1) ── mọi đọc ghi thành IRP: toàn cảnh hệ thống I/O
- Đáy I/O Windows (kỳ 2) ── I/O đồng bộ và bất đồng bộ: nghĩa thật của OVERLAPPED
- Đáy I/O Windows (kỳ 3) ── cổng hoàn tất I/O (IOCP) và thread pool .NET: tầng hầm của async/await
- Kiến thức nền kiểm soát loại trừ khi liên kết tệp ── thực hành tốt nhất khóa tệp và claim nguyên tử
- Bẫy bộ nhớ chia sẻ và thực hành tốt nhất
- Dùng SQLite từ C# trong ứng dụng nghiệp vụ ── chế độ WAL, kiểm soát loại trừ, chống hỏng, dùng tách với EF Core
- Cách so sánh đúng tốc độ theo phiên bản chương trình trên Windows
- Cách xử lý thiết bị USB từ ứng dụng Windows ── chọn COM ảo, HID, WinUSB, SDK riêng
Lĩnh vực tư vấn liên quan
KomuraSoft LLC nhận thiết kế và điều tra I/O tệp của ứng dụng nghiệp vụ Windows, kiểu “dữ liệu tưởng đã lưu lại mất”, “ghi tệp chậm / nhanh đến mức đáng ngờ”.
- Phát triển ứng dụng Windows
- Điều tra lỗi và nguyên nhân
- Tái sử dụng và di chuyển tài sản hiện có
- Liên hệ với chúng tôi
Liên kết tham khảo
-
Microsoft Learn, File Caching. Về Windows mặc định cache dữ liệu tệp, đọc từ system file cache, ghi cũng vào cache theo kiểu write-back; cache quản theo file object, chạy dưới chỉ huy Cache Manager; chính sách giữ trên cache, hoãn ghi ra đĩa gọi là ghi trễ (lazy writing); lúc đọc tệp, đoạn 256KB được đọc vào khe 256KB không gian địa chỉ hệ thống, tiến trình người dùng sao chép dữ liệu với khe đó; Cache Manager mỗi giây khởi lazy writer, chất 1/8 trang gần đây chưa flush vào hàng đợi ghi đĩa, cần thì chất thêm; tệp tạm không bị flush; sự cố hệ thống đột ngột như mất điện thì dữ liệu cache chưa ghi mất; FILE_FLAG_NO_BUFFERING tắt cache rồi metadata tệp vẫn có thể được cache; FILE_FLAG_WRITE_THROUGH thì dữ liệu vừa vào cache vừa ghi ngay ra đĩa không độ trễ lazy writer; metadata hệ thống tệp luôn được cache nên bền metadata cần flush hoặc FILE_FLAG_WRITE_THROUGH. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19
-
Microsoft Learn, FlushFileBuffers function. Về WriteFile thường ghi vào buffer nội bộ, OS định kỳ ghi ra đĩa; FlushFileBuffers ghi hết thông tin đã đệm của tệp chỉ định ra thiết bị; gọi mỗi lần trong nhiều lần ghi thì không hiệu quả, ứng dụng cần bền dữ liệu quan trọng khi ghi thường xuyên nên dùng I/O không đệm bằng FILE_FLAG_NO_BUFFERING và FILE_FLAG_WRITE_THROUGH; gọi trên handle volume (quyền quản trị viên) thì flush mọi tệp đang mở trên volume. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, File Buffering. Về yêu cầu truy cập tệp mở FILE_FLAG_NO_BUFFERING: kích thước đọc ghi và offset tệp (kể cả chỉ định OVERLAPPED) phải là bội số nguyên kích thước sector volume; địa chỉ buffer đọc ghi nên căn kích thước sector vật lý; cần tính đến thiết bị Advanced Format sector vật lý 4.096 byte. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, File Mapping. Về đối tượng file mapping được chống lưng bằng tệp trên đĩa, swap-out trang thực hiện như ghi nội dung đã đổi vào tệp; nhiều tiến trình tạo view tệp cục bộ từ cùng đối tượng file mapping thì dữ liệu coherent (cùng nội dung với tệp trên đĩa). ↩ ↩2 ↩3
-
Microsoft Learn, FlushViewOfFile function. Về FlushViewOfFile bắt đầu ghi trang dirty trong phạm vi map view ra đĩa; hàm này không flush metadata tệp, cũng không chờ hoàn tất ghi vật lý từ cache đĩa phần cứng; để ghi hết trang dirty và metadata ra vật lý thì sau FlushViewOfFile nên gọi FlushFileBuffers. ↩ ↩2 ↩3
-
Microsoft Learn, IRPs Are Different From Fast I/O. Về Fast I/O là đường nhanh I/O đồng bộ cho tệp đã cache, gọi thẳng điểm vào hệ thống tệp hay Cache Manager mà không sinh IRP; dữ liệu được chuyển trực tiếp từ cache sang buffer người dùng (hoặc ngược); không xử lý được bằng Fast I/O thì dùng đường thường dựa IRP. ↩ ↩2 ↩3
-
Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. Về dữ liệu nằm cache thì yêu cầu hoàn tất tại chỗ, trả TRUE; cache Windows triển khai bằng file mapping, không có cơ chế page fault bất đồng bộ khi chưa có trang, nên đọc bất đồng bộ bật cache đôi khi bị xử lý đồng bộ. ↩
-
Microsoft Learn, FileStream.Flush method (.NET). Về Flush() đưa buffer nội bộ của stream ra OS; chỉ định Flush(true) thì thêm flush mọi buffer tệp trung gian (buffer OS). ↩
Bài viết liên quan
Các bài viết gần đây có cùng thẻ để tìm hiểu sâu hơn những chủ đề lân cận.
Đáy I/O Windows (kỳ 5) ── cấu trúc trong của NTFS: hiểu hệ thống tệp từ MFT
Kỳ 5 loạt bài hình giải cấu trúc trong của NTFS. Sắp xếp MFT và bản ghi tệp, nhiều luồng dữ liệu (Zone.Identifier), hard link và tên 8.3,...
Đáy I/O Windows (kỳ 6, cuối) ── filter driver và minifilter: vì sao Procmon và quét virus cắt được vào I/O
Kỳ cuối loạt bài hình giải filter driver và minifilter của Windows. Sắp xếp Filter Manager và altitude, callback pre/post, cơ chế Procmon...
OneDrive "Tệp theo yêu cầu" và ứng dụng nghiệp vụ — giả định mà chỗ giữ chỗ phá vỡ và cách xử lý
CSV trên máy tính không mở được, hoặc nhập thất bại với "không tìm thấy tệp" — nguyên nhân có thể là Known Folder Move và Tệp theo yêu cầ...
Proxy doanh nghiệp và ứng dụng Windows — sắp xếp việc phân giải proxy trong WinINET, WinHTTP và .NET
Trình duyệt đi được, nhưng chỉ ứng dụng nghiệp vụ không vượt proxy doanh nghiệp. Nguyên nhân thường là lệch nhau về thiết lập proxy mà Wi...
Thực hành tốt nhất về đa luồng: ấn bản .NET — Những gì cần quyết trước khi thêm luồng
Tổng hợp các định thức thiết kế ngăn «thêm luồng rồi thỉnh thoảng crash hoặc treo» cho .NET/C#. Bài viết đi từ việc dựa trên Task thay vì...
Chủ đề liên quan
Các trang này đặt chủ đề trong bối cảnh rộng hơn của dịch vụ và quyết định.
Chủ đề kỹ thuật Windows
Cổng vào phát triển Windows, điều tra lỗi và khai thác tài sản hiện có.
Dịch vụ liên quan đến chủ đề này
Bài viết liên quan trực tiếp đến các dịch vụ sau.
Phát triển ứng dụng Windows
Ứng dụng nghiệp vụ, tích hợp thiết bị và công cụ liên lạc, từ yêu cầu đến phát triển.
Câu hỏi thường gặp
Các câu hỏi thường gặp khi tư vấn về chủ đề của bài viết.
- WriteFile trả thành công thì dữ liệu đã ghi ra đĩa chưa?
- Mặc định chưa. Cache tệp Windows là kiểu write-back; WriteFile trả thành công lúc đã sao chép dữ liệu vào system file cache. Ghi ra đĩa do luồng ghi trễ (lazy writer) mà Cache Manager khởi mỗi giây làm sau. Điểm then chốt là khác biệt theo loại sự cố. Tiến trình ứng dụng sập, dữ liệu đã vào cache vẫn được ghi sau miễn OS còn sống, nên không mất. Ngược lại sự cố sập cả OS (mất điện, màn hình xanh) thì cache dirty chưa ghi sẽ mất. Hiểu đúng không phải "WriteFile thành công = đã bền", mà "thành công = đã giao cho OS".
- Làm sao ghi chắc ra đĩa?
- Có ba công cụ. Thứ nhất FlushFileBuffers, ghi hết dữ liệu đã đệm và metadata của tệp đó ra thiết bị (.NET thì FileStream.Flush(true) tương đương). Thứ hai FILE_FLAG_WRITE_THROUGH, mỗi lần ghi vừa vào cache vừa ghi ngay ra đĩa. Thứ ba FILE_FLAG_NO_BUFFERING, không đi qua cache. Tài liệu Microsoft nói gọi FlushFileBuffers mỗi lần ghi là không hiệu quả; ghi thường xuyên mà cần bền chắc thì nên dùng kết hợp FILE_FLAG_NO_BUFFERING và FILE_FLAG_WRITE_THROUGH. Cả ba đều chậm hơn vì bỏ lợi cache, nên điểm thực tế là không "gắn hết" mà chỉ dùng cho ghi dữ liệu không được mất.
- FILE_FLAG_WRITE_THROUGH và FILE_FLAG_NO_BUFFERING khác nhau thế nào?
- WRITE_THROUGH là "vẫn ghi vào cache, nhưng trước khi hoàn tất cũng ghi ra đĩa". Đọc vẫn hưởng cache; chỉ bỏ độ trễ ghi trễ. NO_BUFFERING là "đọc ghi không đi system cache"; cả đọc lẫn ghi mỗi lần thành I/O tới thiết bị đĩa (nhưng chỉ vòng được đến cache Windows, không nhảy luôn cache ghi trong thiết bị). Đổi lại ràng buộc chặt: kích thước đọc ghi và offset tệp phải là bội số nguyên của kích thước sector volume, địa chỉ buffer cũng phải căn biên sector vật lý. NO_BUFFERING rồi metadata hệ thống tệp vẫn được cache, nên ghi chắc cả metadata thì cần FlushFileBuffers hoặc kết hợp WRITE_THROUGH. Phần mềm tự quản buffer kiểu engine cơ sở dữ liệu hay dùng; ứng dụng thường thì xét WRITE_THROUGH hay FlushFileBuffers trước là thuận.
- Task Manager thấy bộ nhớ trống ít, có phải vì cache tệp?
- Nhiều trường hợp đúng, và đó là hành vi bình thường. Windows chủ động dùng bộ nhớ vật lý đang trống làm cache tệp; sao chép tệp lớn hay đọc ghi nhiều thì cache phình, dung lượng bộ nhớ dùng trông tăng. Tuy nhiên nhiều trang cache đang dùng thuộc loại ứng dụng đòi bộ nhớ thì nhường khá nhanh; cần tách với trạng thái "bộ nhớ bị ăn hết, không đủ". Nghi thiếu bộ nhớ thì ngoài dung lượng trống bề ngoài, hãy xem chỉ số như bộ nhớ đã commit và tần suất hard fault.
- Cùng một tệp, memory-mapped file và ReadFile/WriteFile đụng thì nội dung có lệch không?
- Với I/O bật cache thường thì không lệch. Bản thân cache Windows triển khai bằng file mapping; view map và cache của cùng tệp cục bộ chia sẻ cùng dữ liệu, nên đổi bên này bên kia cũng thấy. Nhiều tiến trình tạo view từ cùng đối tượng file mapping thì dữ liệu cũng coherent (nhất quán). Tuy nhiên đọc ghi handle mở FILE_FLAG_NO_BUFFERING không đi cache, nên nằm ngoài khung nhất quán này. Muốn ghi chắc đổi trên map view ra đĩa, chỉ FlushViewOfFile thì metadata không được ghi, cache phần cứng cũng không chờ, nên sau FlushViewOfFile phải gọi FlushFileBuffers.