הקצאת אחסון וקלט ופלט ישיר¶
נניח שקובץ מציג גודל של גיגה־בייט אבל משתמש בכמה קילובייטים של אחסון. ייתכן שהוא sparse: חלק מהטווח הלוגי אינו מגובה בבלוקים מוקצים, וקריאה ממנו מחזירה אפסים. אם נעתיק את כל הבייטים באופן רגיל, אפשר להפוך אותו לקובץ שתופס הרבה יותר מקום בלי לשנות את התוכן הנראה.
הקוד הבא יוצר קובץ תרגיל חדש, מגדיר גודל לוגי וכותב רק בשני מקומות. הוא משתמש בפעולות ממוקמות כדי לא לערב את ה־offset הרגיל.
use std::{fs::OpenOptions, io};
use std::os::unix::fs::{FileExt, MetadataExt};
fn main() -> io::Result<()> {
let file = OpenOptions::new().read(true).write(true)
.create_new(true).open("sparse-lab")?;
let length = 64 * 1024 * 1024u64;
file.set_len(length)?;
file.write_all_at(b"start", 0)?;
file.write_all_at(b"end", length - 3)?;
file.sync_all()?;
let metadata = file.metadata()?;
println!("logical={} allocated_512_units={}", metadata.len(), metadata.blocks());
let mut hole = [1u8; 16];
file.read_exact_at(&mut hole, length / 2)?;
assert_eq!(hole, [0; 16]);
Ok(())
}
השדה blocks מדווח ביחידות של 512 בייט בממשק Unix שבו השתמשנו, ואינו זהה בהכרח לגודל הבלוק הפנימי של מערכת הקבצים. גם התוצאה יכולה להיות מושפעת מדחיסה, הקצאה דחויה ומימוש המערכת. לכן נשמור את סוג filesystem ואת אפשרויות הסביבה יחד עם המדידה.
הממשק fallocate מאפשר לבקש הקצאת טווחים או פעולות אחרות לפי הדגלים ותמיכת מערכת הקבצים. הזמנת מקום מראש יכולה לצמצם הפתעות של חוסר מקום במהלך כתיבה, אבל יש לקרוא את החוזה של הפעולה המסוימת. קבצים עם copy-on-write, שיתוף extents או תכונות אחרות יכולים לדרוש עבודה נוספת בהמשך.
חור לוגי אינו בהכרח מקום שההתקן הפיזי מחק. גם discard הוא בקשה לשכבת האחסון עם סמנטיקה שאינה שקולה בהכרח למחיקה מאובטחת. אנחנו נפריד בין גודל קובץ, בלוקים מוקצים, שיתוף נתונים והבטחת מחיקה, במקום להשתמש במילה מקום עבור כולם.
כעת נבחן O_DIRECT. הדגל מבקש מסלול קלט ופלט שמצמצם שימוש ב־page cache בהתאם למערכת ולפעולה, אבל אינו הבטחה שהכתיבה נעשתה עמידה. נתון יכול לעקוף cache אחד ולהישאר במטמון של התקן או בשכבה אחרת. עדיין צריך חוזה סנכרון מתאים כאשר מחזירים אישור durability.
קלט ופלט ישיר גם דורש לעיתים יישור של כתובת הבאפר, אורך הפעולה וה־offset. הדרישות משתנות לפי filesystem והתקן, ולכן אין לקבע תמיד מספר כמו 4096 רק מפני שזה גודל עמוד נפוץ. ממשקים כמו statx יכולים לספק מידע מתאים בסביבות שתומכות בשדות הרלוונטיים, ויש לבדוק מה חזר בפועל.
באפר של Vec<u8> אינו מבטיח יישור מיוחד מעבר לחוזה הטיפוס והמקצה. אפשר להשתמש בהקצאה בעלת Layout מתאים או בממשק C מתאים, אבל אז חייבים לשחרר באותו חוזה הקצאה. אסור להעביר מצביע כזה ל־Vec::from_raw_parts בלי להוכיח שגם הקיבולת, היישור והמקצה תואמים למה ש־Vec מצפה.
ערבוב גישה ישירה עם מיפוי זיכרון או קלט ופלט רגיל לאותו טווח דורש זהירות נוספת. השכבות צריכות לשמור עקביות בין עותקים ומטמונים, והביצועים יכולים להיפגע בגלל invalidation וסנכרון. לפני שמוסיפים O_DIRECT, נבדוק אם המערכת באמת צריכה לנהל caching בעצמה ואם העומס מצדיק את המורכבות.
ניסוי השוואה צריך לכלול מצב cache, גודל בקשות, יישור, queue depth וסדר סנכרון זהה. אם גרסה אחת מבצעת fsync והשנייה רק write, ההבדל בזמן אינו השוואה הוגנת של מנגנון העברת בייטים. הוא מערבב שתי הבטחות שונות למשתמש.
הפרטים מתועדים בעמוד fallocate ובסעיף הקלט והפלט הישיר של עמוד open. ההחלטה צריכה להתבסס על החוזה של מערכת הקבצים שנבחרה ועל מדידה של העומס, ולא על ההנחה שכל עקיפת cache היא שיפור.