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

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

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

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

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

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

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

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

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

הלקוח יכול גם:

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

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

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

בחירת מחיצה (יחידות קיבולת)

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

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

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

עדכון הדימון של מנוע החיפוש

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

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

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

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

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

דוגמה שעובדת מופיעה בקובץ /device/google/marlin/device-common.mk.

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

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

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

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

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

  • מקרה רגיל: המערכת פועלת מהחריץ הנוכחי שלה, חריץ A או חריץ B. עדיין לא הוחלו עדכונים. החריץ הנוכחי של המערכת הוא חריץ פעיל, שבו ניתן להפעיל את המערכת והוא הצליח.
  • Update in progress: המערכת פועלת מחריץ B, כך שחריץ B הוא החריץ הפעיל, המוצלח והמתאים לאתחול. חריץ A סומן כבלתי ניתן לאתחול כי התוכן של חריץ A מתעדכן אבל עדיין לא הושלם. הפעלה מחדש במצב הזה אמורה להמשיך את האתחול מחריץ B.
  • Update applied, reboot pending: המערכת פועלת מחריץ B, חריץ B ניתן לאתחול והוא פועל, אבל חריץ A סומן כפעיל (ולכן הוא מסומן כאפשרות לאתחול). חריץ A עדיין לא מסומן כ'הפעלה מוצלחת', והתוכנה להפעלת האתחול צריכה לבצע מספר ניסיונות להפעלה מחריץ A.
  • המערכת הופעלה מחדש עם עדכון חדש: המערכת פועלת מחריץ A בפעם הראשונה, חריץ B עדיין ניתן לאתחול והפעלה, וחריץ A עדיין פעיל אבל לא ניתן לאתחול והפעלה. דימון (daemon) במרחב המשתמש, 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 (שנקראת בקוד מטען שימושי) זמינה להורדה. מדיניות במכשיר עשויה לדחות את ההורדה וההחלה של עומס התועלת על סמך רמת הטעינה של הסוללה, פעילות המשתמש, סטטוס הטעינה או מדיניות אחרת. בנוסף, מכיוון שהעדכון פועל ברקע, יכול להיות שהמשתמשים לא ידעו שהוא מתבצע. כל זה אומר שתהליך העדכון עלול להיפסק בכל שלב עקב מדיניות, הפעלות מחדש לא צפויות או פעולות של משתמשים.

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

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

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

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

אחרי ההתקנה

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

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

  • מחברים את הפורמט החדש של מערכת הקבצים. לא ניתן לשנות את סוג מערכת הקבצים אלא אם יש תמיכה בו בליבה הישנה, כולל פרטים כמו אלגוריתם הדחיסה שנעשה בו שימוש אם משתמשים במערכת קבצים דחוסה (למשל SquashFS).
  • הסבר על הפורמט של התוכנית במחיצה החדשה לאחר ההתקנה אם משתמשים בקובץ בינארי בפורמט Executable and Linkable Format‏ (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, שכלול בחבילת OTA מסוג A/B כשמשתמשים בקוד AOSP. לקוח עדכון המערכת של Java, כמו GmsCore, מחלץ את care_map.txt, מגדיר את הרשאת הגישה לפני הפעלת המכשיר מחדש ומחק את הקובץ שחולץ אחרי שהמערכת מאותחלת בהצלחה לגרסה החדשה.