תצפית בממשקי המערכת ובמצב התהליך¶
המקום שבו היישום פוגש את הקרנל¶
נניח שבקשה אורכת חצי שנייה. ייתכן שהיישום מחשב במשך כל הזמן, ממתין לנתונים בסוקט, מחכה לנעילה או אינו מקבל זמן מעבד. עקיבה אחר קריאות מערכת מראה חלק חשוב מהגבול בין היישום לקרנל, אבל אינה רואה כל הוראה שמבוצעת במרחב המשתמש. היעדר קריאות במשך זמן מסוים אינו הוכחה שהתהליך לא עשה עבודה.
נשתמש בכלי strace כדי לחבר בין פעולת היישום לבין הקריאות והערכים שחוזרים ממערכת ההפעלה. השאלה הראשונה אינה כמה שורות הכלי מסוגל להפיק, אלא איזה סוג אירוע יכול להבחין בין ההשערות שלנו. עקיבה ממוקדת קלה יותר לניתוח ופחות מפריעה למערכת מאיסוף בלתי מוגבל של כל פעולה.
שתי תוכניות עם המתנה שנראית דומה מבחוץ¶
נבנה תוכנית אחת עם שני מצבים. מצב החישוב צורך זמן במרחב המשתמש, ומצב ההמתנה קורא מסוקט עד שיצרן שולח בית. שני המצבים מוגבלים בזמן, ולכן אפשר להשוות אותם בלי להשאיר תהליך תקוע. נשמור את הקוד בשם boundary_probe.rs.
use std::hint::black_box;
use std::io::{self, Read, Write};
use std::os::unix::net::UnixStream;
use std::time::{Duration, Instant};
fn main() -> io::Result<()> {
let mode = std::env::args().nth(1).unwrap_or_else(|| "spin".into());
let started = Instant::now();
match mode.as_str() {
"spin" => {
let mut value = 1_u64;
while started.elapsed() < Duration::from_millis(400) {
for _ in 0..10_000 {
value = black_box(value.rotate_left(7).wrapping_add(13));
}
}
println!("value={value}");
}
"wait" => {
let (mut reader, mut writer) = UnixStream::pair()?;
let producer = std::thread::spawn(move || -> io::Result<()> {
std::thread::sleep(Duration::from_millis(400));
writer.write_all(b"x")
});
let mut byte = [0_u8; 1];
reader.read_exact(&mut byte)?;
producer.join().unwrap()?;
assert_eq!(&byte, b"x");
}
_ => return Err(io::Error::other("expected spin or wait")),
}
println!("elapsed={:?}", started.elapsed());
Ok(())
}
נבנה עם אופטימיזציה ומידע ניפוי, ונמדוד קודם בלי עקיבה. אחר כך נריץ את שני המצבים עם אותו מסנן. יש להחזיק את כל קובצי הפלט של ריצה אחת יחד, משום שהאפשרות -ff יכולה ליצור קובץ נפרד לכל משימה שנעקבה.
rustc --edition=2024 -C opt-level=2 -g boundary_probe.rs -o boundary_probe
./boundary_probe spin
./boundary_probe wait
strace -ff -ttt -T -yy -e trace=read,write,recvfrom,sendto,futex,clock_nanosleep \
-o spin.trace ./boundary_probe spin
strace -ff -ttt -T -yy -e trace=read,write,recvfrom,sendto,futex,clock_nanosleep \
-o wait.trace ./boundary_probe wait
במצב ההמתנה נצפה לפעולת קריאה שממתינה ליצרן, לצד שינה של תהליכון היצרן. במצב החישוב ייתכן מרווח ארוך בין קריאות בלי קריאת מערכת שאחראית לרוב הזמן. ספריית הריצה יכולה לבחור קריאות שונות בין גרסאות, ולכן נזהה את הפעולה לפי הארגומנטים והמתארים ולא רק לפי שם שזכרנו מהרצה קודמת.
זמן של קריאה אינו זמן מעבד שלה¶
האפשרות -T מציגה את משך הזמן הנצפה של קריאת המערכת. קריאה חוסמת יכולה לבלות את רובו בהמתנה, בלי לצרוך זמן מעבד. גם הפסקות תזמון ועבודת כלי העקיבה יכולות להשפיע על המדידה. אסור לסכם משכי קריאות של כמה תהליכונים ולהשוות אותם בלי מחשבה לזמן הקיר של התוכנית, משום שחלק מהקריאות חופפות.
תצוגת סיכום עם -c עונה על שאלה אחרת מתצוגת האירועים המלאה. נבדוק האם אנו מבקשים זמן מערכת או זמן קיר, ונשתמש באפשרות המתאימה של הגרסה המותקנת. ספירת קריאות מועילה לזיהוי לולאה שמבצעת אלפי ניסיונות כושלים, אבל אינה מסבירה לבדה האם כל ניסיון יקר.
הדפסת תוכן של קריאה יכולה לחשוף קלט ולהגדיל מאוד את נפח הפלט. נבחר אורך מחרוזת ומסנן מתארים או נתיבים שמתאימים לשאלה. גם כאשר הפלט נכתב לקובץ, יש עלות לאיסוף ולפענוח. מדידת ביצועים סופית צריכה לכלול השוואה בין ריצה עם הכלי לריצה בלעדיו.
מפת זיכרון מול זיכרון תושב¶
נבדוק את /proc/PID/maps כדי להבין אילו אזורים ממופים, ואת /proc/PID/smaps כדי לקבל חשבונאות מפורטת יותר לכל מיפוי. גודל אזור וירטואלי אינו אומר שכל הדפים שלו נמצאים בזיכרון פיזי. מדד הזיכרון התושב מתאר דפים נוכחים, בעוד המדד היחסי מחלק את עלות הדפים המשותפים בין המשתמשים בהם.
התוכנית הבאה מציגה שלושה שלבים של חיי וקטור ומדפיסה חלק ממידע הסיכום של המיפויים. היא מיועדת למערכת שבה הקובץ smaps_rollup זמין. חוסר זמינות מוחזר כשגיאה ולא מתורגם למדידה ריקה.
use std::fs;
use std::io;
use std::time::Duration;
fn report(stage: &str) -> io::Result<()> {
println!("stage={stage}");
for line in fs::read_to_string("/proc/self/smaps_rollup")?.lines() {
if ["Rss:", "Pss:", "Private_Dirty:", "Anonymous:"].iter()
.any(|prefix| line.starts_with(prefix)) {
println!("{line}");
}
}
Ok(())
}
fn main() -> io::Result<()> {
report("before")?;
let mut bytes = Vec::<u8>::with_capacity(64 * 1024 * 1024);
report("reserved")?;
bytes.resize(64 * 1024 * 1024, 0);
for index in (0..bytes.len()).step_by(4096) { bytes[index] = 1; }
std::hint::black_box(&bytes);
report("touched")?;
drop(bytes);
report("dropped")?;
std::thread::sleep(Duration::from_millis(100));
Ok(())
}
ההבדל בין הזמנה לנגיעה מדגים מדוע גודל הקיבולת אינו מדידת זיכרון פיזי. אחרי השחרור התנהגות המקצה וגודל ההקצאה קובעים אם האזור יוחזר מיד למערכת. אין לדרוש שכל מדד יחזור בדיוק לערכו המקורי, משום שהמדידה עצמה והספריות יכולות ליצור מצב נוסף. נשתמש בהפרשים ובחזרות כדי להבין את התופעה.
מידע תהליכי עם גבולות משמעות¶
הקובץ status מציג בין השאר מצב, זהויות, מספר תהליכונים ומדדי זיכרון מסוימים. התיקייה fd מציגה את המתארים שהתהליך מחזיק ברגע הבדיקה. הקריאה אינה צילום אטומי של כל העולם: תהליכון אחר יכול לפתוח או לסגור מתאר בזמן הסריקה. כאשר נדרש קשר מדויק בין אירועים נשלב עקיבה או מדדי יישום ולא ננסה להוכיח הכול מרשימת רגע אחת.
נתוני תזמון כגון schedstat יכולים לעזור לזהות זמן המתנה בתור ריצה, אך זמינותם ומשמעותם תלויים בתצורת הקרנל. ערך אפס אינו תמיד הוכחה שלא הייתה המתנה. נבדוק את התמיכה ואת יחידות המדידה לפני שילוב המספר בדוח או בגרף.
לחץ מערכתי אינו תכונה של תהליך בודד¶
תהליך יכול להיראות קטן בזמן שכל המכונה סובלת מלחץ זיכרון או עומס אחסון. נבדוק מדדים מערכתיים ומדדים של קבוצת הבקרה המתאימה, כולל זמני עצירה בגלל לחץ. הבחירה בתחום המדידה חשובה: לחץ בקבוצה מוגבלת אינו בהכרח לחץ במכונה כולה, ומכונה פנויה אינה מבטיחה שהקבוצה רשאית להשתמש במשאבים שלה.
נגדיר תקציב איסוף. למשל, ניקח תמונת מיפויים בתחילת ובסוף ניסוי קצר, נספור מתארים בקצב נמוך ונעקוב רק אחר קבוצת הקריאות הקשורה לחשד. איסוף של מפות גדולות בכל אלפית שנייה יכול לייצר בעצמו את ההאטה שאנו מנסים להבין. נרשום גם את עלות הכלים כחלק מתנאי הניסוי.
האפשרויות מתועדות במדריך העקיבה, ומשמעות שדות הזיכרון מפורטת בממשק המיפויים. הדוח צריך לקשר בין תצפית בגבול הקרנל לבין מצב היישום, ולציין היכן הכלי אינו רואה את הסיבה.