קלט ופלט ממוקמים וחלקיים¶
נניח ששני רכיבים קוראים טווחים שונים מאותו קובץ פתוח. אם כל אחד מבצע seek ואז read, הם יכולים לשנות זה לזה את ה־offset בין שתי הפעולות. הבעיה קיימת גם עם שני מתארים שנוצרו באמצעות dup, משום שהם חולקים את אותה פתיחה. אנחנו צריכים פעולה שמקבלת את המיקום כחלק מהבקשה עצמה.
הממשקים pread ו־pwrite מבצעים פעולה במיקום נתון בלי לעדכן את ה־offset הרגיל של הפתיחה לפי החוזה. בראסט אפשר להשתמש ב־FileExt של Unix. זאת אינה הבטחה לפעולה מלאה: גם קריאה ממוקמת יכולה להחזיר פחות בייטים מהמבוקש, ולכן ננהל את ההתקדמות המקומית במפורש.
הקוד הבא הוא תוכנית מלאה שמדגימה קריאה ממוקמת ושמירת ה־offset הרגיל. הכינו קודם קובץ letters.txt עם התוכן abcdefgh.
use std::{fs::File, io::{self, Read, Seek}};
use std::os::unix::fs::FileExt;
fn read_full_at(file: &File, mut buffer: &mut [u8], mut offset: u64) -> io::Result<()> {
while !buffer.is_empty() {
match file.read_at(buffer, offset) {
Ok(0) => return Err(io::ErrorKind::UnexpectedEof.into()),
Ok(count) => {
offset = offset.checked_add(count as u64).ok_or_else(||
io::Error::new(io::ErrorKind::InvalidInput, "offset overflow"))?;
buffer = &mut buffer[count..];
}
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
Err(error) => return Err(error),
}
}
Ok(())
}
fn main() -> io::Result<()> {
let mut file = File::open("letters.txt")?;
let mut middle = [0; 3];
read_full_at(&file, &mut middle, 3)?;
assert_eq!(&middle, b"def");
assert_eq!(file.stream_position()?, 0);
let mut first = [0; 2];
file.read_exact(&mut first)?;
assert_eq!(&first, b"ab");
Ok(())
}
הפונקציה אינה הופכת קריאה בכמה חלקים ל־snapshot עקבי מול כותב אחר. היא רק מסירה את המרוץ על ה־offset המשותף. אם התוכן משתנה בין שתי קריאות חלקיות, הבאפר יכול להכיל נתונים משני מצבים. הפרדת מיקום מסנכרון תוכן מונעת מאיתנו לייחס לממשק הבטחה שאין לו.
בכתיבה ממוקמת צריך להיזהר גם מדגלי הפתיחה. בלינוקס קיימת התנהגות מתועדת של pwrite על פתיחה עם O_APPEND, שבה הכתיבה מצטרפת לסוף במקום לכבד את המיקום כמקובל בחוזה POSIX. לכן לא נפתח קובץ ב־append ואז נניח שכתיבה ממוקמת תדרוס רשומה מסוימת. נבחר פתיחה שמתאימה לפרוטוקול ונבדוק את הדגלים שהשותפים חולקים.
הממשקים הווקטוריים מאפשרים להעביר כמה טווחי זיכרון באותה בקשה. זה שימושי כאשר הודעה כוללת כותרת וגוף שכבר נמצאים בבאפרים שונים. אין צורך להעתיק אותם לבאפר אחד רק כדי לקרוא write, אבל צריך לטפל בכתיבה חלקית שיכולה להסתיים באמצע אחד הטווחים.
הפונקציה הבאה עצמאית וניתנת לשימוש עם כל Write. היא מקדמת את רשימת הפרוסות לפי מספר הבייטים שהתקבל בפועל.
use std::io::{IoSlice, Write};
fn write_all_parts<W: Write>(writer: &mut W, mut parts: &mut [IoSlice<'_>])
-> io::Result<()> {
IoSlice::advance_slices(&mut parts, 0);
while !parts.is_empty() {
match writer.write_vectored(parts) {
Ok(0) => return Err(io::ErrorKind::WriteZero.into()),
Ok(count) => IoSlice::advance_slices(&mut parts, count),
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
Err(error) => return Err(error),
}
}
Ok(())
}
המימוש של Write יכול לבחור לטפל רק בפרוסה הראשונה, ולכן הלולאה נשארת נחוצה גם כאשר הממשק נקרא vectored. עבור קריאה גולמית ל־writev צריך לכבד גם מגבלות על מספר הטווחים ועל סכום אורכם. איסוף מאות אלפי טווחים לבקשה אחת אינו חוזה שהקרנל חייב לקבל.
אטומיות של כתיבה תלויה ביעד ובגודל. צינור, קובץ רגיל ו־socket אינם מספקים בדיוק אותן הבטחות לגבי שילוב כותבים או גבולות הודעה. גם אם בקשת writev אחת נראית לקורא כרצף אחד בתרחיש מסוים, אין להסיק ממנה פרוטוקול הודעות מלא לכל יעד.
הפרטים מופיעים בעמוד pread ובעמוד readv. לפני שימוש בביצוע ממוקם או וקטורי, נסחו בנפרד את חוזה המיקום, ההתקדמות, השיתוף והעמידות.