הימנעות מהפיכת עדיפות

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

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

רקע

אנחנו משנים את הארכיטקטורה של שרת האודיו AudioFlinger ב-Android ושל הטמעת הלקוח AudioTrack/AudioRecord כדי לצמצם את זמן האחזור. העבודה הזו התחילה ב-Android 4.1, והמשיכה עם שיפורים נוספים בגרסאות 4.2,‏ 4.3,‏ 4.4 ו-5.0.

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

היפוך עדיפות

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

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

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

ביישום האודיו ב-Android, סביר להניח שהיפוך עדיפות יתרחש במקומות הבאים. לכן כדאי להתמקד בנקודות הבאות:

  • בין השרשור הרגיל של המיקסר לבין השרשור המהיר של המיקסר ב-AudioFlinger
  • בין ה-thread של הקריאה החוזרת של האפליקציה לבין ה-thread של המיקסר המהיר (לשניהם יש עדיפות גבוהה, אבל קצת שונה)
  • בין השרשור של הקריאה החוזרת (callback) של האפליקציה לבין שרשור מהיר של לכידה (דומה לקודם)
  • בתוך ההטמעה של שכבת הפשטת החומרה (HAL) של האודיו, למשל לשיחות טלפון או לביטול הד.
  • בתוך מנהל התקן האודיו בליבת המערכת
  • בין שרשור של קריאה חוזרת של AudioTrack או AudioRecord לבין שרשורים אחרים של האפליקציה (זה לא בשליטתנו)

פתרונות נפוצים

הפתרונות הנפוצים כוללים:

  • השבתת הפרעות
  • priority inheritance mutexes

השבתת ההפרעות לא אפשרית במרחב המשתמש של Linux, והיא לא פועלת במעבדים מרובי ליבות סימטריים (SMP).

לא נעשה שימוש ב-futexes (fast user-space mutexes) במערכת האודיו כי הם יחסית כבדים, וכי הם מסתמכים על לקוח מהימן.

טכניקות שנעשה בהן שימוש ב-Android

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

אנחנו משתמשים גם בפעולות אטומיות כמו:

  • הוסף
  • bitwise "or"
  • bitwise "and"

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

הערה: פעולות אטומיות והאינטראקציות שלהן עם מחסומי זיכרון הן נושאים מורכבים שקל מאוד להבין אותם לא נכון ולהשתמש בהם בצורה שגויה. הוספנו כאן את השיטות האלה כדי לספק מידע מלא, אבל מומלץ גם לקרוא את המאמר SMP Primer for Android כדי לקבל מידע נוסף.

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

  • שימוש בתורי FIFO (First In, First Out) שאינם חוסמים, עם קורא יחיד וכותב יחיד, להעברת נתונים.
  • כדאי לנסות להעתיק מצב במקום לשתף מצב בין מודולים עם עדיפות גבוהה לבין מודולים עם עדיפות נמוכה.
  • אם יש צורך בשיתוף הסטטוס, צריך להגביל אותו למילה בגודל המקסימלי שאפשר לגשת אליו באופן אטומי בפעולה של אוטובוס אחד, בלי ניסיונות חוזרים.
  • למצב מורכב של כמה מילים, משתמשים בתור מצב. תור מצבים הוא בעצם תור FIFO עם קורא יחיד וכותב יחיד שלא חוסם, שמשמש למצבים ולא לנתונים, אלא שהכותב מצמצם פעולות push סמוכות לפעולת push אחת.
  • חשוב לשים לב למחסומי זיכרון כדי להבטיח שה-SMP יפעל בצורה תקינה.
  • סומכים, אבל בודקים. כשמשתפים מצב בין תהליכים, אל תניחו שהמצב תקין. לדוגמה, צריך לוודא שהאינדקסים נמצאים בטווח. האימות הזה לא נדרש בין תהליכים באותו תהליך, בין תהליכים עם אמון הדדי (שבדרך כלל יש להם אותו UID). הוא גם לא נחוץ לנתונים משותפים, כמו אודיו PCM, שבהם פגם לא משפיע על הנתונים.

אלגוריתמים לא חוסמים

אלגוריתמים לא חוסמים היו נושא למחקרים רבים לאחרונה. אבל חוץ מתורים מסוג FIFO עם קורא יחיד וכותב יחיד, גילינו שהם מורכבים ונוטים לשגיאות.

החל מ-Android 4.2, אפשר למצוא את המחלקות שלנו שאינן חוסמות, של קורא/כותב יחיד, במיקומים הבאים:

  • frameworks/av/include/media/nbaio/
  • frameworks/av/media/libnbaio/
  • frameworks/av/services/audioflinger/StateQueue*

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

מפתחים צריכים לעדכן חלק מקוד האפליקציה לדוגמה של OpenSL ES כדי להשתמש באלגוריתמים לא חוסמים או להפנות לספריית קוד פתוח שאינה Android.

פרסמנו דוגמה להטמעה של FIFO לא חוסם, שנועדה במיוחד לקוד אפליקציה. אלה הקבצים שנמצאים בספריית קובצי המקור של הפלטפורמה: frameworks/av/audio_utils:

כלים

למיטב ידיעתנו, אין כלים אוטומטיים למציאת היפוך עדיפות, במיוחד לפני שהוא קורה. חלק מהכלים לניתוח סטטי של קוד במחקר מסוגלים למצוא היפוכי עדיפות אם יש להם גישה לכל ה-codebase. כמובן, אם מעורב קוד משתמש שרירותי (כמו במקרה הזה של האפליקציה) או אם מדובר בבסיס קוד גדול (כמו במקרה של ליבת Linux ומנהלי התקנים), ניתוח סטטי עשוי להיות לא מעשי. הדבר הכי חשוב הוא לקרוא את הקוד בקפידה רבה ולהבין היטב את המערכת כולה ואת האינטראקציות. כלים כמו systrace ו-ps -t -p שימושיים כדי לראות היפוך עדיפות אחרי שהוא מתרחש, אבל הם לא מאפשרים לדעת מראש אם הוא יתרחש.

לסיכום

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