Đáy I/O Windows (kỳ 4) ── Cache Manager: WriteFile của bạn đến đĩa khi nào

· · Windows, Win32, I/O, Cache, Kernel, Hệ thống tệp, .NET, CSharp

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.

Bản đồ tri thức Cache Manager và write-behindSơ đồ 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/Otriển khaisử dụngsử dụngsử dụngsử dụngsử dụngtự động hóagiảm thiểukhông tương thíchcó thể gâylưu trongcó thể gâyngăn chặnsử dụngcấu hình bằngcấu hình bằngngăn chặnngăn chặngiảm thiểuyêu cầungăn chặncó thể gâykhông khuyến nghịkhuyến nghị chokhuyến nghị chokhông tương thíchyêu cầunên làm trướcxác minh bằngyêu cầukhông tương thíchyêu cầuCache ManagerCache write-back (kiểu ghi trễ)File mapping (memory-mapped file)View cache hệ thống (khe 256KB)File objectlazy writer (luồng ghi trễ)Mất dữ liệu chưa phản ánhFILE_ATTRIBUTE_TEMPORARYTrang dirtyMất điện / OS sậpĐọc trước (read-ahead)FILE_FLAG_SEQUENTIAL_SCANFILE_FLAG_RANDOM_ACCESSFlushFileBuffersFILE_FLAG_WRITE_THROUGHFILE_FLAG_NO_BUFFERINGYêu cầu căn chỉnh sectorERROR_INVALID_PARAMETER (87)Bền chắc khi ghi thường xuyênFlushViewOfFileFast I/OProcess Monitor (procmon.exe)IRP (gói yêu cầu I/O)I/O đồng bộ

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

Không gian địa chỉ hệ thốngỨng dụng (user mode)ReadFile/WriteFile =sao chép bộ nhớ với kheĐọc lúc truy cập lần đầu vàghi lại sau theo trangSystem file cachekhe map đoạn 256KB của tệpBuffer ứng dụng(vùng đưa cho ReadFile/WriteFile)Tệp trên đĩa

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.

Lịch sử yêu cầu đọc của ứng dụngđang đọc từ đầu theo thứ tựCache Managerphát hiện mẫuĐọc trước: đoạn tiếpđọc sẵn trước khi bị yêu cầu(lượng đổi theo mẫu và kích thước yêu cầu)Gợi ý FILE_FLAG_SEQUENTIAL_SCAN= đọc trước tích cựcGợi ý FILE_FLAG_RANDOM_ACCESS= đọc trước thành lãng phí nên kìm

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.

Đĩalazy writer (khởi mỗi giây)System cacheỨng dụngĐĩalazy writer (khởi mỗi giây)System cacheỨng dụngSao chép vào kheđánh dấu trang dirty (chưa ghi)Từ đây đến ghi lại là "cửa sổ nguy hiểm"mất điện, OS sập thì dữ liệu này mấtLần đầu bền ở đâyWriteFile (dữ liệu)TRUE trả ngayChọn 1/8 trang dirtyGhi lại gộp

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.

Dữ liệu ngay sau WriteFile thành công(trang dirty trên cache)Chuyện gì xảy raTiến trình ứng dụngsập / bị buộc kết thúcDừng cả OS(mất điện, màn hình xanh)Dữ liệu còncache thuộc OS nênlazy writer ghi lại đúng lịchTrang dirty mấtchỉ phần đã đến đĩa còn

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:\\", &sectorsPerCluster, &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

WriteFile mặc định: đến đây thì trả thành cônglazy writer (mỗi giây) / WRITE_THROUGH (ngay)Nhịp thiết bị /FlushFileBuffers đòi ghi hếtNO_BUFFERING nhảy cache, đi thẳngBuffer ứng dụngSystem file cache(trang dirty)Cache trong thiết bị đĩaMôi trường ghi không bay hơi

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.

Được(vài giây log gần nhất, v.v.)Không đượcMốc(xác nhận giao dịch, v.v.)Từng bảnKhông (ứng dụng thường)Có (engine DB, v.v.)Đang định ghi dữ liệu nàyKhoảnh khắc mất điện, màn hình xanhmất được khôngGiữ mặc định (bật cache)nhanh nhất. Hầu hết I/O ở đâyThứ không được mất là'mốc' hay 'từng bản'Mốc thì FlushFileBuffers.NET thì Flush(true)chi phí: chỉ chờ ở mốcTự quản buffer vàthỏa yêu cầu căn chỉnh 5.3 khôngFILE_FLAG_WRITE_THROUGHmỗi lần ghi ra đĩa ngayđọc vẫn nhanh nhờ cacheFILE_FLAG_NO_BUFFERING+ FILE_FLAG_WRITE_THROUGHdạng 'bền thường xuyên' tài liệu chính thức nêu

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.

  1. “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”.
  2. 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.
  3. 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

Không gian địa chỉ hệ thốngKhông gian địa chỉ tiến trình AView của Cache Manager(khe ReadFile/WriteFile dùng)View của MapViewOfFileCùng nhóm trang vật lý(bộ nhớ chống lưng bằng tệp)Tệp trên đĩaI/O FILE_FLAG_NO_BUFFERINGnằm ngoài khung chia sẻ này (thẳng ra đĩa)

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_BUFFERING nằ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. FlushViewOfFile bắ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ì sau FlushViewOfFile gọi FlushFileBuffers.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_READFASTIO_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.

đượckhông đượcĐọc ghi đồng bộ tới handle bật cacheXử lý được bằng Fast I/O không(đang nằm cache, v.v.)Fast I/Okhông tạo IRP, sao chép thẳng với cacheProcmon hiện FASTIO_Đường thườngdựng IRP rồi xuống device stack(thế giới Hình 6 kỳ 1)

Hình 8: Nhánh Fast I/O. Procmon thấy lẫn FASTIO_READIRP_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/RandomAccess là 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

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ờ”.

Liên kết tham khảo

  1. 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

  2. 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

  3. 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

  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

  5. 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

  6. 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

  7. 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ộ. 

  8. 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). 

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.

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.

Bài viết liên quan trực tiếp đến các dịch vụ sau.

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.

Hồ sơ tác giả

Trang giới thiệu tác giả bài viết.

Go Komura

Đại diện của KomuraSoft LLC

Chuyên về phát triển phần mềm Windows, tư vấn kỹ thuật và điều tra lỗi, đặc biệt trong các dự án có hệ thống hiện hữu và lỗi khó tái hiện.

Liên kết công khai

Quay lại blog