עדכוני מערכת מסוג A/B (חלקים)

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

מידע נוסף על עדכוני מערכת A/B ועל אופן הפעולה שלהם זמין במאמר בנושא בחירת מחיצה (משבצות).

עדכוני מערכת מסוג A/B מספקים את היתרונות הבאים:

  • עדכוני OTA יכולים להתבצע בזמן שהמערכת פועלת, בלי להפריע למשתמש. המשתמשים יכולים להמשיך להשתמש במכשירים שלהם במהלך עדכון OTA. זמן ההשבתה היחיד במהלך עדכון הוא כשהמכשיר מופעל מחדש למחיצת הדיסק המעודכנת.
  • אחרי עדכון, הפעלה מחדש לא נמשכת יותר זמן מהפעלה מחדש רגילה.
  • אם עדכון OTA לא יוחל (למשל, בגלל פלאש פגום), לא תהיה השפעה על המשתמש. המשתמש ימשיך להפעיל את מערכת ההפעלה הישנה, והלקוח יוכל לנסות שוב לבצע את העדכון.
  • אם מוחל עדכון OTA אבל המכשיר לא מצליח לאתחל, הוא יופעל מחדש בחזרה למחיצה הישנה ויישארו אפשרויות שימוש. הלקוח יכול לנסות שוב לעדכן.
  • שגיאות (כמו שגיאות קלט/פלט) משפיעות רק על קבוצת המחיצות שלא נעשה בה שימוש, ואפשר לנסות שוב. בנוסף, הסיכוי לשגיאות כאלה קטן יותר כי עומס הקלט/פלט נמוך בכוונה כדי למנוע פגיעה בחוויית המשתמש.
  • אפשר להזרים עדכונים למכשירי A/B, כך שלא צריך להוריד את החבילה לפני ההתקנה. סטרימינג אומר שהמשתמש לא צריך שיהיה לו מספיק מקום פנוי כדי לאחסן את חבילת העדכון ב-/data או ב-/cache.
  • מחיצת המטמון לא משמשת יותר לאחסון חבילות עדכון OTA, ולכן אין צורך לוודא שמחיצת המטמון גדולה מספיק לעדכונים עתידיים.
  • dm-verity מבטיח שהמכשיר יפעיל תמונה לא פגומה. אם מכשיר לא מופעל בגלל בעיה ב-OTA או ב-dm-verity, המכשיר יכול להפעיל מחדש קובץ אימג' ישן. ‫(Android Verified Boot לא דורש עדכוני A/B )

מידע על עדכוני מערכת מסוג A/B

עדכוני A/B מחייבים שינויים גם בלקוח וגם במערכת. אבל לא צריך לבצע שינויים בשרת חבילות ה-OTA: חבילות העדכון עדיין מוגשות באמצעות HTTPS. במכשירים שמשתמשים בתשתית OTA של Google, כל שינויי המערכת נמצאים ב-AOSP, וקוד הלקוח מסופק על ידי Google Play Services. יצרני ציוד מקורי שלא משתמשים בתשתית OTA של Google יוכלו לעשות שימוש חוזר בקוד המערכת של AOSP, אבל יצטרכו לספק לקוח משלהם.

יצרני ציוד מקורי (OEM) שמספקים לקוח משלהם צריכים לוודא שהלקוח:

  • החלטה מתי לבצע עדכון. עדכוני A/B מתבצעים ברקע, ולכן הם לא מתבצעים יותר על ידי המשתמש. כדי לא לשבש את הפעילות של המשתמשים, מומלץ לתזמן את העדכונים לזמן שבו המכשיר נמצא במצב תחזוקה לא פעיל, למשל בלילה, ושהוא מחובר ל-Wi-Fi. עם זאת, הלקוח יכול להשתמש בכל היוריסטיקה שתרצו.
  • בודקים בשרתי חבילות ה-OTA אם יש עדכון זמין. הקוד הזה צריך להיות דומה לקוד הלקוח הקיים, אבל צריך לציין שהמכשיר תומך ב-A/B. (הלקוח של Google כולל גם את הלחצן בדיקה עכשיו שמאפשר למשתמשים לבדוק אם יש עדכון חדש).
  • מתקשרים אל update_engine עם כתובת ה-URL מסוג HTTPS של חבילת העדכון, אם היא זמינה. ‫update_engine יעודכן את הבלוקים הגולמיים במחיצה שלא נמצאת בשימוש כרגע, בזמן שהעדכון יועבר בסטרימינג.
  • דיווח על הצלחות או על כשלים בהתקנה לשרתים, על סמך קוד התוצאה update_engine. אם העדכון יוחל בהצלחה, ‫update_engine יורה לטוען האתחול לבצע אתחול למערכת ההפעלה החדשה בפעם הבאה שבה מתבצעת הפעלה מחדש. אם מערכת ההפעלה החדשה לא תצליח לבצע אתחול, טוען האתחול יחזור למערכת ההפעלה הישנה, כך שהלקוח לא צריך לעשות כלום. אם העדכון נכשל, הלקוח צריך להחליט מתי (ואם) לנסות שוב, על סמך קוד השגיאה המפורט. לדוגמה, לקוח טוב יכול לזהות שחבילת OTA חלקית ("diff") נכשלה ולנסות חבילת OTA מלאה במקום.

אופציונלי, הלקוח יכול:

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

בצד המערכת, עדכוני מערכת A/B משפיעים על הדברים הבאים:

  • בחירת מחיצה (משבצות), ה-daemon‏ update_engine ואינטראקציות של bootloader (מתואר בהמשך)
  • תהליך הבנייה ויצירת חבילת עדכון OTA (מתואר במאמר בנושא הטמעה של עדכוני A/B)

בחירת מחיצה (משבצות)

עדכוני מערכת A/B משתמשים בשני סטים של מחיצות שנקראות slots (בדרך כלל slot A ו-slot B). המערכת פועלת מהמשבצת הנוכחית, ואילו המחיצות במשבצת שלא בשימוש לא נגישות למערכת הפועלת במהלך פעולה רגילה. הגישה הזו מאפשרת לעדכונים להיות עמידים בפני תקלות, כי המשבצת שלא בשימוש משמשת כגיבוי: אם מתרחשת שגיאה במהלך עדכון או מיד אחריו, המערכת יכולה לחזור למשבצת הישנה ולהמשיך לפעול. כדי להשיג את המטרה הזו, לא צריך לעדכן אף מחיצה שמשמשת את המשבצת הנוכחית כחלק מעדכון ה-OTA (כולל מחיצות שיש להן רק עותק אחד).

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

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

עדכון של דמון המנוע

עדכוני מערכת A/B משתמשים בדימון (daemon) ברקע שנקרא update_engine כדי להכין את המערכת לאתחול לגרסה חדשה ומעודכנת. הדמון הזה יכול לבצע את הפעולות הבאות:

  • קריאה ממחיצות A/B של המשבצת הנוכחית וכתיבת נתונים למחיצות A/B של המשבצת שלא נעשה בה שימוש, בהתאם להוראות בחבילת ה-OTA.
  • מפעילים את הממשק boot_control בתהליך עבודה מוגדר מראש.
  • מריצים תוכנית post-install ממחיצת new אחרי כתיבת כל מחיצות הסלוטים שלא נעשה בהן שימוש, כמו שמוסבר בחבילת ה-OTA. (פרטים נוספים מופיעים במאמר בנושא הגדרה לאחר ההתקנה).

הדמון update_engine לא מעורב בתהליך האתחול עצמו, ולכן היכולות שלו במהלך עדכון מוגבלות על ידי המדיניות והתכונות של SELinux במשבצת הנוכחית (אי אפשר לעדכן את המדיניות והתכונות האלה עד שהמערכת תאותחל לגרסה חדשה). כדי לשמור על מערכת חזקה, תהליך העדכון לא צריך לשנות את טבלת המחיצות, את התוכן של המחיצות במשבצת הנוכחית או את התוכן של מחיצות שאינן A/B שלא ניתן למחוק באמצעות איפוס להגדרות המקוריות.

עדכון מקור המנוע

מקור update_engine נמצא ב-system/update_engine. קובצי ה-dexopt של A/B OTA מפוצלים בין installd לבין מנהל חבילות:

דוגמה שעובדת זמינה בכתובת /device/google/marlin/device-common.mk.

עדכון יומני המנוע

בגרסאות Android 8.x ומטה, אפשר למצוא את היומנים update_engine ב-logcat ובדוח הבאגים. כדי להפוך את היומנים של update_engine לזמינים במערכת הקבצים, צריך להוסיף את השינויים הבאים ל-build:

השינויים האלה שומרים עותק של יומן update_engine האחרון ב-/data/misc/update_engine_log/update_engine.YEAR-TIME. בנוסף ליומן הנוכחי, חמשת היומנים האחרונים נשמרים בתיקייה /data/misc/update_engine_log/. משתמשים עם מזהה הקבוצה log יוכלו לגשת ליומנים של מערכת הקבצים.

אינטראקציות עם תוכנת האתחול

רכיב ה-HAL‏ boot_control משמש את update_engine (ואולי דמונים אחרים) כדי להנחות את תוכנת האתחול מאיפה לבצע את האתחול. דוגמאות נפוצות לתרחישים ולמצבים שקשורים אליהם:

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

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

לא תמיד יש מספיק מקום במכשירי המשתמשים ב-/data כדי להוריד את חבילת העדכון. מכיוון שיצרני ציוד מקורי ומשתמשים לא רוצים לבזבז מקום במחיצה של /cache, חלק מהמשתמשים לא מקבלים עדכונים כי אין במכשיר מקום לאחסון חבילת העדכון. כדי לפתור את הבעיה הזו, ב-Android 8.0 נוסף תמיכה בעדכוני A/B בסטרימינג שכותבים בלוקים ישירות למחיצה B בזמן ההורדה, בלי לאחסן את הבלוקים ב-/data. עדכוני A/B בסטרימינג כמעט לא דורשים אחסון זמני, ונדרש אחסון רק לכ-100KB של מטא-נתונים.

כדי להפעיל עדכונים בסטרימינג ב-Android 7.1, צריך לבחור את התיקונים הבאים:

התיקונים האלה נדרשים כדי לתמוך בעדכוני A/B של סטרימינג ב-Android מגרסה 7.1 ואילך, בין אם משתמשים ב-Google Mobile Services‏ (GMS) או בכל לקוח עדכון אחר.

מחזור החיים של עדכון בדיקת A/B

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

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

אחרי שמטען ייעודי זמין, תהליך העדכון מתבצע באופן הבא:

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

מטען העדכון הוא blob אטום עם ההוראות לעדכון לגרסה החדשה. המטען הייעודי (payload) של העדכון כולל את הרכיבים הבאים:
  • מטא-נתונים. המטא-נתונים הם חלק קטן יחסית ממטען הייעודי (Payload) של העדכון, והם מכילים רשימה של פעולות ליצירה ולאימות של הגרסה החדשה במשבצת היעד. לדוגמה, פעולה יכולה להיות פתיחת דחיסה של blob מסוים וכתיבתו לבלוקים ספציפיים במחיצת יעד, או קריאה ממחיצת מקור, החלת תיקון בינארי וכתיבה לבלוקים מסוימים במחיצת יעד.
  • נתונים נוספים. ברוב המקרים, המידע הנוסף שמשויך לפעולות האלה הוא ה-blob הדחוס או התיקון הבינארי בדוגמאות האלה, והוא מהווה את רוב מטען העדכון.
3 המטא-נתונים של המטען הייעודי (Payload) יורדים.
4 לכל פעולה שמוגדרת במטא-נתונים, הנתונים המשויכים (אם יש כאלה) מורדים לזיכרון, הפעולה מוחלת והזיכרון המשויך מושמד.
5 המחיצות כולן נקראות מחדש ומאומתות מול הגיבוב הצפוי.
6 שלב ההתקנה (אם קיים) מופעל. במקרה של שגיאה במהלך הביצוע של אחד מהשלבים, העדכון נכשל ומבוצע ניסיון חוזר, אולי עם מטען ייעודי (payload) שונה. אם כל השלבים עד עכשיו בוצעו בהצלחה, העדכון יצליח והשלב האחרון יבוצע.
7 המשבצת שלא נעשה בה שימוש מסומנת כפעילה על ידי קריאה ל-setActiveBootSlot(). סימון המשבצת שלא בשימוש כפעילה לא אומר שההפעלה תסתיים. תוכנת האתחול (או המערכת עצמה) יכולה להחזיר את החריץ הפעיל אם היא לא קוראת מצב של הצלחה.
8 בשלב שאחרי ההתקנה (שמתואר בהמשך) מריצים תוכנה מגרסת 'העדכון החדש' בזמן שהגרסה הישנה עדיין פועלת. אם השלב הזה מוגדר בחבילת ה-OTA, הוא חובה והתוכנית צריכה להחזיר את קוד היציאה 0. אחרת, העדכון ייכשל.
9 אחרי שהמערכת מופעלת בהצלחה בחריץ החדש ומסיימת את הבדיקות אחרי ההפעלה מחדש, החריץ הנוכחי (שנקרא בעבר 'חריץ היעד') מסומן כהצלחה על ידי קריאה ל-markBootSuccessful().

אחרי ההתקנה

לכל מחיצה שמוגדר בה שלב אחרי ההתקנה, ‫update_engine מעלה את המחיצה החדשה למיקום ספציפי ומריץ את התוכנית שצוינה ב-OTA ביחס למחיצה שהועלתה. לדוגמה, אם התוכנית להפעלה אחרי ההתקנה מוגדרת כ-usr/bin/postinstall במחיצת המערכת, המחיצה הזו מהמשבצת שלא נעשה בה שימוש תותקן במיקום קבוע (כמו /postinstall_mount) והפקודה /postinstall_mount/usr/bin/postinstall תופעל.

כדי שההתקנה תתבצע בהצלחה, ליבת המערכת הישנה צריכה להיות מסוגלת:

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

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

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

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

אחרי הפעלה מחדש

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

update_verifier יקרא רק את הבלוקים שמפורטים ב-/data/ota_package/care_map.txt, שנכלל בחבילת A/B OTA כשמשתמשים בקוד AOSP. לקוח עדכון המערכת של Java, כמו GmsCore, מחלץ את care_map.txt, מגדיר את הרשאת הגישה לפני הפעלה מחדש של המכשיר ומוחק את הקובץ המחולץ אחרי שהמערכת מאותחלת בהצלחה לגרסה החדשה.