WriteFile คืนความสำเร็จแล้ว ข้อมูลตอนนี้อยู่ที่ไหน?
คำตอบคือ เกือบชัวร์ว่า ยังไม่อยู่บนดิสก์ แค่ถูกคัดลอกเข้าแคชบนหน่วยความจำ นั่นคือเหตุที่ «บันทึกแล้ว แต่ตัดไฟแล้วกลับมาหาย» เกิด เหตุที่เกณฑ์วัดการคัดลอกไฟล์ตีความเร็วที่เป็นไปไม่ได้ทางกายภาพ และเหตุที่ฐานข้อมูลเรียก fsync อย่างเคร่ง
ตอนที่ 4 ของซีรีส์ «เชิงลึกของ Windows I/O» คือ Cache Manager ที่ยืนกลางเรื่องนี้ ใน ตอนที่ 2 เขียนว่า «ถ้าอยู่บนแคช แม้ I/O อะซิงค์ก็จบแบบซิงค์» และใน ตอนที่ 1 ตั้งการบ้าน «ทางลัดที่ไม่สร้าง IRP (Fast I/O)» คราวนี้จะเก็บเส้นทั้งสอง
1. สรุปก่อนเลย
- แคชไฟล์ของ Windows เป็นแบบไรต์แบ็ก อ่านจากแคชไฟล์ระบบก่อน เขียนก็เข้าแคชก่อน การสะท้อนไปดิสก์ OS ทำทีหลัง1
- ตัวจริงของแคชคือการแมปไฟล์ Cache Manager แมปช่วง 256KB ของไฟล์เข้าพื้นที่ที่อยู่ของระบบ และการอ่านเขียนที่แคชเปิดกลายเป็น «การคัดลอกหน่วยความจำระหว่างวิวนั้นกับบัฟเฟอร์ของแอป» (หมวด 2)1
- การเขียนถูกตามด้วยการเขียนแบบหน่วง (lazy writer) ทุกวินาที แอปพังข้อมูลไม่หาย แต่ ตัดไฟหรือ OS พัง เพจสกปรกบนแคชหาย (หมวด 4)1
- เครื่องมือ «เขียนให้ชัวร์» มีสาม
FlushFileBuffers(=Flush(true)ของ .NET)FILE_FLAG_WRITE_THROUGHFILE_FLAG_NO_BUFFERINGฟลัชทุกครั้งที่เขียนบ่อยไม่มีประสิทธิภาพ และเอกสารทางการยกการใช้ NO_BUFFERING+WRITE_THROUGH คู่กัน (หมวด 5)21 - NO_BUFFERING มีข้อกำหนดจัดแนว ขนาด・ออฟเซ็ตต้องเป็นจำนวนเท่าของขนาดเซกเตอร์ ที่อยู่บัฟเฟอร์ก็ต้องจัดแนวตามขอบเซกเตอร์กายภาพ และแม้ NO_BUFFERING เมทาดาตายังถูกแคชต่อ (หมวด 5.3)31
- วิวแมปกับแคชแชร์ข้อมูลเดียวกัน ไฟล์แมปหน่วยความจำกับ I/O แคชปกติสอดคล้อง และการทำให้แมปทนทานเป็นสองขั้น
FlushViewOfFile+FlushFileBuffers(หมวด 6)45 - การอ่านเขียนซิงค์ที่อยู่บนแคช บางครั้งไม่สร้างแม้ IRP ทางลัด Fast I/O ตรงเข้า Cache Manager ── คำตอบการบ้านของตอนที่ 1 (หมวด 7)6
แผนที่ความรู้ของบทความนี้
ความสำเร็จของ WriteFile ไม่ได้หมายถึงความทนทานบนดิสก์ แคชไฟล์ของ Windows คัดลอกเข้าวิวแคชระบบก่อน และการสะท้อนไปดิสก์ถูกตามด้วย lazy writer ทุกวินาที เมื่อตัดไฟหรือ OS พัง เพจสกปรกที่ยังไม่ถูกเขียนจะหาย ดังนั้นข้อมูลที่อยากเขียนให้ชัวร์ต้องเลือกใช้ FlushFileBuffers, FILE_FLAG_WRITE_THROUGH และ FILE_FLAG_NO_BUFFERING
flowchart LR
accTitle: แผนที่ความรู้ Cache Manager กับการเขียนแบบหน่วง
accDescr: แผนภาพที่แสดงความสัมพันธ์ระหว่าง Cache Manager, แคชไรต์แบ็ก, วิวแคชระบบ 256KB, การอ่านล่วงหน้า, การเขียนแบบหน่วงโดย lazy writer, การสูญเสียเพจสกปรกเมื่อตัดไฟ, การเขียนให้ชัวร์ด้วย FlushFileBuffers・FILE_FLAG_WRITE_THROUGH・FILE_FLAG_NO_BUFFERING, ข้อกำหนดจัดแนว, ความสอดคล้องกับไฟล์แมปหน่วยความจำ และ Fast I/O
cache_manager["Cache Manager"]
write_behind_caching["แคชไรต์แบ็ก (วิธีเขียนแบบหน่วง)"]
memory_mapped_file["ไฟล์แมปหน่วยความจำ"]
system_cache_view["วิวแคชระบบ (ช่อง 256KB)"]
file_object["ไฟล์อ็อบเจ็กต์"]
lazy_writer["lazy writer (เธรดการเขียนแบบหน่วง)"]
unflushed_write_loss["การสูญเสียข้อมูลที่ยังไม่ถูกสะท้อน"]
temp_file_attribute["FILE_ATTRIBUTE_TEMPORARY"]
dirty_page["เพจสกปรก"]
power_loss["การตัดไฟ・OS พัง"]
read_ahead["การอ่านล่วงหน้า (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["ข้อกำหนดจัดแนวเซกเตอร์"]
invalid_parameter_error["ERROR_INVALID_PARAMETER (87)"]
frequent_durable_write["ความทนทานที่ชัวร์กับการเขียนบ่อย"]
flushviewoffile["FlushViewOfFile"]
fast_io["Fast I/O"]
procmon["Process Monitor (procmon.exe)"]
irp["IRP (แพ็กเก็ตคำขอ I/O)"]
synchronous_io["I/O แบบซิงค์"]
cache_manager -->|"อิมพลีเมนต์"| write_behind_caching
write_behind_caching -->|"ใช้"| memory_mapped_file
cache_manager -->|"ใช้"| system_cache_view
system_cache_view -->|"ใช้"| memory_mapped_file
cache_manager -->|"ใช้"| file_object
cache_manager -->|"ใช้"| lazy_writer
lazy_writer -->|"ทำอัตโนมัติ"| write_behind_caching
lazy_writer -->|"บรรเทา"| unflushed_write_loss
temp_file_attribute -.->|"ใช้ร่วมไม่ได้"| lazy_writer
write_behind_caching -->|"อาจก่อให้เกิด"| dirty_page
dirty_page -->|"ถูกเก็บใน"| system_cache_view
power_loss -.->|"อาจก่อให้เกิด"| unflushed_write_loss
cache_manager -.->|"ป้องกัน"| unflushed_write_loss
cache_manager -->|"ใช้"| read_ahead
read_ahead -.->|"กำหนดค่าด้วย"| sequential_scan_hint
read_ahead -.->|"กำหนดค่าด้วย"| random_access_hint
flushfilebuffers -->|"ป้องกัน"| unflushed_write_loss
write_through -->|"ป้องกัน"| unflushed_write_loss
no_buffering -.->|"บรรเทา"| unflushed_write_loss
no_buffering -->|"ต้องมี"| sector_alignment_requirement
sector_alignment_requirement -->|"ป้องกัน"| invalid_parameter_error
no_buffering -->|"อาจก่อให้เกิด"| invalid_parameter_error
flushfilebuffers -->|"ไม่แนะนำให้ใช้กับ"| frequent_durable_write
no_buffering -->|"แนวทางที่แนะนำสำหรับ"| frequent_durable_write
write_through -->|"แนวทางที่แนะนำสำหรับ"| frequent_durable_write
no_buffering -->|"ใช้ร่วมไม่ได้"| system_cache_view
flushviewoffile -->|"ต้องมี"| memory_mapped_file
flushviewoffile -->|"ควรทำก่อน"| flushfilebuffers
fast_io -->|"ตรวจยืนยันด้วย"| procmon
fast_io -->|"ต้องมี"| system_cache_view
fast_io -->|"ใช้ร่วมไม่ได้"| irp
fast_io -->|"ต้องมี"| synchronous_io
ในแผนภาพ เส้นทึบหมายถึงความสัมพันธ์ที่ถือเสมอ และเส้นประหมายถึงความสัมพันธ์ที่มีเงื่อนไข (เงื่อนไขอยู่ในการอธิบายของแต่ละความสัมพันธ์ในหน้าละเอียด) รายการความสัมพันธ์ทั้งหมด (รวม 32 รายการ พร้อมหลักฐานและระดับความเชื่อมั่น) และนิยามของแนวคิดหลักรวบรวมไว้ที่ หน้าละเอียดของแผนที่ความรู้ (เป็นภาษาญี่ปุ่น) ข้อมูล: JSON-LD / Turtle
2. ตัวจริงของแคช — ไฟล์ถูกแมปเข้าหน่วยความจำ
2.1. ช่อง 256KB กับการคัดลอกหน่วยความจำ
ถ้าคิดแคชไฟล์ของ Windows เป็น «ภาชนะบล็อกดิสก์» พฤติกรรมหลายอย่างอธิบายไม่ได้ ภาพที่ถูกคือดังนี้ ── Cache Manager แมปช่วงหน่วย 256KB ของไฟล์เข้า «ช่อง» ในพื้นที่ที่อยู่ระบบ และการอ่านเขียนที่แคชเปิดถูกทำเป็นการคัดลอกหน่วยความจำระหว่างช่องนั้นกับบัฟเฟอร์ของแอป1
flowchart TB
subgraph U["แอป (โหมดผู้ใช้)"]
BUF["บัฟเฟอร์ของแอป<br/>(พื้นที่ที่ส่งให้ ReadFile/WriteFile)"]
end
subgraph S["พื้นที่ที่อยู่ระบบ"]
SLOT["แคชไฟล์ระบบ<br/>ช่องที่แมปช่วง 256KB ของไฟล์"]
end
DISK[("ไฟล์บนดิสก์")]
BUF <-->|"ReadFile/WriteFile =<br/>การคัดลอกหน่วยความจำกับช่อง"| SLOT
SLOT <-->|"การอ่านตอนเข้าถึงครั้งแรกกับ<br/>การเขียนกลับทีหลังเป็นหน่วยเพจ"| DISK
ภาพ 1: ภาพจริงของ I/O ที่แคชเปิด «การอ่านเขียนไฟล์» จากมุมแอป ในหลายกรณีเป็นการคัดลอกหน่วยความจำอย่างเดียว
จุดที่เข้าใจผิดง่ายมีหนึ่ง 256KB คือความละเอียดของวิว (แมป) ไม่ได้แปลว่า I/O ดิสก์ทำทีละ 256KB เสมอ เพจในช่องถูกอ่านตามต้องการ และปริมาณ I/O ที่บินไปดิสก์จริงเปลี่ยนตามขนาดคำขอและรูปแบบการเข้าถึง ช่วงที่อ่านครั้งแรก I/O ดิสก์เกิดเพื่อเติม (ตรงนี้ IRP ของตอนที่ 1 บินไปสแตกที่เก็บด้านล่าง) ถ้าอยู่บนแคชแล้ว การอ่านจบด้วยการคัดลอกอย่างเดียว «ถ้าแคชฮิต แม้ส่งอะซิงค์ก็จบแบบซิงค์» ที่เห็นใน ตอนที่ 2 หมวด 5 คือการปรากฏของ «คำขอที่ตอบได้ทันทีทำให้จบตรงนั้น» ในทางกลับกัน ถ้าแคชเปิดแต่เพจไม่อยู่ในหน่วยความจำ ไม่มีกลไกอะซิงค์ในการจัดการเพจฟอลต์ การอ่านอะซิงค์อาจถูกประมวลผลแบบซิงค์ ── กับดักที่เห็นในตอนที่ 2 เช่นกัน7
2.2. ตัวจริงของ «หน่วยความจำว่างลด»
ว่าจะใช้แคชหรือไม่ และสถานะการอ่านล่วงหน้า ถูกจัดการต่อการเปิด (หน่วยไฟล์อ็อบเจ็กต์)1 แต่ ตัวข้อมูลที่ถูกแคชถูกแชร์ต่อหน่วยไฟล์ (สตรีม) แม้เปิดไฟล์เดียวกันหลายครั้ง ไม่ได้เกิดแคชแยก และการเปิดจากแฮนเดิลใดก็เห็นเนื้อแคชเดียวกัน (ฐานความสอดคล้องของหมวด 6) แคชทำงานภายใต้การบัญชาของ Cache Manager ตลอดที่ Windows ทำงาน1 ถ้าคัดลอกไฟล์ใหญ่หรืออ่านเขียนจำนวนมาก หน่วยความจำกายภาพที่ว่างถูกนำไปใช้เป็นแคชเรื่อย ๆ แม้หน่วยความจำว่างใน Task Manager ดูลด ส่วนใหญ่คือ «หน่วยความจำที่รออยู่ซึ่งถูกใช้มีค่า และแอปขอแล้วสละให้ค่อนข้างเร็ว» เพื่อไม่ให้แยกผิดตอนวินิจฉัยหน่วยความจำไม่พอ งานสังเกตอยู่ใน «แยกการรอ GC กับหน่วยความจำรั่วใน .NET» ด้วย
การเคลื่อนนี้ยืนยันบนจอได้ เปิด Task Manager > ประสิทธิภาพ > หน่วยความจำ แถบ «องค์ประกอบหน่วยความจำ» ด้านล่างถูกแบ่งเป็น กำลังใช้ / ถูกเปลี่ยน / สแตนด์บาย / ว่าง แคชไฟล์ส่วนใหญ่เข้า สแตนด์บาย นี้ และในรายการขวาถูกรวมเป็น «ถูกแคช» ถ้าจะดูละเอียดกว่า แท็บ ตัวตรวจสอบทรัพยากร > หน่วยความจำ เรียงช่วงเดียวกันพร้อมความจุ คัดลอกไฟล์หลาย GB หนึ่งไฟล์แล้วดูใหม่ สแตนด์บายเพิ่ม ว่างลด แต่ «กำลังใช้» แทบไม่เปลี่ยน ── กล่าวคือ ไม่ใช่ «หน่วยความจำถูกกิน» แต่เป็น «หน่วยความจำที่ว่างถูกใช้เป็นแคช» ยืนยันด้วยตาได้
3. การอ่านล่วงหน้า — การเก็งกำไรของการอ่าน
Cache Manager อ่านช่วงที่น่าจะถูกอ่านต่อไปล่วงหน้า จากรูปแบบการเข้าถึงในอดีต (read-ahead) ถ้าเป็นไฟล์ที่อ่านตามลำดับ ข้อมูลต่อเนื่องอยู่บนแคชแล้วก่อนแอปขอ ── นี่คือเคล็ดความเร็วของการอ่านตามลำดับ ปริมาณการอ่านล่วงหน้าไม่คงที่ เปลี่ยนตามรูปแบบที่ตรวจพบและขนาดคำขอ
flowchart LR
A["ประวัติคำขออ่านของแอป<br/>อ่านจากต้นตามลำดับ"]
D{"Cache Manager<br/>ตรวจพบรูปแบบ"}
R["อ่านล่วงหน้า: อ่านช่วงต่อเนื่อง<br/>ไว้ก่อนถูกขอ<br/>(ปริมาณแปรตามรูปแบบกับขนาดคำขอ)"]
H1["คำใบ้ FILE_FLAG_SEQUENTIAL_SCAN<br/>= อ่านล่วงหน้าอย่างกระตือรือร้น"]
H2["คำใบ้ FILE_FLAG_RANDOM_ACCESS<br/>= อ่านล่วงหน้าสูญเปล่าจึงกด"]
A --> D
D --> R
H1 -.-> D
H2 -.-> D
ภาพ 2: การอ่านล่วงหน้า นอกจากการตรวจรูปแบบการเข้าถึง ยังให้คำใบ้ด้วยแฟลกของ CreateFile ได้
FileOptions.SequentialScan / RandomAccess ที่ใส่ในตารางเทียบของตอนที่ 1 คือคำใบ้สู่เอนจินอ่านล่วงหน้านี้ การประมวลผลแบตช์ที่ «ไล่ทั้งหมด» ใช้ตัวแรก การเข้าถึงที่เดินดัชนีใช้ตัวหลัง ── คิดว่าเป็น แฟลกที่บอกอนาคตที่แอปเท่านั้นรู้ ให้ OS จุดใช้จะชัด
4. การเขียนแบบหน่วง — ความหมายของ «สำเร็จ» ของ WriteFile
4.1. lazy writer มาทุกวินาที
ฝั่งเขียนคือ แคชไรต์แบ็ก WriteFile คืนความสำเร็จเมื่อคัดลอกข้อมูลเข้าช่องแล้ว และการสะท้อนไปดิสก์ถูกเลื่อน นโยบาย «เขียนช้ากว่า» นี้คือ การเขียนแบบหน่วง (lazy writing)1
ผู้รับผิดชอบการสะท้อนคือ lazy writer ที่ Cache Manager ปลุก ทุกวินาที กอง หนึ่งในแปด ของเพจที่ยังไม่ถูกฟลัชเร็ว ๆ นี้เข้าคิวแล้วเขียนออก ถ้ามีข้อมูลที่ต้องเขียนมากก็กองเพิ่ม อนึ่ง ไฟล์ชั่วคราวที่สร้างด้วยแอตทริบิวต์ FILE_ATTRIBUTE_TEMPORARY ถูกตัดออกจากเป้าฟลัชของ lazy writer ── เพราะเขียนสิ่งที่สมมติว่าจะถูกลบทันทีก็เปล่า1 อย่างไรก็ตาม นี่เป็น คำใบ้ จากแอตทริบิวต์ ถ้าหน่วยความจำตึงอาจถูกเขียนกลับ และไฟล์ที่ «ชื่อดูชั่วคราวอย่างเดียว» ไม่ถูกใช้
sequenceDiagram
participant App as แอป
participant C as แคชระบบ
participant LW as lazy writer (ปลุกทุกวินาที)
participant D as ดิสก์
App->>C: WriteFile (ข้อมูล)
Note over C: คัดลอกเข้าช่อง<br/>ทำเพจเป็นสกปรก (ยังไม่เขียน)
C-->>App: คืน TRUE ทันที
Note over App,C: จากตรงนี้ถึงเขียนกลับคือ 「หน้าต่างอันตราย」<br/>ถ้าตัดไฟ・OS พัง ข้อมูลนี้หาย
LW->>C: เลือก 1/8 ของเพจสกปรก
LW->>D: เขียนกลับรวม
Note over D: ตรงนี้ถูกทำให้ทนทานครั้งแรก
ภาพ 3: การเขียนแบบหน่วง ความสำเร็จของ WriteFile คือ «ส่งมอบให้ OS» ไม่ใช่ «ถูกทำให้ทนทานแล้ว»
4.2. อะไรเกิดแล้ว อะไรหายแค่ไหน
ทำให้ความหมายของ «หน้าต่างอันตราย» ถูก ความล้มเหลวชนิดต่าง ชะตาต่าง
flowchart TB
W["ข้อมูลทันทีหลัง WriteFile สำเร็จ<br/>(เพจสกปรกบนแคช)"]
Q{"อะไรเกิด"}
A1["โพรเซสของแอป<br/>พัง/ถูกบังคับจบ"]
A2["OS ทั้งก้อนหยุด<br/>(ตัดไฟ・หน้าจอสีน้ำเงิน)"]
S["ข้อมูลเหลือ<br/>แคชเป็นของ OS<br/>lazy writer เขียนกลับตามกำหนด"]
L["เพจสกปรกหาย<br/>เหลือแค่ส่วนที่ถึงดิสก์แล้ว"]
W --> Q
Q --> A1
Q --> A2
A1 --> S
A2 --> L
ภาพ 4: ชนิดของความล้มเหลวกับจุดแยกของการรอด แคชไม่ใช่ «ของโพรเซส» แต่เป็น «ของ OS»
- แอปตาย ข้อมูลไม่หาย เมื่อคัดลอกเข้าแคชเสร็จ เจ้าของข้อมูลคือ OS «บันทึกแล้วแอปพังแต่ไฟล์รอด» ได้เพราะอันนี้
- OS ทั้งก้อนตาย ส่วนสกปรกหาย ความถี่ของฟลัชถูกปรับเป็นการแลกสมรรถนะกับความน่าเชื่อถือ และเอกสารก็ระบุชัดว่า «ถ้าเกิดความล้มเหลวระบบฉับพลันอย่างการสูญเสียไฟ ข้อมูลแคชที่ยังไม่ถูกเขียนจะหาย»1
กล่าวคือ คำถามในการออกแบบแอปธุรกิจคือ «ข้อมูลนี้ ณ วินาทีตัดไฟ หายได้ไหม» ล็อกไม่กี่วินาทีอาจยอมได้ เรกคอร์ดยืนยันข้อมูลสั่งซื้อคงยอมไม่ได้ สิ่งที่ยอมไม่ได้เท่านั้น ใช้เครื่องมือหมวดถัดไป
5. กล่องเครื่องมือที่ทำให้ «เขียนชัวร์»
5.1. FlushFileBuffers — เขียนให้จบตอนนี้
FlushFileBuffers เขียนข้อมูลในบัฟเฟอร์ของไฟล์ที่ระบุไปอุปกรณ์จนจบ เมทาดาตาของระบบไฟล์ถูกแคชเสมอ ดังนั้นการส่งเมทาดาตาให้ถึงชัวร์ต้องฟลัช (หรือ WRITE_THROUGH) ก็เป็นจุดที่ต้องกด12 ใน .NET FileStream.Flush(true) เทียบเท่า (Flush() อย่างเดียวแค่ส่งบัฟเฟอร์ภายใน .NET ให้ OS แคชของ OS ยังอยู่)8
อย่างไรก็ตาม เอกสารทางการตอกชัด ── เรียกทุกครั้งที่เขียนไม่มีประสิทธิภาพ ถ้าต้องทำให้ทนทานทุกครั้งในการเขียนจำนวนมาก ควรใช้ NO_BUFFERING+WRITE_THROUGH ที่กล่าวต่อไป2
5.2. FILE_FLAG_WRITE_THROUGH — ตัดแค่ความหน่วง
เปิดด้วย FILE_FLAG_WRITE_THROUGH การเขียน ถูกเขียนเข้าแคชด้วย และถูกเขียนลงดิสก์ทันทีโดยไม่รอ lazy writer1 จุดคือการอ่านยังได้คุณของแคช เป็นคำตอบตรงต่อ «ให้อ่านเร็วเหมือนเดิม ตัดแค่ความหน่วงของการเขียน»
5.3. FILE_FLAG_NO_BUFFERING — ไม่ผ่านแคช
FILE_FLAG_NO_BUFFERING ถอดแคชระบบเอง ออกจากการอ่านเขียน การอ่านเขียนทั้งหมดกลายเป็น I/O ไปอุปกรณ์ดิสก์ทุกครั้งโดยไม่ผ่านแคช1 อย่างไรก็ตาม ที่เลี่ยงได้คือ ถึงแคชระบบของ Windows และตามภาพ 5 แคชเขียนในอุปกรณ์เป็นขั้นอีกขั้น ถ้าต้องการทนทานถึงตัดไฟ การใช้ WRITE_THROUGH คู่หรือ FlushFileBuffers ยังจำเป็น เป็นเครื่องมือสำหรับการส่งข้อมูลจำนวนมากครั้งเดียว หรือเอนจินฐานข้อมูลที่จัดการบัฟเฟอร์เอง แต่มาพร้อมสัญญาเข้ม3
- ขนาดและออฟเซ็ตไฟล์ของการอ่านเขียนต้องเป็นจำนวนเท่าของขนาดเซกเตอร์ของวอลุ่ม (เซกเตอร์ 512 ไบต์ คือ 512・1024・1536…)
- ที่อยู่ของบัฟเฟอร์ก็ต้องจัดแนวตามขนาดเซกเตอร์กายภาพ (ต้องคำนึงดิสก์ «Advanced Format» เซกเตอร์กายภาพ 4096 ไบต์ด้วย)
- แม้กระนั้น เมทาดาตายังถูกแคชต่อ ดังนั้นความทนทานเต็มที่ต้องใช้ WRITE_THROUGH คู่หรือ
FlushFileBuffers12
«สัญญา» นี้คือจุดที่คนที่คิดแค่เติมแฟลกแล้วจบ ล้มครั้งแรก ถ้าอ่านเขียนโดยไม่รักษาการจัดแนว จะล้มด้วย ERROR_INVALID_PARAMETER (87) สามจุดที่ต้องรักษา มีดังนี้3
| สิ่งที่จัดให้ตรง | เงื่อนไข | ทำให้ตรงอย่างไร |
|---|---|---|
| ขนาดการอ่านเขียน | จำนวนเท่าของขนาดเซกเตอร์ของวอลุ่ม | ดึง lpBytesPerSector ของ GetDiskFreeSpace แล้วปัดเป็นเท่าของนั้น |
| ออฟเซ็ตไฟล์ | เช่นกัน (รวมเมื่อระบุด้วย Offset ของ OVERLAPPED) |
เดินทีละเท่าของขนาดเซกเตอร์ |
| ที่อยู่ของบัฟเฟอร์ | จัดแนวตามขนาดเซกเตอร์กายภาพ | จองด้วย VirtualAlloc (คืนพื้นที่จัดแนวตามขอบเพจ = โดยปกติ 4096 ไบต์) |
ข้อที่สามถูกมองข้ามเป็นพิเศษ ที่อยู่ที่ malloc หรือ new หรืออาร์เรย์ของ C# คืน ไม่มีการรับประกันการจัดแนวตามขอบเซกเตอร์ ถ้าใช้ VirtualAlloc ที่จองตามขอบเพจได้ ข้อกำหนดดิสก์ «Advanced Format» เซกเตอร์กายภาพ 4096 ไบต์ก็ถูกพร้อมกัน รูปเล็กสุดเป็นดังนี้
// C++ / Win32 การจัดการข้อผิดพลาดทำไว้ขั้นต่ำ
DWORD sectorsPerCluster = 0, bytesPerSector = 0, freeClusters = 0, totalClusters = 0;
if (!GetDiskFreeSpaceW(L"C:\\", §orsPerCluster, &bytesPerSector,
&freeClusters, &totalClusters))
{
return GetLastError();
}
// ทำให้หน่วยอ่านเขียนเป็นจำนวนเท่าของขนาดเซกเตอร์(ที่นี่เทียบเท่า 1MiB)
const DWORD chunk = (1024 * 1024 / bytesPerSector) * bytesPerSector;
// บัฟเฟอร์จองพื้นที่ที่จัดแนวตามขอบเพจ(malloc/new ไม่มีการรับประกัน)
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 คืนผลของ «การเรียก Win32 ก่อนหน้า» ถ้าเรียก VirtualFree
// ก่อน เหตุที่ CreateFileW ล้ม(ถูกปฏิเสธการเข้าถึง・ไม่มีพาธ ฯลฯ)
// ถูกเขียนทับด้วยผลของการเก็บกวาด คืนแค่รหัสที่หาสาเหตุไม่ได้
const DWORD err = GetLastError();
VirtualFree(buffer, 0, MEM_RELEASE);
return err;
}
// เดินทีละ chunk ไบต์ทุกครั้ง ดังนั้นขนาดและออฟเซ็ตคงการจัดแนว
DWORD read = 0;
while (ReadFile(h, buffer, chunk, &read, nullptr) && read > 0)
{
// ประมวลผล read ไบต์ต้นของ buffer
// (ที่ท้ายไฟล์ read < chunk นี่ปกติ)
}
CloseHandle(h);
VirtualFree(buffer, 0, MEM_RELEASE);
อนึ่ง FileOptions ของ .NET ไม่มีค่าที่ตรง FILE_FLAG_NO_BUFFERING ถ้าจำเป็นจริงต้องเรียก CreateFile โดยตรง แต่กรณีนั้นก็ต้องรักษาข้อกำหนดจัดแนวข้างบนเอง พิจารณาลำดับเป็น «NO_BUFFERING เพราะจัดการบัฟเฟอร์เอง» ไม่ใช่ «NO_BUFFERING เพราะอยากเร็ว»
5.4. จัดเมื่อใช้อะไร
flowchart TB
A["บัฟเฟอร์ของแอป"]
B["แคชไฟล์ระบบ<br/>(เพจสกปรก)"]
C["แคชในอุปกรณ์ดิสก์"]
D[("สื่อบันทึกที่ไม่ลบเมื่อดับไฟ")]
A -->|"WriteFile ค่าเริ่มต้น: สำเร็จเมื่อถึงตรงนี้"| B
B -->|"lazy writer (ทุกวินาที) / WRITE_THROUGH (ทันที)"| C
C -->|"จังหวะของอุปกรณ์ /<br/>FlushFileBuffers ขอให้เขียนจนจบ"| D
A -.->|"NO_BUFFERING ข้ามแคชตรงไป"| C
ภาพ 5: ชั้นของข้อมูล และแต่ละเครื่องมือดันไปถึงไหน อย่าลืมขั้นสุดท้าย «แคชในอุปกรณ์ดิสก์»
| วิธี | อะไรเกิด | เหมาะกับสถานการณ์ |
|---|---|---|
| ค่าเริ่มต้น (แคชเปิด) | จบด้วยการคัดลอกแคช การสะท้อนเป็น lazy writer | ไฟล์ I/O ส่วนใหญ่ |
FlushFileBuffers / Flush(true) |
เขียนข้อมูล ณ จุดนั้น + เมทาดาตาจนจบ | การยืนยันที่จุดตัด (คอมมิตธุรกรรม ฯลฯ) |
FILE_FLAG_WRITE_THROUGH |
ทุกครั้งที่เขียนตรงไปดิสก์ (อ่านยังใช้แคช) | ล็อก・วารสารที่เขียนต่อเนื่องแล้วเสียไม่ได้ |
FILE_FLAG_NO_BUFFERING (+WRITE_THROUGH) |
ไม่ผ่านแคช มีข้อกำหนดจัดแนว | จัดการบัฟเฟอร์เอง・I/O ก้อนใหญ่ |
ลำดับเลือกคือสองขั้น ตัดสินก่อนว่า «ตัดไฟแล้วหายได้กี่รายการ» แล้วตรวจว่า «เพื่ออันนั้นช้าได้แค่ไหน» อย่ากวาดตารางจากบนลงล่าง ให้เดินกิ่งนี้
flowchart TB
S["กำลังจะเขียนข้อมูลนี้"]
Q1{"ณ วินาทีตัดไฟ・หน้าจอสีน้ำเงิน<br/>หายแล้วยอมได้หรือ"}
A0["ค่าเริ่มต้น (แคชเปิด)<br/>เร็วที่สุด I/O ส่วนใหญ่อยู่ที่นี่"]
Q2{"สิ่งที่เสียไม่ได้คือ<br/>「จุดตัด」หรือ「ทุกรายการ」"}
A1["FlushFileBuffers ที่จุดตัด<br/>.NET คือ Flush(true)<br/>ต้นทุน: แค่การรอที่จุดตัด"]
Q3{"จัดการบัฟเฟอร์เองและ<br/>ตรงข้อกำหนดจัดแนวของ 5.3 ได้หรือ"}
A2["FILE_FLAG_WRITE_THROUGH<br/>ทุกครั้งที่เขียนตรงไปดิสก์<br/>อ่านยังเร็วผ่านแคช"]
A3["FILE_FLAG_NO_BUFFERING<br/>+ FILE_FLAG_WRITE_THROUGH<br/>รูป 「ความทนทานบ่อย」 ที่เอกสารทางการยก"]
S --> Q1
Q1 -->|"ยอมได้<br/>(ล็อกไม่กี่วินาทีล่าสุด ฯลฯ)"| A0
Q1 -->|"ยอมไม่ได้"| Q2
Q2 -->|"จุดตัด<br/>(ยืนยันธุรกรรม ฯลฯ)"| A1
Q2 -->|"ทุกรายการ"| Q3
Q3 -->|"ไม่ (แอปปกติ)"| A2
Q3 -->|"ใช่ (เอนจินฐานข้อมูล ฯลฯ)"| A3
ภาพ 6: วิธีเลือกเครื่องมือ กิ่งขั้นแรกคือความต้องการความน่าเชื่อถือ ขั้นสองคือต้นทุนที่ยอมจ่าย ไม่มีทาง 「FlushFileBuffers ทุกรายการ」 เพราะตามที่เห็นใน 5.1 เอกสารทางการจัดว่าไม่มีประสิทธิภาพ
รูปแบบในงานจริงยกแค่สามอย่าง
- «เขียนไฟล์ชั่วคราว→ฟลัช→เปลี่ยนชื่อ» คือหลักที่ไม่ทิ้งไฟล์พังครึ่ง เขียนเนื้อในจนจบแล้วยืนยันด้วยชื่อ ── การส่งมอบแบบอะตอมนี้จัดการละเอียดใน «ความรู้พื้นฐานของการควบคุมเฉพาะกิจสำหรับการเชื่อมไฟล์»
- ปล่อยให้ฐานข้อมูลทำก็เป็นการออกแบบที่ดี เรื่องที่ SQLite สร้างความทนทานด้วย WAL กับฟลัช ดู «ใช้ SQLite ในแอปธุรกิจจาก C#» ทางเลือก «ไม่เขียนกลยุทธ์ฟลัชเอง» มีเสมอ
- เกณฑ์วัดให้สงสัยแคช การวัดที่ «อ่านเร็วเกินไป» มักวัดแคชฮิตตั้งแต่รอบสองเป็นต้นไป ธรรมเนียมการวัดอยู่ใน «วิธีเทียบความเร็วโปรแกรมตามรุ่นบน Windows ให้ถูก»
อนึ่ง ขั้นสุดท้ายของภาพ 5 ── แคชในอุปกรณ์ดิสก์ ก็อย่าลืม FlushFileBuffers ขอให้เขียนจนจบรวมถึงตรงนั้น แต่ USB และดิสก์ภายนอก นโยบายแคชเขียนฝั่งอุปกรณ์ («ถอดเร็ว» กับ «สมรรถนะสูง») เข้ามาเกี่ยว การจัดการอุปกรณ์ถอดได้ดู «วิธีจัดการอุปกรณ์ USB จากแอป Windows» ด้วย
6. ความสอดคล้องกับไฟล์แมปหน่วยความจำ
ในตอนที่ 1 เมื่อได้ยินว่า «ตัวจริงของแคชคือการแมปไฟล์» คงมีคนคิด ── งั้นวิวที่ตน MapViewOfFile เอง กับแคชของ ReadFile/WriteFile จะชนกันหรือ?
ไม่ชน เพราะอยู่บนกลไกเดียวกัน อ็อบเจ็กต์แมปไฟล์ถูกหนุนด้วยไฟล์ และการไล่เพจออกถูกทำเป็นการเขียนกลับไฟล์ แม้หลายโพรเซสสร้างวิวต่อไฟล์ท้องถิ่นเดียวกัน เนื้อหาที่เห็นสอดคล้อง4
flowchart TB
subgraph P1["พื้นที่ที่อยู่ของโพรเซส A"]
V1["วิวของ MapViewOfFile"]
end
subgraph SYS["พื้นที่ที่อยู่ระบบ"]
SC["วิวของ Cache Manager<br/>(ช่องที่ ReadFile/WriteFile ใช้)"]
end
PAGES["กลุ่มเพจกายภาพเดียวกัน<br/>(หน่วยความจำที่หนุนด้วยไฟล์)"]
DISK[("ไฟล์บนดิสก์")]
V1 --> PAGES
SC --> PAGES
PAGES --> DISK
NB["I/O ของ FILE_FLAG_NO_BUFFERING<br/>อยู่นอกกรอบการแชร์นี้ (ตรงไปดิสก์)"]
NB -.-> DISK
ภาพ 7: ทั้งวิวแมปและแคชมอง «เพจที่หนุนด้วยไฟล์» เดียวกัน สิ่งที่อยู่นอกกรอบมีแค่ NO_BUFFERING
จุดควรระวังมีสอง
- I/O ของ
FILE_FLAG_NO_BUFFERINGอยู่นอกกรอบความสอดคล้องนี้ การอ่านเขียนที่ไม่ผ่านแคช ไม่ถูกจับคู่กับเนื้อหาผ่านวิวแมป/แคช ถ้าผสม ต้องรักษาความสอดคล้องเอง - การทำให้วิวแมปทนทานเป็นสองขั้น
FlushViewOfFileเริ่มเขียนเพจสกปรกในช่วงออก แต่ ไม่เขียนเมทาดาตา และไม่รอการเขียนกายภาพจากแคชของอุปกรณ์ดิสก์ เพื่อส่งให้ถึงชัวร์ เรียกFlushFileBuffersหลังFlushViewOfFile5
งานปฏิบัติของการแมปไฟล์ในฐานะหน่วยความจำร่วม (การแชร์มีชื่อ การซิงค์ รูปแบบอุบัติเหตุ) อยู่ใน «กับดักหน่วยความจำร่วมและแนวปฏิบัติที่ดีที่สุด»
7. Fast I/O — เก็บการบ้านของตอนที่ 1
สองบรรทัดก่อน Fast I/O คือ ทางลัดที่จัดไว้สำหรับการอ่านเขียนซิงค์ต่อไฟล์ที่อยู่บนแคช แลกข้อมูลตรงกับแคชโดยไม่ประกอบ IRP (I/O Request Packet ภาชนะคำขอที่เคอร์เนลส่งให้ไดรเวอร์) เหตุที่คอลัมน์ Operation ของ Procmon มี IRP_MJ_READ กับ FASTIO_READ ปน คือความต่างที่ «การอ่าน» เดียวกันเดินทางปกติหรือทางลัด
ในตอนที่ 1 หมวด 5.2 เขียนว่า «ไม่ใช่ I/O ทั้งหมดกลายเป็น IRP» นี่คือการเฉลย
การอ่านเขียนไฟล์ที่อยู่บนแคช รู้แล้วว่า จบด้วยการคัดลอกหน่วยความจำกับแคช โดยไม่ต้องประกอบ IRP แล้วไหลลงสแตกอุปกรณ์ ดังนั้น Windows จึงจัดทางลัด Fast I/O สำหรับ I/O ซิงค์ต่อไฟล์ที่ถูกแคช ── ไม่สร้าง IRP เรียก «จุดเข้า Fast I/O» ของระบบไฟล์โดยตรง และคัดลอกตรงจาก Cache Manager6 เมื่อ Fast I/O ประมวลผลไม่ได้ (ไม่อยู่บนแคช มีล็อกเกี่ยว ฟิลเตอร์แทรก ฯลฯ) จะพับกลับเส้นทาง IRP ปกติ อนึ่ง นี่คือ เส้นทางเร็วสำหรับคำขอซิงค์ ไม่ใช่ «แคชฮิต = Fast I/O เสมอ» การดำเนินการของแฮนเดิลอะซิงค์ (FILE_FLAG_OVERLAPPED) แม้จบตรงนั้นจากแคช (ตอนที่ 2 หมวด 5) ก็อาจถูกประมวลผลบนเส้นทาง IRP
flowchart TB
REQ["การอ่านเขียนซิงค์ต่อแฮนเดิลที่แคชเปิด"]
Q{"ประมวลผลด้วย Fast I/O ได้หรือ<br/>(อยู่บนแคช ฯลฯ)"}
FAST["Fast I/O<br/>ไม่สร้าง IRP คัดลอกตรงกับแคช<br/>ใน Procmon แสดงเป็น FASTIO_"]
IRP["เส้นทางปกติ<br/>ประกอบ IRP แล้วไปสแตกอุปกรณ์<br/>(โลกของภาพ 6 ในตอนที่ 1)"]
REQ --> Q
Q -->|ได้| FAST
Q -->|ไม่ได้| IRP
ภาพ 8: กิ่งของ Fast I/O เหตุที่ใน Procmon เห็น FASTIO_READ กับ IRP_MJ_READ ปน คืออันนี้
เหตุที่ในการสังเกต Procmon ของ ตอนที่ 1 หมวด 7 มีแถว FASTIO_ ปน อธิบายได้แล้ว การอ่านที่แคชฮิตของแบบซิงค์ แม้ IRP ก็เป็นของฟุ่มเฟือย การมีเส้นทางนี้มีผลต่อไดรเวอร์ฟิลเตอร์ที่จัดการในตอนที่ 6 ด้วย (มินิฟิลเตอร์ถูกทำให้แทรก Fast I/O ได้)
8. สรุป
- แคชไฟล์ของ Windows เป็น ไรต์แบ็ก และตัวจริงคือ การแมปช่วง 256KB ของไฟล์ การอ่านเขียนที่แคชเปิดเป็นการคัดลอกหน่วยความจำกับช่อง1
- อ่านถูก การอ่านล่วงหน้า เก็ง และ
SequentialScan/RandomAccessคือคำใบ้นั้น1 - เขียนถูก lazy writer ทุกวินาที ตาม แอปตายข้อมูลเหลือ OS ทั้งก้อนตาย ส่วนสกปรกหาย คำถามของการออกแบบคือ «ข้อมูลนี้ ณ วินาทีตัดไฟ หายได้ไหม»1
- เครื่องมือเขียนชัวร์คือ
FlushFileBuffers(ยืนยันที่จุดตัด) /WRITE_THROUGH(ทุกครั้งที่เขียน) /NO_BUFFERING(ไม่ผ่านแคช + ข้อกำหนดจัดแนว) ฟลัชทุกครั้งไม่มีประสิทธิภาพ และความทนทานบ่อย เอกสารทางการแนะนำใช้ NO_BUFFERING+WRITE_THROUGH คู่ จุดที่เมทาดาตาถูกแคชเสมอก็ต้องระวัง231 - วิวแมปกับแคชแชร์เพจเดียวกัน และสอดคล้อง นอกกรอบมีแค่ NO_BUFFERING การทำให้แมปทนทานเป็นสองขั้น
FlushViewOfFile+FlushFileBuffers45 - การอ่านเขียน ซิงค์ ที่แคชฮิตถูก Fast I/O ข้ามแม้ IRP นี่คือตัวจริงของ
FASTIO_ที่เห็นใน Procmon ของตอนที่ 16
ต่อคือตอนที่ 5 «โครงสร้างภายในของ NTFS ── เข้าใจระบบไฟล์จาก MFT» ถึงตอนนี้จัดการไฟล์เป็น «ออฟเซ็ตกับลำดับไบต์» แต่ด้านหลัง NTFS วางข้อมูลอย่างไร ── MFT สตรีมข้อมูลหลายอัน วารสาร ฮาร์ดลิงก์ ── จะลงสู่โครงสร้างสถิตบนดิสก์
บทความที่เกี่ยวข้อง
- เชิงลึกของ Windows I/O (ตอนที่ 1) — การอ่านเขียนทั้งหมดกลายเป็น IRP: ภาพรวมของระบบ I/O
- เชิงลึกของ Windows I/O (ตอนที่ 2) — I/O ซิงค์กับอะซิงค์: ความหมายจริงของ OVERLAPPED
- เชิงลึกของ Windows I/O (ตอนที่ 3) — พอร์ตจบ I/O (IOCP) กับเธรดพูล .NET: ชั้นใต้ async/await
- ความรู้พื้นฐานของการควบคุมเฉพาะกิจสำหรับการเชื่อมไฟล์ — แนวปฏิบัติที่ดีที่สุดของล็อกไฟล์กับการ claim แบบอะตอม
- กับดักหน่วยความจำร่วมและแนวปฏิบัติที่ดีที่สุด
- ใช้ SQLite ในแอปธุรกิจจาก C# — โหมด WAL การควบคุมเฉพาะกิจ มาตรการพัง และการใช้แยกกับ EF Core
- วิธีเทียบความเร็วโปรแกรมตามรุ่นบน Windows ให้ถูก
- วิธีจัดการอุปกรณ์ USB จากแอป Windows — การเลือก COM เสมือน・HID・WinUSB・SDK เฉพาะ
ด้านให้คำปรึกษาที่เกี่ยวข้อง
KomuraSoft LLC รับงานออกแบบและสอบสวนบั๊กของไฟล์ I/O ในแอปธุรกิจ Windows เช่น «ข้อมูลที่ควรบันทึกแล้วหาย» «การเขียนไฟล์ช้า/เร็วจนน่าสงสัย»
- การพัฒนาแอปพลิเคชัน Windows
- การตรวจสอบบั๊กและวิเคราะห์หาสาเหตุ
- การใช้ประโยชน์และโยกย้ายสินทรัพย์ซอฟต์แวร์เดิม
- ติดต่อเรา
ลิงก์อ้างอิง
-
Microsoft Learn, File Caching. ว่าด้วย Windows ที่แคชข้อมูลไฟล์ตามค่าเริ่มต้น การอ่านที่ทำจากแคชไฟล์ระบบ และการเขียนที่เข้าแคชด้วยในแคชไรต์แบ็ก แคชที่ถูกจัดการต่อหน่วยไฟล์อ็อบเจ็กต์และทำงานภายใต้การบัญชาของ Cache Manager นโยบายที่หน่วงการเขียนไปดิสก์แล้วถือในแคชที่เรียกการเขียนแบบหน่วง (lazy writing) เมื่ออ่านไฟล์ช่วง 256KB ถูกอ่านเข้าช่อง 256KB ในพื้นที่ที่อยู่ระบบ และโพรเซสผู้ใช้คัดลอกข้อมูลกับช่องนั้น Cache Manager ที่ปลุก lazy writer ทุกวินาที แล้วกองหนึ่งในแปดของเพจที่ยังไม่ถูกฟลัชเร็ว ๆ นี้เข้าคิวเขียนดิสก์ และถ้าจำเป็นกองเพิ่ม ไฟล์ชั่วคราวที่ไม่ถูกฟลัช ถ้าเกิดความล้มเหลวระบบฉับพลันอย่างการสูญเสียไฟ ข้อมูลแคชที่ยังไม่ถูกเขียนจะหาย FILE_FLAG_NO_BUFFERING ที่ปิดแคชแล้วเมทาดาตาไฟล์ยังถูกแคชได้ FILE_FLAG_WRITE_THROUGH ที่ข้อมูลถูกเขียนเข้าแคชด้วยและถูกเขียนลงดิสก์ทันทีโดยไม่มีความหน่วงของ lazy writer และเมทาดาตาของระบบไฟล์ที่ถูกแคชเสมอ จึงต้องฟลัชหรือ 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. ว่าด้วย WriteFile ที่ปกติเขียนเข้าบัฟเฟอร์ภายใน และ OS เขียนออกดิสก์เป็นระยะ FlushFileBuffers ที่เขียนข้อมูลในบัฟเฟอร์ของไฟล์ที่ระบุทั้งหมดไปอุปกรณ์ การเรียกทุกครั้งในการเขียนจำนวนมากที่ไม่มีประสิทธิภาพ และแอปที่ต้องการความทนทานของข้อมูลสำคัญในการเขียนบ่อย ควรใช้ I/O ไม่บัฟเฟอร์ด้วย FILE_FLAG_NO_BUFFERING กับ FILE_FLAG_WRITE_THROUGH และการเรียกต่อแฮนเดิลวอลุ่ม (ด้วยสิทธิ์ผู้ดูแล) ที่ฟลัชไฟล์ที่เปิดทั้งหมดบนวอลุ่มได้ ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, File Buffering. ว่าด้วยข้อกำหนดการเข้าถึงไฟล์ที่เปิดด้วย FILE_FLAG_NO_BUFFERING ขนาดและการออฟเซ็ตไฟล์ของการอ่านเขียน (รวมเมื่อระบุด้วย OVERLAPPED) ต้องเป็นจำนวนเท่าของขนาดเซกเตอร์ของวอลุ่ม ที่อยู่ของบัฟเฟอร์อ่านเขียนควรจัดแนวตามขนาดเซกเตอร์กายภาพ และการคำนึงอุปกรณ์ Advanced Format เซกเตอร์กายภาพ 4,096 ไบต์ที่จำเป็น ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, File Mapping. ว่าด้วยอ็อบเจ็กต์แมปไฟล์ที่ถูกหนุนด้วยไฟล์บนดิสก์ และการสลับเพจออกที่ถูกทำเป็นการเขียนเนื้อหาที่เปลี่ยนไปไฟล์ เมื่อหลายโพรเซสสร้างวิวของไฟล์ท้องถิ่นจากอ็อบเจ็กต์แมปไฟล์เดียวกัน ข้อมูลสอดคล้อง (เนื้อหาเดียวกับไฟล์บนดิสก์) ↩ ↩2 ↩3
-
Microsoft Learn, FlushViewOfFile function. ว่าด้วย FlushViewOfFile ที่เริ่มเขียนเพจสกปรกในช่วงวิวแมปไปดิสก์ ฟังก์ชันนี้ไม่ฟลัชเมทาดาตาไฟล์ และไม่รอการเขียนกายภาพจากแคชดิสก์ฮาร์ดแวร์ให้จบ และเพื่อเขียนเพจสกปรกกับเมทาดาตาทั้งหมดทางกายภาพจนจบ ควรเรียก FlushFileBuffers หลัง FlushViewOfFile ↩ ↩2 ↩3
-
Microsoft Learn, IRPs Are Different From Fast I/O. ว่าด้วย Fast I/O ที่เป็นเส้นทางเร็วของ I/O ซิงค์สำหรับไฟล์ที่ถูกแคช ซึ่งเรียกจุดเข้าของระบบไฟล์หรือ Cache Manager โดยตรงโดยไม่สร้าง IRP ข้อมูลถูกส่งตรงจากแคชไปบัฟเฟอร์ผู้ใช้ (หรือกลับกัน) และเมื่อ Fast I/O ประมวลผลไม่ได้ เส้นทางปกติฐาน IRP ถูกใช้ ↩ ↩2 ↩3
-
Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. ว่าด้วยเมื่อข้อมูลอยู่ในแคช คำขอจบตรงนั้นแล้วคืน TRUE แคชของ Windows ที่ถูกอิมพลีเมนต์เป็นการแมปไฟล์ และเพราะไม่มีกลไกเพจฟอลต์อะซิงค์เมื่อเพจไม่อยู่ การอ่านอะซิงค์ที่แคชเปิดอาจถูกประมวลผลแบบซิงค์ ↩
-
Microsoft Learn, FileStream.Flush method (.NET). ว่าด้วย Flush() ที่เขียนบัฟเฟอร์ภายในของสตรีมออกให้ OS และเมื่อระบุ Flush(true) จะฟลัชบัฟเฟอร์ไฟล์ขั้นกลางทั้งหมด (บัฟเฟอร์ของ OS) ด้วย ↩
บทความที่เกี่ยวข้อง
บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง
เชิงลึกของ Windows I/O (ตอนที่ 5) — โครงสร้างภายในของ NTFS: เข้าใจระบบไฟล์จาก MFT
ตอนที่ 5 ของซีรีส์ที่อธิบายโครงสร้างภายในของ NTFS ด้วยแผนภาพ จัดจากมุมนักพัฒนา ทั้ง MFT กับเรกคอร์ดไฟล์ สตรีมข้อมูลหลายอัน (Zone.Identifi...
เชิงลึกของ Windows I/O (ตอนที่ 6・ตอนสุดท้าย) — ไดรเวอร์ฟิลเตอร์กับมินิฟิลเตอร์: เหตุที่ Procmon และการสแกนไวรัสแทรก I/O ได้
ตอนสุดท้ายของซีรีส์ที่อธิบายไดรเวอร์ฟิลเตอร์และมินิฟิลเตอร์ของ Windows ด้วยแผนภาพ ครอบ Filter Manager กับอัลติจูด คอลแบ็ก pre/post กลไกที...
แนวปฏิบัติมัลติเธรดเชิงปฏิบัติ: ฉบับ .NET — สิ่งที่ต้องตัดสินก่อนเพิ่มเธรด
เรียบเรียงหลักออกแบบที่กันไม่ให้โค้ดมัลติเธรด .NET/C# «แครชหรือค้างเป็นครั้งคราว»: อย่าสร้างเธรดเอง ให้ขี่ Task ลดสถานะที่เปลี่ยนแปลงร่วม...
ใช้ WMI/CIM จาก C# และ PowerShell — คู่มือปฏิบัติการดึงข้อมูลฮาร์ดแวร์ เฝ้าโพรเซส และสอบถามระยะไกล
หมายเลขซีเรียลของพีซี การเฝ้าพื้นที่ว่างดิสก์ และการตรวจจับการเริ่มโพรเซส คำตอบมาตรฐานคือ WMI/CIM บทความนี้อธิบายการใช้ cmdlet CIM เช่น G...
Named Pipe ในทางปฏิบัติ — IPC มาตรฐานของ Windows ตั้งแต่การออกแบบถึงความปลอดภัย
คู่มือเชิงปฏิบัติเรื่อง named pipe ซึ่งเป็นการสื่อสารระหว่างโปรเซสมาตรฐานของ Windows บทความนี้จัดระเบียบจากแหล่งปฐมภูมิ ทั้งการเลือกระหว่...
หัวข้อที่เกี่ยวข้อง
หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ
หัวข้อเทคนิคของ Windows
ประตูสู่การพัฒนา Windows การตรวจสอบบั๊ก และการใช้ประโยชน์จากสินทรัพย์เดิม
บริการที่เกี่ยวข้องกับหัวข้อนี้
บทความนี้เกี่ยวข้องโดยตรงกับบริการต่อไปนี้
พัฒนาแอป Windows
แอปธุรกิจ การเชื่อมต่ออุปกรณ์ และเครื่องมือสื่อสาร ตั้งแต่ความต้องการจนถึงการพัฒนา
คำถามที่พบบ่อย
คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้
- เมื่อ WriteFile คืนความสำเร็จ ข้อมูลถูกเขียนลงดิสก์แล้วหรือ?
- ตามค่าเริ่มต้นยังไม่ถูกเขียน แคชไฟล์ของ Windows เป็นแบบไรต์แบ็ก และ WriteFile คืนความสำเร็จเมื่อคัดลอกข้อมูลเข้าแคชไฟล์ระบบแล้ว การเขียนลงดิสก์ทำทีหลังโดย lazy writer ที่ Cache Manager ปลุกทุกวินาที จุดสำคัญคือความต่างตามชนิดของความล้มเหลว แม้โพรเซสของแอปพัง ข้อมูลที่เข้าแคชแล้วไม่หาย เพราะ OS ยังมีชีวิตจะเขียนกลับทีหลัง ในทางกลับกัน ความล้มเหลวที่ OS ทั้งก้อนล้ม (ตัดไฟ หน้าจอสีน้ำเงิน) แคชสกปรกที่ยังไม่ถูกเขียนจะหาย การอ่านที่ถูกของ «WriteFile สำเร็จ» ไม่ใช่ «ถูกทำให้ทนทานแล้ว» แต่เป็น «ส่งมอบให้ OS แล้ว»
- จะเขียนลงดิสก์ให้ชัวร์ได้อย่างไร?
- มีสามเครื่องมือ ข้อแรก FlushFileBuffers ที่เขียนข้อมูลในบัฟเฟอร์และเมทาดาตาของไฟล์นั้นไปอุปกรณ์จนจบ (.NET เทียบเท่า FileStream.Flush(true)) ข้อสอง FILE_FLAG_WRITE_THROUGH ที่ทุกครั้งที่เขียน ทั้งเขียนเข้าแคชและเขียนลงดิสก์ทันที ข้อสาม FILE_FLAG_NO_BUFFERING ที่ไม่ผ่านแคชเอง เอกสารของ Microsoft ระบุว่าเรียก FlushFileBuffers ทุกครั้งที่เขียนไม่มีประสิทธิภาพ และถ้าต้องการความทนทานที่ชัวร์กับการเขียนบ่อย ควรใช้ FILE_FLAG_NO_BUFFERING คู่กับ FILE_FLAG_WRITE_THROUGH ทั้งสามสละคุณของแคชแลกความช้า ดังนั้นหลักในงานจริงไม่ใช่ «ติดทุกอย่าง» แต่จำกัดใช้กับการเขียนที่เสียไม่ได้
- FILE_FLAG_WRITE_THROUGH กับ FILE_FLAG_NO_BUFFERING ต่างกันอย่างไร?
- WRITE_THROUGH คือ «เขียนเข้าแคชด้วย แต่ก่อนจบก็เขียนลงดิสก์ด้วย» การอ่านยังได้คุณของแคช ตัดแค่ความหน่วงของการเขียนแบบหน่วง NO_BUFFERING คือ «การอ่านเขียนไม่ผ่านแคชระบบ» ทั้งอ่านและเขียนกลายเป็น I/O ไปอุปกรณ์ทุกครั้ง (แต่ที่เลี่ยงได้คือแคชของ Windows เท่านั้น ไม่ได้ข้ามแคชเขียนในอุปกรณ์) แลกกับข้อจำกัดเข้ม ขนาดและออฟเซ็ตไฟล์ของการอ่านเขียนต้องเป็นจำนวนเท่าของขนาดเซกเตอร์ของวอลุ่ม และที่อยู่ของบัฟเฟอร์ต้องจัดแนวตามขอบเซกเตอร์กายภาพ นอกจากนี้ แม้ NO_BUFFERING เมทาดาตาของระบบไฟล์ยังถูกแคชต่อ ดังนั้นการเขียนเมทาดาตาให้ชัวร์ยังต้องคู่ FlushFileBuffers หรือ WRITE_THROUGH ซอฟต์แวร์ที่จัดการบัฟเฟอร์เองอย่างเอนจินฐานข้อมูลใช้เป็นฉบับ แอปปกติควรพิจารณา WRITE_THROUGH หรือ FlushFileBuffers ก่อน
- Task Manager แสดงหน่วยความจำว่างน้อย เป็นเพราะแคชไฟล์หรือ?
- หลายกรณีใช่ และเป็นพฤติกรรมปกติ Windows ใช้หน่วยความจำกายภาพที่ว่างเป็นแคชไฟล์อย่างกระตือรือร้น การคัดลอกไฟล์ใหญ่หรืออ่านเขียนจำนวนมากทำให้แคชพองและดูเหมือนการใช้หน่วยความจำเพิ่ม อย่างไรก็ตาม เพจที่แคชใช้อยู่จำนวนมากเป็นชนิดที่แอปขอหน่วยความจำแล้วถูกนำไปใช้ค่อนข้างเร็ว จึงต้องแยกจากสถานะ «หน่วยความจำถูกกินจนไม่พอ» เมื่อสงสัยว่าหน่วยความจำไม่พอ ในงานจริงควรมองตัวชี้วัดอย่างหน่วยความจำที่คอมมิตแล้วและความถี่ของฮาร์ดฟอลต์ ไม่ใช่แค่ความจุว่างที่เห็น
- แตะไฟล์เดียวกันด้วยไฟล์แมปหน่วยความจำกับ ReadFile/WriteFile เนื้อหาจะเลื่อนหรือไม่?
- กับ I/O ที่แคชเปิดปกติไม่เลื่อน แคชของ Windows เองถูกอิมพลีเมนต์เป็นการแมปไฟล์ ดังนั้นวิวแมปกับแคชของไฟล์ท้องถิ่นเดียวกันแชร์ข้อมูลเดียวกัน การเปลี่ยนของฝ่ายหนึ่งเห็นจากอีกฝ่าย หลายโพรเซสที่สร้างวิวจากอ็อบเจ็กต์แมปไฟล์เดียวกัน ข้อมูลก็สอดคล้อง อย่างไรก็ตาม การอ่านเขียนของแฮนเดิลที่เปิดด้วย FILE_FLAG_NO_BUFFERING ไม่ผ่านแคช จึงอยู่นอกกรอบความสอดคล้องนี้ นอกจากนี้ การเขียนการเปลี่ยนของวิวแมปให้ถึงดิสก์ชัวร์ FlushViewOfFile อย่างเดียวไม่เขียนเมทาดาตาและไม่รอแคชฮาร์ดแวร์ จึงต้องเรียก FlushFileBuffers หลัง FlushViewOfFile