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

במאמר הזה נסביר איך מערכת האודיו של 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 ב-Android כדי לקבל מידע נוסף.

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

  • שימוש בתורים מסוג FIFO (נכנס ראשון, יוצא ראשון) של קורא יחיד וכותב יחיד שלא חוסמים, להעברת נתונים.
  • כדאי לנסות להעתיק מצב במקום לשתף מצב בין מודולים עם עדיפות גבוהה לבין מודולים עם עדיפות נמוכה.
  • אם יש צורך בשיתוף מצב, צריך להגביל את המצב למילה בגודל המקסימלי שאפשר לגשת אליו באופן אטומי בפעולה של אוטובוס אחד ללא ניסיונות חוזרים.
  • למצב מורכב של כמה מילים, משתמשים בתור למצב. תור מצבים הוא בעצם תור FIFO לא חוסם עם קורא יחיד וכותב יחיד, שמשמש למצבים ולא לנתונים, אלא שהכותב מצמצם שליפות סמוכות לשליפה אחת.
  • כדאי לשים לב למחסומי זיכרון כדי לוודא שה-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:

כלים

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

לסיכום

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