9.9 - צמצום העתקות ובעלות על נתוני רשת - הרצאה
איזה מחיר אנחנו מנסים להסיר¶
העברת נתונים יכולה לעלות בכמה דרכים: כניסות לקרנל, העתקת בתים, ניהול דפים, נעילות, מיפויים והמתנה להשלמות. לכן הביטוי ללא העתקה אינו מדד ביצועים בפני עצמו. צריך לזהות איזו העתקה נחסכה, איזה מחיר נוסף נוצר ואילו משאבים נשארים תפוסים לזמן ארוך יותר.
גם איגוד כמה הודעות בקריאת מערכת אחת אינו אותו דבר כמו חיסכון בהעתקה. אפשר להפחית את מספר הכניסות לקרנל ועדיין להעתיק את כל הבתים. אפשר גם להימנע מהעתקה למרחב משתמש אבל לשלם על ניהול הפניות לדפים ועל הודעות השלמה. בחירה נכונה מתחילה במדידה של צוואר הבקבוק הקיים.
כמה הודעות בקריאה אחת¶
הממשק sendmmsg מאפשר להגיש מערך הודעות. ערך ההחזרה מציין כמה איברים טופלו, ובכל איבר יש מידע על כמות הבתים שלו. אם רק חלק מהמערך נשלח, הניסיון הבא צריך להתחיל מהאיבר הבא ולא לשדר שוב את כל המערך. אחרת האופטימיזציה יוצרת כפילויות בפרוטוקול.
בצד הקבלה אפשר להשתמש ב־recvmmsg כדי לקבל כמה הודעות אל מערך באפרים. עדיין צריך לבדוק עבור כל הודעה את אורכה, את דגלי הקטיעה ואת נתוני הבקרה שלה. מספר הודעות שנקלטו אינו הוכחה שכל אחת מהן מתאימה לפרוטוקול.
האיגוד מוסיף גם שאלת זמן. אם מחכים למלא אצווה גדולה לפני עיבוד, בקשה בודדת עלולה להמתין יותר. לכן בוחרים גודל אצווה ומדיניות המתנה לפי עומס וזמן תגובה. בשילוב עם מנוע אירועים אפשר להמתין למוכנות ואז לצרוך אצווה במצב לא חוסם, תוך שמירת מועד הסיום ברמת המנוע.
העברה מקובץ לסוקט¶
הממשק sendfile מאפשר להעביר נתונים בין מתארים בלי שהיישום יבצע את מחזור הקריאה לבאפר משתמש והכתיבה ממנו. סוגי המקור והיעד הנתמכים מוגדרים בממשק ובגרסת המערכת. אין להניח שהוא תחליף כללי לכל שילוב של שני מתארים.
גם כאן יכולה להיות התקדמות חלקית. מספר הבתים שביקשנו אינו בהכרח המספר שהועבר בקריאה אחת. כאשר משתמשים במצב לא חוסם, צריך לשמור היסט ולחזור למנגנון המוכנות במקרה של המתנה. כאשר משתמשים בהיסט מפורש, ניתן להימנע משינוי הסמן המשותף של קובץ המקור.
נוסף על כך, מקור שמועבר במסלול המבוסס על הפניות לדפים צריך להישאר בעל תוכן מתאים לאורך התקופה שהממשק דורש. העובדה שהקריאה חזרה אינה תמיד רשות לשנות מיד את אותם בתים במקור. בדוגמה נשתמש בקובץ אנונימי חתום, כך שלא נצטרך להניח שכותב אחר מתנהג יפה.
ניסוי עם מקור חתום¶
הקוד הבא שולח מיביבייט אחד דרך חיבור מקומי ובודק בצד המקבל גם כמות וגם סכום בדיקה פשוט. סכום הבדיקה נועד לזהות שגיאות בניסוי, ואינו מנגנון קריפטוגרפי. החבילה דורשת את התלות libc = "0.2" ומערכת לינוקס.
use std::fs::File;
use std::io::{self, Read, Write};
use std::net::{Shutdown, TcpListener, TcpStream};
use std::os::fd::{AsRawFd, FromRawFd};
use std::time::Duration;
fn update(hash: &mut u64, bytes: &[u8]) {
for &byte in bytes {
*hash ^= byte as u64;
*hash = hash.wrapping_mul(1_099_511_628_211);
}
}
fn main() -> io::Result<()> {
let raw = unsafe { libc::memfd_create(c"sendfile-source".as_ptr(),
libc::MFD_CLOEXEC | libc::MFD_ALLOW_SEALING) };
if raw < 0 { return Err(io::Error::last_os_error()); }
let mut source = unsafe { File::from_raw_fd(raw) };
let data: Vec<u8> = (0..1_048_576).map(|index| index as u8).collect();
source.write_all(&data)?;
let mut expected_hash = 14_695_981_039_346_656_037_u64;
update(&mut expected_hash, &data);
let expected_length = data.len();
drop(data);
let seals = libc::F_SEAL_WRITE | libc::F_SEAL_GROW | libc::F_SEAL_SHRINK | libc::F_SEAL_SEAL;
if unsafe { libc::fcntl(source.as_raw_fd(), libc::F_ADD_SEALS, seals) } < 0 {
return Err(io::Error::last_os_error());
}
let listener = TcpListener::bind(("127.0.0.1", 0))?;
let address = listener.local_addr()?;
let receiver = std::thread::spawn(move || -> io::Result<(usize, u64)> {
let (mut stream, _) = listener.accept()?;
stream.set_read_timeout(Some(Duration::from_secs(3)))?;
let mut length = 0;
let mut hash = 14_695_981_039_346_656_037_u64;
let mut buffer = [0_u8; 65536];
loop {
match stream.read(&mut buffer) {
Ok(0) => return Ok((length, hash)),
Ok(count) => { length += count; update(&mut hash, &buffer[..count]); }
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
Err(error) => return Err(error),
}
}
});
let stream = TcpStream::connect(address)?;
stream.set_write_timeout(Some(Duration::from_secs(3)))?;
let mut offset: libc::off_t = 0;
let mut transferred = 0;
while transferred < expected_length {
let count = unsafe { libc::sendfile(stream.as_raw_fd(), source.as_raw_fd(), &mut offset,
(expected_length - transferred).min(65536)) };
if count > 0 { transferred += count as usize; continue; }
if count == 0 { return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "short source")); }
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted { return Err(error); }
}
stream.shutdown(Shutdown::Write)?;
let observed = receiver.join().unwrap()?;
assert_eq!(observed, (expected_length, expected_hash));
println!("verified {transferred} bytes");
Ok(())
}
הפלט הצפוי מאשר את מספר הבתים. אין בדוגמה מדידת ביצועים או הוכחה שלא התבצעה שום העתקה בתוך הקרנל. היא מוכיחה את מסלול השימוש בממשק ואת שלמות הנתונים בניסוי, ומספקת בסיס להשוואה מול לולאת קריאה וכתיבה רגילה.
המקור נשאר חתום גם אחרי שהשליחה מסתיימת, ולכן אין אפשרות לשנות את הנתונים בזמן שהצד השני עדיין קורא. מיקום המקור מועבר במשתנה נפרד, כך שהסמן שנשאר אחרי יצירת הקובץ אינו משנה את תחילת השליחה.
צינור כאמצעי ביניים¶
הממשק splice מעביר נתונים במסלול הכולל צינור. לפחות אחד הצדדים צריך להתאים לדרישה הזאת, וההיסטים והדגלים נקבעים לפי סוג המתאר. הוא מתאים לבניית מסלולי העברה מסוימים בלי להעתיק דרך באפר משתמש, אבל אינו מבטל את מגבלת הקיבולת של הצינור.
במסלול מקור אל צינור אל יעד צריך לנהל שני סוגי המתנה: מקום בצינור ויכולת של היעד לקבל נתונים. אם ממלאים את הצינור ומפסיקים לטפל בכיוון הריקון, המערכת יכולה להיתקע למרות שכל קריאה בנפרד נראית תקינה. גם דגל שמבקש פעולה לא חוסמת אינו בהכרח הופך כל מתאר משתתף ללא חוסם בלי תצורה מתאימה.
התיעוד של העברה מקובץ ושל העברה דרך צינור מגדיר את המגבלות. לפני שילוב עם הצפנה נבדוק גם היכן ההצפנה מתבצעת, משום שמסלול שקורא נתונים דרך ספריית משתמש אינו שקול להעברה ישירה של אותם בתים לסוקט.
באפר משתמש שמוחזר רק אחרי הודעה¶
במסלול MSG_ZEROCOPY היישום צריך לבקש את התכונה המתאימה ולצרוך הודעות השלמה דרך ערוץ השגיאות של הסוקט. עד ההודעה המתאימה אסור למחזר את הבאפר כאילו הקרנל כבר סיים להשתמש בנתונים. ההודעה יכולה לתאר טווח של פעולות, ולכן נדרש מעקב זהויות שמתאים לחוזה המספור.
המנגנון יכול לדווח שבמקרה מסוים בוצעה בכל זאת העתקה. הדבר אינו בהכרח כשל עסקי, אבל הוא משנה את פרשנות מדידת הביצועים. אם אנחנו מודדים רק את הדגל ששלחנו ולא את מסלול ההשלמות והמשאבים, אנחנו עלולים להכריז על חיסכון שלא התרחש.
העלות של ניהול דפים והשלמות יכולה להיות גדולה מהחיסכון עבור הודעות קטנות. לכן נמדוד כמה גדלי הודעות ונפריד בין זמן מעבד, תפוקה, זמן תגובה וכמות באפרים שממתינים להחזרה. התיעוד של מניעת העתקה בסוקט מגדיר את שינוי חוזה החיים, והתיעוד של שליחה באצווה מגדיר את ההתקדמות החלקית ברמת ההודעות.