אופטימיזציה של זמן האתחול

בדף הזה מפורטים טיפים לשיפור זמן האתחול.

הסרת סמלי ניפוי באגים מהמודולים

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

  • הזמן שלוקח לקרוא את הקבצים הבינאריים מהפלאש.
  • הזמן שלוקח לבצע דחיסה לאחור של ה-ramdisk.
  • הזמן שלוקח לטעון את המודולים.

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

הסרת הסמלים מופעלת כברירת מחדל ב-build של פלטפורמת Android, אבל כדי להפעיל אותה באופן מפורש, צריך להגדיר את BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES בתצורה הספציפית למכשיר בקטע device/vendor/device.

שימוש בדחיסת LZ4 לליבת המעבד ול-ramdisk

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

נוספה תמיכה בדחיסה של LZ4 ב-ramdisk לגרסה היציבה של פלטפורמת Android דרך BOARD_RAMDISK_USE_LZ4. אפשר להגדיר את האפשרות הזו בתצורה הספציפית למכשיר. אפשר להגדיר דחיסת ליבה דרך kernel defconfig.

המעבר ל-LZ4 אמור להביא לקיצור זמן האתחול ב-500 אלפיות השנייה עד 1,000 אלפיות השנייה.

הימנעות מרישום מוגזם ביומן בדרייברים

ב-ARM64 וב-ARM32, כדי לקודד את כתובת הקפיצה המלאה, קריאות לפונקציות שנמצאות במרחק מסוים מאתר הקריאה צריכות טבלת קפיצה (שנקראת טבלת קישור של פרוצדורה, או PLT). מאחר שהמודולים נטענים באופן דינמי, צריך לתקן את טבלאות הקפיצה האלה במהלך טעינת המודול. הקריאות שצריך להעביר נקרא רשומות העברה עם ערכים מפורשים (או RELA בקיצור) בפורמט ELF.

ליבה של Linux מבצעת אופטימיזציה של גודל הזיכרון (כמו אופטימיזציה של היטים במטמון) בזמן הקצאת ה-PLT. בעקבות ההתחייבות הזו, מורכבות תוכנית האופטימיזציה היא O(N^2), כאשר N הוא מספר ה-RELA מסוג R_AARCH64_JUMP26 או R_AARCH64_CALL26. לכן, כדאי להשתמש בפחות RELAs מהסוגים האלה כדי לקצר את זמן הטעינה של המודול.

דפוס תכנות נפוץ אחד שמגדיל את מספר ה-RELA של R_AARCH64_CALL26 או R_AARCH64_JUMP26 הוא רישום יתר ביומן ב-driver. בדרך כלל, כל קריאה ל-printk() או לכל סכימה אחרת של רישום ביומן מוסיפה רשומת RELA מסוג CALL26/JUMP26. בטקסט השמירה ב-commit של ה-upstream, שימו לב שלמרות האופטימיזציה, הטעינה של ששת המודולים נמשכת כ-250 אלפיות השנייה. הסיבה לכך היא שששת המודולים האלה היו ששת המודולים המובילים עם כמות הנתונים הגדולה ביותר ביומן.

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

הפעלת בדיקה אסינכרונית באופן סלקטיבי

כשמודול נטען, אם המכשיר שהוא תומך בו כבר מאוכלס מ-DT (devicetree) ונוסף לליבת הנהג, בדיקת המכשיר מתבצעת בהקשר של הקריאה ל-module_init(). כשבודקים מכשיר בהקשר של module_init(), הטעינה של המודול לא יכולה להסתיים עד שהבדיקה מסתיימת. מכיוון שהטעינה של המודולים מתבצעת בעיקר בסדרה, מכשיר שלוקח לו זמן רב יחסית לבצע בדיקה מאט את זמן האתחול.

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

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

כדי להפעיל בדיקה אסינכרונית של מודול, לא מספיק להגדיר רק את הדגל PROBE_PREFER_ASYNCHRONOUS בקוד הנהג. במודולים, צריך גם להוסיף את module_name.async_probe=1 בשורת הפקודה של הליבה או להעביר את async_probe=1 כפרמטר של מודול בזמן טעינת המודול באמצעות modprobe או insmod.

הפעלת בדיקה אסינכרונית יכולה לחסוך בערך 100-500 אלפיות השנייה בזמני האתחול, בהתאם לחומרה או לדרייברים שלכם.

בדיקה של מנהל ההתקן של CPUfreq מוקדם ככל האפשר

ככל שהבדיקות של מנהל ההתקן CPUfreq יתבצעו מוקדם יותר, כך תוכלו לשנות את תדר המעבד למקסימום (או למקסימום מוגבל מבחינה תרמית) במהלך האתחול. ככל שהמעבד מהיר יותר, כך האתחול מהיר יותר. ההנחיה הזו חלה גם על devfreq דרייברים ששולטים בתדירות של ה-DRAM, הזיכרון והחיבור המחבר.

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

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

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

העברת מודולים למחיצה של שלב שני של init, של ספק או של vendor_dlkm

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

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

טוענים את מנהלי ההתקנים החיוניים הבאים:

  • watchdog
  • reset
  • cpufreq

בשלב הראשון של תהליך האיניטיאל במצב fastbootd של התאוששות ומרחב המשתמש, נדרשים יותר מכשירים לבדיקה (כמו USB) ולתצוגה. שומרים עותק של המודולים האלה ב-ramdisk של השלב הראשון ובמחיצה של הספק או vendor_dlkm. כך אפשר לטעון אותם בשלב הראשון של האינטליגנציה המלאכותית לשחזור או לתהליך האתחול של fastbootd. עם זאת, אין לטעון את המודולים של מצב השחזור בשלב הראשון של האינטליגנציה במהלך תהליך האתחול הרגיל. אפשר לדחות את המודול של מצב השחזור ל-init של השלב השני כדי לקצר את זמן האתחול. כל שאר המודולים שלא נדרשים בשלב הראשון של האינטליגנציה צריכים להיות מועברים למחיצה של הספק או למחיצה vendor_dlkm.

כשנותנים ל-script dev needs.sh רשימה של מכשירי עלים (למשל UFS או Serial), הוא מוצא את כל מנהלי ההתקנים, המכשירים והמודולים הנדרשים לבדיקה של יחסי התלות או של הספקים (למשל שעונים, רגולטורים או gpio).

העברת מודולים ל-init בשלב השני מקצרת את זמני האתחול בדרכים הבאות:

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

העברת מודולים לשלב השני יכולה לחסוך 500 עד 1,000 אלפיות שנייה בזמני האתחול, בהתאם למספר המודולים שאפשר להעביר ל-init של השלב השני.

לוגיסטיקה של טעינה של מודולים

ב-build העדכני ביותר של Android יש הגדרות של לוח הבקרה ששולטות במודולים שמעתיקים לכל שלב ובמודולים שמטעינים. הקטע הזה מתמקד בקבוצת המשנה הבאה:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES. רשימת המודולים שרוצים להעתיק ל-ramdisk.
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD. רשימת המודולים שצריך לטעון בשלב הראשון של האינטליגנציה המלאכותית.
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD. זוהי רשימת המודולים שצריך לטעון כשבוחרים באפשרות 'שחזור' או ב-fastbootd מה-ramdisk.
  • BOARD_VENDOR_KERNEL_MODULES. רשימת המודולים שצריך להעתיק למחיצה של הספק או של vendor_dlkm בספרייה /vendor/lib/modules/.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD. רשימת המודולים שצריך לטעון בשלב השני של האינטליגנציה המלאכותית.

צריך להעתיק גם את המודולים של האתחול והשחזור ב-ramdisk למחיצה של הספק או של vendor_dlkm‏ ב-/vendor/lib/modules. העתקת המודולים האלה למחיצה של הספק מבטיחה שהמודולים לא יהיו בלתי נראים במהלך ההפעלה של השלב השני, וזה שימושי לניפוי באגים ולאיסוף modinfo לדיווחים על באגים.

הכפילות אמורה לדרוש שטח מינימלי במחיצה של הספק או של vendor_dlkm, כל עוד קבוצת המודולים של האתחול מוקטנת. מוודאים שבקובץ modules.list של הספק יש רשימה מסוננת של מודולים ב-/vendor/lib/modules. הרשימה המסוננת מבטיחה שזמני האתחול לא יושפעו מהטעינה מחדש של המודולים (תהליך יקר).

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

אפשר להשתמש בקבצים Board.Config.mk של המכשיר כדי לבצע את הפעולות האלה, כפי שמתואר בדוגמה הבאה:

# All kernel modules
KERNEL_MODULES := $(wildcard $(KERNEL_MODULE_DIR)/*.ko)
KERNEL_MODULES_LOAD := $(strip $(shell cat $(KERNEL_MODULE_DIR)/modules.load)

# First stage ramdisk modules
BOOT_KERNEL_MODULES_FILTER := $(foreach m,$(BOOT_KERNEL_MODULES),%/$(m))

# Recovery ramdisk modules
RECOVERY_KERNEL_MODULES_FILTER := $(foreach m,$(RECOVERY_KERNEL_MODULES),%/$(m))
BOARD_VENDOR_RAMDISK_KERNEL_MODULES += \
     $(filter $(BOOT_KERNEL_MODULES_FILTER) \
                $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES))

# ALL modules land in /vendor/lib/modules so they could be rmmod/insmod'd,
# and modules.list actually limits us to the ones we intend to load.
BOARD_VENDOR_KERNEL_MODULES := $(KERNEL_MODULES)
# To limit /vendor/lib/modules to just the ones loaded, use:
# BOARD_VENDOR_KERNEL_MODULES := $(filter-out \
#     $(BOOT_KERNEL_MODULES_FILTER),$(KERNEL_MODULES))

# Group set of /vendor/lib/modules loading order to recovery modules first,
# then remainder, subtracting both recovery and boot modules which are loaded
# already.
BOARD_VENDOR_KERNEL_MODULES_LOAD := \
        $(filter-out $(BOOT_KERNEL_MODULES_FILTER), \
        $(filter $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD)))
BOARD_VENDOR_KERNEL_MODULES_LOAD += \
        $(filter-out $(BOOT_KERNEL_MODULES_FILTER) \
            $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD))

# NB: Load order governed by modules.load and not by $(BOOT_KERNEL_MODULES)
BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD := \
        $(filter $(BOOT_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD))

# Group set of /vendor/lib/modules loading order to boot modules first,
# then the remainder of recovery modules.
BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD := \
    $(filter $(BOOT_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD))
BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD += \
    $(filter-out $(BOOT_KERNEL_MODULES_FILTER), \
    $(filter $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD)))

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

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

אפשר להתעלם מכשל בטעינת מודול לניפוי באגים שלא קיים ב-builds של משתמשים. כדי להתעלם מהכשל הזה, מגדירים את המאפיין vendor.device.modules.ready כך שיפעיל שלבים מאוחרים יותר של תהליך האתחול של הסקריפטים של init rc כדי להמשיך למסך ההפעלה. אם הקוד הבא נמצא ב-/vendor/etc/init.insmod.sh:

#!/vendor/bin/sh
. . .
if [ $# -eq 1 ]; then
  cfg_file=$1
else
  # Set property even if there is no insmod config
  # to unblock early-boot trigger
  setprop vendor.common.modules.ready
  setprop vendor.device.modules.ready
  exit 1
fi

if [ -f $cfg_file ]; then
  while IFS="|" read -r action arg
  do
    case $action in
      "insmod") insmod $arg ;;
      "setprop") setprop $arg 1 ;;
      "enable") echo 1 > $arg ;;
      "modprobe") modprobe -a -d /vendor/lib/modules $arg ;;
     . . .
    esac
  done < $cfg_file
fi

בקובץ ה-rc של החומרה, אפשר לציין את השירות one shot באמצעות:

service insmod-sh /vendor/etc/init.insmod.sh /vendor/etc/init.insmod.<hw>.cfg
    class main
    user root
    group root system
    Disabled
    oneshot

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

כדי לשפר את זמני האתחול, אפשר לבחור מודולים ספציפיים בשירות הטעינה של המודולים שמתאימים יותר לטעינה אחרי מסך ההפעלה. לדוגמה, אפשר לטעון מאוחר באופן מפורש את המודולים של ממיר הווידאו או ה-Wi-Fi אחרי שהסתיים תהליך האתחול של init (למשל, sys.boot_complete, אות של נכס Android). חשוב לוודא שה-HALs של המודולים שמופעלים בשלב מאוחר יותר חוסמים מספיק זמן כשמנגיני הליבה לא נמצאים.

לחלופין, אפשר להשתמש בפקודה wait<file>[<timeout>] של init בסקריפטים של תהליך האתחול כדי להמתין לרשאות sysfs נבחרות שיציגו שהמודולים של הנהגים השלימו את פעולות הבדיקה. דוגמה לכך היא המתנה להשלמת הטעינה של מנהל התצוגה ברקע של תהליך השחזור או של fastbootd, לפני הצגת הגרפיקה של התפריט.

איפוס תדר המעבד לערך סביר בתוכנת האתחול

יכול להיות שלא כל ה-SoC או המוצרים יוכלו להפעיל את המעבד בתדר הגבוה ביותר בגלל בעיות חום או חשמל במהלך בדיקות לולאת האתחול. עם זאת, חשוב לוודא שה-bootloader מגדיר את התדירות של כל מעבדי ה-CPU במצב אונליין למהירות הגבוהה ביותר שאפשר ללא סיכון למוצר או ל-SoC. זה חשוב מאוד כי בליבה מודולרית לחלוטין, הדחיסה של init ramdisk מתרחשת לפני שאפשר לטעון את מנהל ה-CPUfreq. לכן, אם מעבד ה-CPU נשאר בקצה התחתון של התדירות שלו על ידי מנהל האתחול, זמן הדחיסה של ה-ramdisk עשוי להימשך זמן רב יותר מאשר ליבה שעבר הידור סטטי (אחרי התאמה להבדל בגודל ה-ramdisk), כי תדירות ה-CPU תהיה נמוכה מאוד כשמבצעים עבודה שמתבצעת בעיקר על ידי המעבד (דחיסה). אותו עיקרון חל על תדירות הזיכרון ועל תדירות החיבורים.

איפוס תדר המעבד של מעבדים גדולים בתוכנת האתחול

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

חשוב לוודא שהביצועים של המעבדים הגדולים טובים לפחות כמו הביצועים של המעבדים הקטנים בתדירות שבה מנהל האתחול משאיר אותם במצב מופעל. לדוגמה, אם הביצועים של המעבד הגדול טובים פי 2 מאלה של המעבד הקטן באותה תדר, אבל bootloader מגדיר את התדר של המעבד הקטן ל-1.5GHz ואת התדר של המעבד הגדול ל-300MHz, ביצועי האתחול ירדו אם הליבה תעביר שרשור למעבד הגדול. בדוגמה הזו, אם בטוח להפעיל את המעבד הגדול במהירות של 750MHz, כדאי לעשות זאת גם אם לא מתכננים להשתמש בו באופן מפורש.

נהגים לא צריכים לטעון קושחה בשלב הראשון של האתחול

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