עדכוני מערכת מסוג 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, התכונה הפעלה מאומתת לא דורשת עדכוני 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 משפיעים על הדברים הבאים:

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

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

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

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

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

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

עדכוני מערכת 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 לנייד (GMS) או בכל לקוח עדכון אחר.

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

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

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

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

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

מטען העדכון הוא blob אטום עם ההוראות לעדכון לגרסה החדשה. המטען הייעודי (payload) של העדכון כולל את הרכיבים הבאים:
  • מטא-נתונים. חלק קטן יחסית ממטען העדכון, המטא-נתונים, מכיל רשימה של פעולות ליצירה ולאימות של הגרסה החדשה במשבצת היעד. לדוגמה, פעולה יכולה להיות פתיחת דחיסה של blob מסוים וכתיבתו לבלוקים ספציפיים במחיצת יעד, או קריאה ממחיצת מקור, החלת תיקון בינארי וכתיבה לבלוקים מסוימים במחיצת יעד.
  • נתונים נוספים. הנתונים הנוספים שמשויכים לפעולות האלה הם חלק גדול ממטען העדכון, והם מורכבים מ-blob דחוס או מתיקון בינארי בדוגמאות האלה.
3 המטא-נתונים של מטען הייעודי הורדו.
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 (Executable and Linkable Format), הוא צריך להיות תואם לליבת המערכת הישנה (לדוגמה, תוכנית חדשה של 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, מגדיר את הרשאת הגישה לפני הפעלה מחדש של המכשיר ומוחק את הקובץ שחולץ אחרי שהמערכת מאותחלת בהצלחה לגרסה החדשה.