לדלג לתוכן

9.8 - נתיב חבילה ובקרת רשת - הרצאה

מה קורה לפני שהסוקט מתעורר

כאשר היישום מקבל מוכנות לקריאה, כבר התרחשו כמה שלבים מתחתיו. במערכת עם כרטיס רשת פיזי, ההתקן קיבל נתונים והעביר אותם לזיכרון לפי חוזה הדרייבר. הקרנל עיבד את המנות, זיהה את הפרוטוקול ואת הסוקט המתאים ועדכן את מצב הקבלה. רק אחר כך היישום יכול להתעורר ולקרוא.

המסלול המדויק משתנה בין התקן פיזי, ממשק וירטואלי וממשק הלולאה המקומית. לכן לא נציג תרשים אחד כאילו כל חבילה בכל מערכת עוברת בדיוק באותן פונקציות. במקום זאת נזהה את גבולות האחריות: התקן ותור, טיפול קרנלי, שכבת פרוטוקול, באפר סוקט ויישום.

אם זמן התגובה גדל, צריך לדעת באיזה גבול נוצרה ההמתנה. שימוש גבוה במעבד של תהליך השרת מצביע לכיוון אחר משימוש גבוה בטיפול רשת קרנלי. באפר סוקט מלא מצביע לכיוון אחר מתור התקן שמאבד מנות עוד לפני שהן מגיעות לסוקט.

תורים ועיבוד במנות

מנגנון NAPI מאפשר לדרייבר לעבד קבוצת אירועים במסגרת תקציב עבודה, במקום להניח שכל מנה מחייבת פסיקה נפרדת וטיפול מלא מיידי. תחת עומס, המשך העבודה יכול להיות מתוזמן שוב לפי מצב התור. ההקשר שבו העבודה מתבצעת תלוי בתצורה ובמימוש, ולכן יש לבדוק את הקרנל של המעבדה.

הדמיון לתקציב הסבב שבנינו במנוע האסינכרוני אינו מקרי מבחינה רעיונית. גם כאן צריך לאזן בין ריקון תור לבין מתן זמן לעבודה אחרת. ההבדל הוא שהדרייבר פועל תחת חוזי קרנל והתקן, ולכן כללי הזיכרון והנעילה שלו שונים מקוד משתמש.

כרטיסים ומנגנוני תוכנה יכולים לפזר זרמים בין תורים ומעבדים. הפיזור עשוי לשפר מקביליות, אבל גם לשנות מקומיות מטמון ותחרות על נתונים. הגדלת מספר תורים אינה מבטיחה שזרם יחיד ישתמש בכל המעבדים או שתהליך שקורא מסוקט יחיד יהפוך למקביל אוטומטית.

התיעוד של מנגנון עיבוד הרשת ושל פיזור העבודה בין מעבדים מספק את שמות המנגנונים. במעבדה נשאל איזה מהם מופעל בפועל, ולא נסיק זאת רק ממספר הליבות של המחשב.

תצורה אינה נתוני יישום

כדי לקרוא מצב של ממשקים, כתובות ונתיבים אפשר להשתמש בממשק netlink. גם הוא משתמש בסוקט ובהודעות, אך הוא משרת תקשורת עם רכיבי קרנל וממשקי בקרה. בקשת רשימה יכולה להחזיר כמה הודעות בכמה קבלות, והסיום מסומן במפורש.

מספר הרצף מאפשר להתאים תשובה לבקשה. כתובת המקור של הסוקט מאפשרת לבדוק מי שלח את ההודעה. אין לבלבל בין מזהה יציאה של הממשק לבין מזהה תהליך בכל מצב, ואין להניח שכל שדה מזהה בכותרת של תשובה חייב להיות אפס רק משום שהמקור הוא הקרנל.

הודעות מכילות לעיתים רצף מאפיינים משתנה. כל מאפיין מציין אורך וסוג, וצריך לדלג לפי היישור של הממשק. סוג לא מוכר אינו בהכרח שגיאה; ממשק שמתפתח יכול להוסיף מידע שהגרסה שלנו אינה משתמשת בו. לעומת זאת, אורך שאינו מתאים לגבולות ההודעה הוא שגיאת מבנה שחייבים לדחות.

בקשת רשימת ממשקים

הניסוי הבא מבקש רשימת ממשקים ומדפיס את מזהיהם. הוא מיועד לממשק הבינארי של לינוקס, משתמש בתלות libc = "0.2" ואינו משנה תצורת רשת. הפענוח נעשה מבתים כדי להימנע מיצירת השאלות לא מיושרות למבנים שהגיעו בתוך הודעה.

use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};

fn invalid(message: &str) -> io::Error {
    io::Error::new(io::ErrorKind::InvalidData, message)
}

fn u16_at(bytes: &[u8], offset: usize) -> u16 {
    u16::from_ne_bytes(bytes[offset..offset + 2].try_into().unwrap())
}

fn u32_at(bytes: &[u8], offset: usize) -> u32 {
    u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap())
}

fn main() -> io::Result<()> {
    let raw = unsafe { libc::socket(libc::AF_NETLINK, libc::SOCK_RAW | libc::SOCK_CLOEXEC,
                                  libc::NETLINK_ROUTE) };
    if raw < 0 { return Err(io::Error::last_os_error()); }
    let socket = unsafe { OwnedFd::from_raw_fd(raw) };
    let mut local: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
    local.nl_family = libc::AF_NETLINK as _;
    let address_size = std::mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t;
    if unsafe { libc::bind(socket.as_raw_fd(), (&local as *const libc::sockaddr_nl).cast(),
                          address_size) } < 0 { return Err(io::Error::last_os_error()); }
    let mut request = [0_u8; 32];
    request[..4].copy_from_slice(&32_u32.to_ne_bytes());
    request[4..6].copy_from_slice(&(libc::RTM_GETLINK as u16).to_ne_bytes());
    request[6..8].copy_from_slice(&((libc::NLM_F_REQUEST | libc::NLM_F_DUMP) as u16).to_ne_bytes());
    request[8..12].copy_from_slice(&1_u32.to_ne_bytes());
    request[16] = libc::AF_UNSPEC as u8;
    let sent = unsafe { libc::sendto(socket.as_raw_fd(), request.as_ptr().cast(), request.len(), 0,
                                   (&local as *const libc::sockaddr_nl).cast(), address_size) };
    if sent < 0 { return Err(io::Error::last_os_error()); }
    if sent as usize != request.len() { return Err(invalid("short request")); }
    let mut interfaces = Vec::new();
    loop {
        let mut buffer = [0_u8; 32768];
        let mut peer: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
        let mut vector = libc::iovec { iov_base: buffer.as_mut_ptr().cast(), iov_len: buffer.len() };
        let mut message: libc::msghdr = unsafe { std::mem::zeroed() };
        message.msg_name = (&mut peer as *mut libc::sockaddr_nl).cast();
        message.msg_namelen = address_size;
        message.msg_iov = &mut vector;
        message.msg_iovlen = 1;
        let count = unsafe { libc::recvmsg(socket.as_raw_fd(), &mut message, 0) };
        if count < 0 {
            let error = io::Error::last_os_error();
            if error.kind() == io::ErrorKind::Interrupted { continue; }
            return Err(error);
        }
        if count == 0 || message.msg_flags & libc::MSG_TRUNC != 0 {
            return Err(invalid("truncated or empty datagram"));
        }
        if message.msg_namelen != address_size || peer.nl_pid != 0 {
            return Err(invalid("unexpected sender"));
        }
        let bytes = &buffer[..count as usize];
        let mut offset = 0;
        while offset < bytes.len() {
            if bytes.len() - offset < 16 { return Err(invalid("short message header")); }
            let length = u32_at(bytes, offset) as usize;
            if length < 16 || length > bytes.len() - offset { return Err(invalid("bad message length")); }
            let item = &bytes[offset..offset + length];
            let kind = u16_at(item, 4);
            let flags = u16_at(item, 6);
            if u32_at(item, 8) != 1 { return Err(invalid("unexpected sequence")); }
            if flags & libc::NLM_F_DUMP_INTR as u16 != 0 { return Err(invalid("dump interrupted; retry")); }
            if kind == libc::NLMSG_ERROR as u16 || kind == libc::NLMSG_DONE as u16 {
                if item.len() >= 20 {
                    let error = i32::from_ne_bytes(item[16..20].try_into().unwrap());
                    if error < 0 { return Err(io::Error::from_raw_os_error(-error)); }
                } else if kind == libc::NLMSG_ERROR as u16 { return Err(invalid("short error")); }
                if kind == libc::NLMSG_DONE as u16 {
                    for index in interfaces { println!("interface index: {index}"); }
                    return Ok(());
                }
            } else if kind == libc::RTM_NEWLINK as u16 {
                if item.len() < 32 { return Err(invalid("short interface record")); }
                let index = i32::from_ne_bytes(item[20..24].try_into().unwrap());
                interfaces.push(index);
            }
            let aligned = (length + 3) & !3;
            if aligned > bytes.len() - offset {
                if length == bytes.len() - offset { break; }
                return Err(invalid("missing alignment bytes"));
            }
            offset += aligned;
        }
    }
}

הפריסה של הכותרות בדוגמה היא חלק מהממשק הבינארי של לינוקס ואינה פורמט רשת כללי. לכן המספרים משתמשים בסדר הבתים המקומי. בקוד רב־יעדים נשען על הגדרות יעד שנבדקו ועל ספריית פענוח מתאימה, ולא נעתיק את ההיסטים האלה לפרוטוקול אחר.

הרשימה נשמרת עד הודעת הסיום ורק אז מודפסת. כך כשל באמצע אינו מציג חצי רשימה כאילו הייתה תמונה מלאה. עדיין אין הבטחה שהרשת לא השתנתה בזמן הבקשה. דגל הפרעה לתמונה מחייב ניסיון חדש או טיפול מתאים, ולא שימוש בתוצאה כאמת מוחלטת.

מאפיינים משתנים ואובדן הודעות

כדי לקרוא שם ממשק, ממשיכים אחרי מבנה הממשק ומפענחים מאפיינים עד סוף ההודעה. לפני כל מאפיין בודקים שיש כותרת, שהאורך כולל לפחות אותה ושהמאפיין כולו נמצא בתחום. לאחר מכן מדלגים לפי יישור של ארבעה בתים. עבור שם בודקים גם את סיום המחרוזת בתוך האורך שנמסר.

מנוי לעדכונים אינו הופך את הערוץ ליומן אינסופי שאינו מאבד מידע. אם המקבל אינו מספיק לצרוך או שהבאפר נקטע, ייתכן שצריך לבנות מחדש את התמונה באמצעות בקשת רשימה. מצב כזה חייב לסמן את התמונה המקומית כלא מעודכנת, ולא להמשיך להציג אותה כאילו כל העדכונים נקלטו.

הצטרפות לעדכונים לפני בקשת תמונה יכולה לצמצם חלון חסר, אבל שילוב תמונה ועדכונים עדיין דורש מדיניות עקבית וטיפול בשינויים בזמן הבנייה. עבור כלי תצפית אפשר לפעמים לסמן תמונה זמנית ולרענן; עבור בקר שמבצע פעולות נדרשת זהירות נוספת לפני הסתמכות על מצב ישן.

שינוי נתיב אינו חיבור חדש

שינוי טבלת ניתוב יכול להשפיע על מנות עתידיות של סוקט קיים בלי לשנות את זהות החיבור כפי שהיישום רואה אותה. הדבר אינו מבטיח שהחיבור ימשיך לעבוד: כתובת המקור, מדיניות, מצב עמיתים ונתיבים חוזרים עדיין חשובים. נבדוק את ההתנהגות בסביבת רשת מבודדת ונקשר בין זמן השינוי, ההודעות שהתקבלו ומדדי הסוקט.

התיעוד של ערוץ הבקרה ושל הודעות ניתוב וממשקים מגדיר את הפורמטים. הניסוי נותן בסיס לקריאת המצב מהקרנל, ומאפשר בהמשך לחבר בין תצפית ברמת היישום לבין מה שמתרחש בתורים ובנתיבים שמתחתיו.