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

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

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

רקע

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

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

היפוך עדיפות

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

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

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

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

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

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

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

  • השבתת ההפרעות
  • מנעולים מסוג mutex לירושה של עדיפות

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

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

שיטות שבהן משתמשת Android

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

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

  • הוסף
  • 'or' ברמת הסיביות
  • 'and' בייט-בייט

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

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

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

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

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

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

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

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

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

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

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

כלים

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

לסיכום

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