אינטראקציה בין kswapd ל-lmkd

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

רקע

ב-Android יש שני מנגנונים עיקריים לשחזור זיכרון כשהמערכת מתחילה להתמלא:

  1. ‫kswapd: דימון סטנדרטי של ליבת Linux שמחזיר דפים על ידי פינוי דפים נקיים שמגובים בקבצים או החלפת דפים אנונימיים ל-ZRAM.
  2. ‫lmkd: תהליך הרקע Low Memory Killer (מחסל זיכרון נמוך) במרחב המשתמש של Android. כאשר kswapd לא מצליח לעמוד בדרישות הזיכרון והלחץ על המערכת גובר (כפי שמצוין על ידי PSI), lmkd מתערב כדי להרוג תהליכים ולפנות נתחים גדולים יותר של זיכרון.

‫kswapd ו-lmkd הן שתי מערכות משלימות להחזרת זיכרון. הם פועלים כדי להשיג את אותה מטרה (לפנות זיכרון), אבל בדרכים שונות. כדי להשוות ולמצוא הבדלים:

  • ניתן לשחזור לעומת הרסני: kswapd משחרר דפי זיכרון שאפשר לקרוא בחזרה מהאחסון, או מחליף דפי זיכרון למחיצות זיכרון אחרות או לאחסון שאפשר לקרוא בחזרה לפי דרישה. הפקודה lmkd סוגרת תהליכים, מה שגורם לאובדן של מצב לא נדיף.
  • החזרת רוחב פס: kswapd משתמש ביותר משאבי מעבד (CPU) כדי להחזיר כמות נתונה של זיכרון פנוי בהשוואה ל-lmkd, ולוקח לו יותר זמן להחזיר את הזיכרון הזה. ‫lmkd יכול לפנות כמויות גדולות של זיכרון בפרקי זמן קצרים ובמאמץ קטן יחסית. חשוב לזכור שבמכשירים ניידים, מאמץ של המעבד משפיע לא רק על זמן האחזור, אלא גם על צריכת הסוללה.

קטגוריות של ניקוד OOM

מערכת Android מקצה התאמה של ניקוד OOM‏ (oom_score_adj) לתהליכים על סמך החשיבות שלהם למשתמש. תהליכים עם ציונים גבוהים יותר נסגרים קודם.

דוגמאות לקטגוריות נפוצות:

  • שמורים במטמון (ציון >= 900): תהליכים שלא נדרשים כרגע, נשמרים כדי להפעיל מחדש מהר יותר.
  • שירות (ציון 500): תהליכים שמארחים שירותים שהופעלו.
  • שירות שפועל בחזית (ציון 200): תהליכים שמארחים שירותים שפועלים בחזית.
  • חזית (ציון 0): האפליקציה שהמשתמש נמצא איתה באינטראקציה כרגע.

מידע מעמיק: הפעלת PSI ו-LMKD

מה זה PSI?

‫Pressure Stall Information‏ (PSI) הוא תכונה של ליבת Linux שמספקת דרך קנונית למדוד מחסור במשאבים. הוא עוקב אחרי הזמן שבו משימות מתעכבות בגלל מחסור במעבד (CPU), בזיכרון ובקלט/פלט (I/O).

‫Memory PSI: "some" לעומת "full"

‫Memory PSI עוקב באופן ספציפי אחרי עיכובים שנגרמים בגלל מחסור בזיכרון (למשל, המתנה להחזרת דף או לשגיאות חוזרות). הוא כולל שני מדדים:

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

איך PSI מפעיל את LMKD

הדמון (daemon) של lmkd במרחב המשתמש נרשם בליבה כדי לקבל התראות כשחורגים מסף ה-PSI (על ידי מעקב אחרי /proc/pressure/memory). כשהמערכת חווה לחץ גבוה על הזיכרון (למשל, השהיה של full שחורגת מהמשך שהוגדר), הליבה מעירה את lmkd. לאחר מכן, lmkd מעריך את מצב המערכת ומחליט אם להפסיק תהליכים, ואילו תהליכים, על סמך הציונים והגודל שלהם ב-OOM.

כוונון LMKD

אפשר לכוונן את ההתנהגות של LMKD באמצעות מאפייני המערכת.

איך מוצאים את ערכי הכוונון במכשיר

כדי למצוא את ערכי הכוונון הנוכחיים של LMKD במכשיר מחובר, מריצים את הפקודה:

adb shell getprop | grep lmk

מאפיינים נפוצים של LMKD

אלה כמה מהמאפיינים העיקריים שמשמשים לכוונון LMKD (לרשימה המלאה, עיינו במסמכי התיעוד של Android):

  • ‫ro.lmk.use_psi: צריך להגדיר את הערך true כדי להשתמש במוניטורים של PSI לזיהוי עומס בזיכרון (ברירת המחדל ב-Android מגרסה 10 ואילך).
  • ‫ro.lmk.psi_partial_stall_ms: ערך הסף למשך הזמן של השהיה (חלקית) של some באלפיות השנייה, שגורם להפעלה של סגירה (ברירת המחדל היא 70 אלפיות השנייה במכשירים רגילים, ו-200 אלפיות השנייה במכשירים עם זיכרון RAM נמוך).
  • ro.lmk.psi_complete_stall_ms: ערך הסף של משך ההשהיה של full באלפיות השנייה להפעלת סגירה (ברירת המחדל היא 700 אלפיות השנייה).
  • ‫ro.lmk.kill_heaviest_task: אם הערך הוא true, ה-LMKD סוגר את התהליך שמשתמש בהכי הרבה זיכרון מבין המועמדים באותו קטגוריית OOM.

ערכי כוונון רלוונטיים במכשיר הבדיקה (Google Pixel 8 Pro)

(הערה: ערכי ברירת מחדל אופייניים למכשירים מתקדמים עם Android 15)

  • ro.lmk.use_psi: true
  • ‫ro.lmk.psi_partial_stall_ms: 70 (ערך טיפוסי)
  • ‫ro.lmk.psi_complete_stall_ms: 700 (ערך טיפוסי)
  • ro.lmk.kill_heaviest_task: true

אימות ZRAM והחלפה

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

# Check ZRAM sizing and usage
adb shell free -m

# Check swap partition details
adb shell cat /proc/swaps

תרגיל מעשי: הפעלת kswapd ו-lmkd

בתרגיל הזה תשתמשו באפליקציה לדוגמה KswapdLmkdLab כדי ליצור תהליכים בקטגוריות OOM שונות, ואז תצרכו זיכרון בתהליך הראשי כדי להפעיל את kswapd ו-lmkd.

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

1. יצירת תהליכי משנה

האפליקציה מפעילה באופן אוטומטי את תהליכי המשנה הבאים בזמן ההפעלה:

  • תהליך במטמון (oom_score_adj >= 900)
  • תהליך שירות (oom_score_adj 500)
  • תהליך של שירות שפועל בחזית (oom_score_adj 200)

מאמתים את התהליכים ואת ציוני ה-OOM שלהם באמצעות ADB:

adb shell dumpsys activity processes | grep com.android.kswapdlmkdlab

קטע קוד לדוגמה של פלט:

    Proc #  0: fg       T/A/TOP  LCMNFUATI  t: 0 18959:com.android.kswapdlmkdlab/u0a358 (top-activity)
    Proc #  1: prcp     b/ /FGS  -------TI  t: 0 19050:com.android.kswapdlmkdlab:foreground/u0a358 (fg-service)
    Proc #  2: svc      b/ /SVC  -------TI  t: 0 19077:com.android.kswapdlmkdlab:service/u0a358 (started-services)
    Proc #  3: cch      b/ /CACC -------TI  t: 0 19014:com.android.kswapdlmkdlab:cached/u0a358 (cch-client-act)

הקיצורים של מצבי התהליך תואמים ל:

  • ‫fg: חזית (ציון OOM‏ 0)
  • ‫prcp: ניתנת לתפיסה (שירות שפועל בחזית, ציון OOM‏ 200)
  • svc: שירות שהופעל (ניקוד OOM‏ 500)
  • ‫cch: תהליך שנשמר במטמון (ציון OOM‏ 900)

2. הפעלת לחץ על הזיכרון

האפליקציה תתחיל להקצות זיכרון באופן אוטומטי בתהליך הראשי. כדי שהבדיקה תהיה יעילה במכשירים עם זיכרון RAM גדול, האפליקציה גם מורה לתהליכי המשנה להקצות זיכרון (למשל, 500MB לכל אחד) כדי להפוך אותם ליעדים טובים יותר ל-LMKD.

מעקב אחרי סיום תהליך משנה

האפליקציה עוקבת אחרי הסגירה של תהליכי המשנה האלה באמצעות המנגנון IBinder.linkToDeath:

  1. התהליך הראשי נקשר לשירות בתהליך היעד באמצעות bindService.
  2. אחרי החיבור, הוא מקבל את האובייקט IBinder שמייצג את השירות.
  3. הוא רושם הטמעה של IBinder.DeathRecipient על ידי קריאה ל-linkToDeath ב-binder.
  4. כשהתהליך של היעד מסתיים (לדוגמה, כשהוא נסגר על ידי LMKD), המערכת קוראת ל-method‏ binderDied(), וכך האפליקציה יכולה לתעד את אירוע הסיום באופן אסינכרוני.

כדי למנוע מהקישור הזה להעלות את העדיפות של תהליכי היעד (מה שיפגע במטרת התרגיל), האפליקציה משתמשת בדגל Context.BIND_WAIVE_PRIORITY.

אפליקציה לדוגמה בפעולה

הנה צילום מסך של האפליקציה אחרי הרצת בדיקה ב-Google Pixel 8 Pro (עם זיכרון RAM כולל של כ-12GB).

צילום מסך של אפליקציה לדוגמה אחרי הרצת בדיקה שהושלמה.
איור 1. אפליקציה לדוגמה אחרי הרצת בדיקה ב-Pixel 8 Pro.

ניתוח נתוני מעקב של Perfetto

כדי לתעד את האינטראקציה, משתמשים בהגדרות הבאות של Perfetto כדי לתעד את התזמון, מוני הזיכרון, סגירות של LMK ומידע על עיכובים בגלל עומס (PSI).

הגדרת Perfetto

כדי לתעד את האינטראקציה, מריצים את Perfetto עם תזמון, מוני זיכרון, סגירות של LMK ומידע על השהיית לחץ (PSI):

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/kswapd_lmkd.perfetto-trace <<EOF
buffers: { size_kb: 131072 }
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "lowmemorykiller/lowmemory_kill"
            ftrace_events: "vmscan/mm_vmscan_kswapd_wake"
            ftrace_events: "vmscan/mm_vmscan_kswapd_sleep"
            ftrace_events: "psi/psi_event"
        }
    }
}
duration_ms: 180000
EOF

בדיקת המעקב

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

צילום מסך של ממשק המשתמש של Perfetto שבו רואים את הפעילות של kswapd ו-lmkd.
איור 2. מעקב Perfetto שמציג את החזרת kswapd והסגירות של lmkd.

אפשר לראות שהערך של MemFree כבר נמוך למדי ונמצא תחת לחץ מתמיד. ‫kswapd פועל כל הזמן ומפחית את הלחץ על ידי הסרת mem.rss.file דפים והעברת mem.rss.anon אל mem.swap מכל התהליכים של kswapdlmkdlab (התהליך הראשי ותהליכי המשנה), וכנראה גם מתהליכים אחרים שלא מוצגים בצילום המסך הזה. במקביל, האפליקציה kswapdlmkdlab ממשיכה להקצות זיכרון, ומגבירה את הלחץ מהר יותר מהקצב שבו kswapd יכולה להחזיר את הזיכרון.

כשהלחץ גבוה מדי, כמו במצב שבו psi.mem גבוה למשך זמן מספיק, lmkd מתעורר והורג כמה תהליכים. אפשר לראות ש-lmkd פועל לפי ציוני ה-OOM, ומפסיק קודם את kswapdlmkdlab:cached, אחר כך את :service ואז את :foreground.

בודקים את ציר הזמן של האירועים וחושבים על הדברים הבאים:

  • איך kswapd ו-lmkd משלימים זה את זה: kswapd הוא הראשון שמגיב ללחץ על הזיכרון, אבל lmkd עוזר כשרוחב הפס להחזרה של kswapd לא עומד בדרישת ההקצאה.
  • קצב פינוי הזיכרון: בודקים את זמן המעבד (CPU) שמושקע ב-kswapd ואת הזיכרון שמתפנה על ידי השמטת mem.rss.file או החלפת mem.rss.anon ב-mem.swap. אפשר להשוות את זה לפעילות הקצרה והאינטנסיבית ב-lmkd, שגורמת לשחרור כמויות גדולות של זיכרון פרטי של תהליכים.

שאילתות

כדי לחלץ נתונים מהמעקב, מריצים את השאילתות האלה בממשק המשתמש של Perfetto (שאילתת SQL).

‫1. פעילות kswapd

כדי לראות מתי ה-swap daemon של הליבה היה פעיל ופעל, מריצים את הפקודה:

SELECT ts, dur, state
FROM thread_state
JOIN thread USING (utid)
WHERE thread.name LIKE 'kswapd%'
  AND state = 'Running'
LIMIT 5;

פלט לדוגמה:

ts dur הסמוי הסופי
30317633052969 159180 פועל
30317633398347 3999186 פועל
30317638448111 1847941 פועל
30317641455354 1031006 פועל
30317643272615 888387 פועל

2. סגירות של LMKD

‫LMKD כותב אירועי LMK בטבלה instant. כדי לשלוח שאילתה על כל הפעולות של LMKD:

SELECT ts, name
FROM instant
WHERE name LIKE 'lmk,%'
ORDER BY ts ASC;

הפורמט של שם האירוע הוא: lmk,<pid>,<kill_reason>,<oom_score_adj>,<min_oom_score>,<max_thrashing>.

  • ‫kill_reason: מספר שלם שמציין למה lmkd החליט להפסיק את התהליך (מוגדר ב-system/memory/lmkd/statslog.h):
    • 0: PRESSURE_AFTER_KILL
    • 1: NOT_RESPONDING
    • ‫2: LOW_SWAP_AND_THRASHING (שטח ההחלפה של ZRAM נמוך מאוד והתרחשה תופעת Thrashing)
    • ‫3: LOW_MEM_AND_SWAP (במערכת יש מעט זיכרון פנוי וגם מעט זיכרון החלפה ב-ZRAM)
    • 4: LOW_MEM_AND_THRASHING
    • 5: ‏DIRECT_RECL_AND_THRASHING
    • 6: ‏LOW_MEM_AND_SWAP_UTIL
    • 7: ‏LOW_FILECACHE_AFTER_THRASHING
    • 8: ‏LOW_MEM
    • 9: DIRECT_RECL_STUCK
  • ‫max_thrashing: האחוז המקסימלי של דפים שמועברים בין הזיכרון לדיסק בזמן הסגירה.

פלט לדוגמה:

ts שם
30326148679967 lmk,2308,3,900,201,0
30330439763710 lmk,2309,3,500,201,0
30334985999837 lmk,2310,2,200,0,307

המחשה

כדי להמחיש את הנתונים, אתם יכולים להשתמש בסקריפט gnuplot הבא כדי להציג את הזיכרון הפנוי במערכת, את הפעילות של kswapd, את ההריגות של lmkd ואת ה-PSI בסדרת זמן אחת. הסקריפט מניח שהוא מופעל מהספרייה העליונה של ה-build של Android.

תרשים פלט לדוגמה

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

תרשים של זיכרון פנוי, פעילות kswapd,‏ PSI וסגירות של lmkd.
איור 3. תרשים של עומס הזיכרון שבו מוצגים הזיכרון הפנוי, kswapd,‏ PSI וסגירות של lmkd.

הסבר על התרשים:

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

  1. ירידה בזיכרון הפנוי: ככל שהאפליקציה הראשית מקצה זיכרון, הזיכרון הפנוי במערכת (הקו הסגול) יורד בהדרגה.
  2. הפעלה של kswapd: כשהזיכרון הפנוי יורד מתחת לסימני מים מסוימים, kswapd מתעורר (פולסים כחולים) כדי לפנות זיכרון על ידי החלפה ל-ZRAM. התופעה הזו נראית כמו קפיצות לסירוגין.
  3. קפיצות ב-PSI: כשהתהליך kswapd מתקשה לעמוד בקצב הגבוה של ההקצאה, השרשורים מתחילים להיתקע בהקצאת הזיכרון, מה שגורם לקפיצות ב-PSI (מידע על עיכובים בגלל עומס) (הקו הירוק).
  4. סגירת תהליכים על ידי LMKD: כשמדד ה-PSI מגיע לרמות קריטיות (למשל, חריגה מהסף של ro.lmk.psi_complete_stall_ms) והזיכרון הפנוי נמוך מאוד, lmkd מתערב וסוגר תהליכים בעדיפות נמוכה (הקווים הכתומים בזמנים 30 ו-45) כדי לפנות חלקים גדולים מהזיכרון. שימו לב שמיד אחרי סגירת תהליך, הזיכרון הפנוי עולה וה-PSI יורד, מה שמראה שהסגירה יעילה בהפחתת הלחץ. ________________________________________________________________________________

← החזרת זיכרון: פינוי והחלפה | ↑ למעלה | החזרת דפי זיכרון למערכת ההפעלה →