במאמר הזה נסביר איך מערכת האודיו של Android מנסה למנוע היפוך תעדוף, ונציג שיטות שגם אתם יכולים להשתמש בהן.
השיטות האלה יכולות להיות שימושיות למפתחים של אפליקציות אודיו בעלות ביצועים גבוהים, ליצרני ציוד מקורי ולספקי SoC שמטמיעים HAL אודיו. חשוב לזכור שהטמעת השיטות האלה לא מבטיחה למנוע תקלות או כשלים אחרים, במיוחד אם משתמשים בהן מחוץ להקשר האודיו. התוצאות עשויות להשתנות, ועליך לבצע הערכה ובדיקה משלך.
רקע
אנחנו משפרים את הארכיטקטורה של שרת האודיו Android AudioFlinger וההטמעה של הלקוח AudioTrack/AudioRecord כדי לצמצם את זמן האחזור. העבודה הזו התחילה ב-Android 4.1 והמשיכה עם שיפורים נוספים ב-4.2, ב-4.3, ב-4.4 וב-5.0.
כדי להשיג את זמן האחזור הנמוך הזה, נדרשו שינויים רבים ברחבי המערכת. שינוי חשוב אחד הוא הקצאת משאבי מעבד לשרשראות קריטיות לזמן באמצעות מדיניות תזמון צפויה יותר. תזמון מהימן מאפשר לצמצם את הגדלים והמספרים של מאגרי האודיו, ועדיין להימנע ממצבי 'חוסר זמן עיבוד' וממצבי 'זמן עיבוד עודף'.
היפוך עדיפות
היפוך עדיפויות הוא מצב כשל קלאסי במערכות בזמן אמת, שבו משימה בעדיפות גבוהה יותר חסומה למשך זמן בלתי מוגבל בהמתנה למשימה בעדיפות נמוכה יותר שתפנה משאב, כמו 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
מאפשרים לראות את היפוך העדיפות אחרי שהוא מתרחש, אבל לא מאפשרים לדעת מראש.
מילה אחרונה
אחרי כל הדיון הזה, אל תפחדו ממנעולים מרובים לשימוש בו-זמנית. מומלץ להשתמש במנעולים מרובים (mutexes) לשימוש רגיל, כשמשתמשים בהם ומטמיעים אותם בצורה נכונה בתרחישי שימוש רגילים שאינם קריטיים לזמן. עם זאת, סביר יותר שמנעולים מרובים לשימוש בו-זמנית יגרמו לבעיות בין משימות בעדיפות גבוהה לבין משימות בעדיפות נמוכה, ובמערכות זמן-רגישות.