Linux下實(shí)現(xiàn)低延遲音頻

除去音頻處理耗時(shí)外,音頻延遲主要由緩存引起。本文描述了Linux下如何減小音頻緩存,并介紹該如何避免音頻緩存欠載和過(guò)載。

一、如何減小音頻緩存

圖1 frame、period和buffer的關(guān)系
  • buffer是環(huán)形緩沖區(qū),大小為period_size * period_count 。由于buffer可以設(shè)置得很大,如果要在一次中斷中傳輸完整個(gè)buffer會(huì)引入過(guò)大的延遲,因此buffer被劃分成多個(gè)period
  • period是每個(gè)硬件中斷之間傳輸?shù)膄rame的個(gè)數(shù),大小由period_size指定
  • frame為同一時(shí)刻采集或播放的樣本,如果是雙聲道,那么一個(gè)frame包含左右聲道的sample

使用更小的buffer(調(diào)小period_size和period_count)可以縮短延遲,也更容易觸發(fā)欠載和過(guò)載(在ALSA中統(tǒng)稱為XRUN):

  • 欠載指播放音頻過(guò)程中應(yīng)用送數(shù)據(jù)到buffer不及時(shí),導(dǎo)致buffer中無(wú)數(shù)據(jù)可播放
  • 過(guò)載指采集音頻過(guò)程中應(yīng)用從buffer取數(shù)據(jù)不及時(shí),導(dǎo)致buffer循環(huán)緩沖區(qū)被覆蓋

渲染音頻對(duì)實(shí)時(shí)性要求非常高,需要在固定時(shí)間內(nèi)將固定數(shù)量的音頻數(shù)據(jù)傳輸?shù)揭纛l硬件。音頻進(jìn)程(本文并不區(qū)分進(jìn)程與線程,對(duì)Linux調(diào)度器而言它們是等價(jià)的)要和系統(tǒng)中的其他進(jìn)程競(jìng)爭(zhēng)CPU資源,一旦音頻進(jìn)程沒(méi)有在給定的時(shí)間內(nèi)獲得CPU時(shí)間片,音頻緩沖區(qū)耗盡,用戶會(huì)聽到刺耳的爆破音,緩沖區(qū)越小越容易出現(xiàn)該問(wèn)題。

圖2 音頻進(jìn)程(紅色)與其他進(jìn)程競(jìng)爭(zhēng)CPU資源

二、如何避免欠載和過(guò)載

常見的欠載和過(guò)載的原因如下:

  • 優(yōu)先級(jí)過(guò)低
  • 優(yōu)先級(jí)反轉(zhuǎn)
  • 調(diào)度延遲
  • 長(zhǎng)時(shí)間中斷禁用
  • 內(nèi)存管理

優(yōu)先級(jí)過(guò)低

圖3 內(nèi)核優(yōu)先級(jí)標(biāo)度

Linux進(jìn)程按調(diào)度策略可分為普通進(jìn)程和實(shí)時(shí)進(jìn)程:

  • 普通進(jìn)程的調(diào)度策略包括:SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE
  • 實(shí)時(shí)進(jìn)程的調(diào)度策略包括:SCHED_DEADLINE、SCHED_FIFO、SCHED_RR

如圖3所示,內(nèi)核使用0到139表示內(nèi)部?jī)?yōu)先級(jí),值越低,優(yōu)先級(jí)越高。0到99供實(shí)時(shí)進(jìn)程使用,普通進(jìn)程的nice值[-20,+19]映射到范圍100到139,可見實(shí)時(shí)進(jìn)程的優(yōu)先級(jí)總是比普通進(jìn)程高。

普通進(jìn)程使用CFS(完全公平調(diào)度類)實(shí)現(xiàn)調(diào)度。CFS旨在公平地對(duì)待共享通用CPU資源的競(jìng)爭(zhēng)性工作負(fù)載,進(jìn)程nice值越小,優(yōu)先級(jí)越高,分配的CPU時(shí)間越多,同時(shí)CFS要避免低優(yōu)先級(jí)的進(jìn)程饑餓,因此會(huì)出現(xiàn)較低nice值的進(jìn)程的CPU被轉(zhuǎn)移到較高nice值的進(jìn)程的情況,對(duì)音頻而言,這可能會(huì)導(dǎo)致欠載或過(guò)載。
與普通進(jìn)程不同,調(diào)度程序總是讓優(yōu)先級(jí)高的實(shí)時(shí)進(jìn)程運(yùn)行,換句話說(shuō),實(shí)時(shí)進(jìn)程運(yùn)行的過(guò)程中禁止低優(yōu)先級(jí)進(jìn)程的執(zhí)行。只有在下述事件之一發(fā)生時(shí),實(shí)時(shí)進(jìn)程才會(huì)被另一個(gè)進(jìn)程取代:

  • 進(jìn)程被另外一個(gè)更高優(yōu)先級(jí)的實(shí)時(shí)進(jìn)程搶占
  • 進(jìn)程執(zhí)行了阻塞操作并進(jìn)入睡眠
  • 進(jìn)程停止或被殺死
  • 進(jìn)程調(diào)用sched_yield()自愿放棄CPU
  • 進(jìn)程事基于SCHED_RR(時(shí)間片輪轉(zhuǎn))的實(shí)時(shí)進(jìn)程,而且用完了自己的時(shí)間片

為避免音頻進(jìn)程被搶占,可以使用SCHED_FIFO調(diào)度策略。當(dāng)然使用SCHED_FIFO調(diào)度策略后,音頻進(jìn)程仍然會(huì)被其他優(yōu)先級(jí)更高的SCHED_FIFO進(jìn)程搶占,需要檢查更高優(yōu)先級(jí)的進(jìn)程保證它們?cè)谟邢迺r(shí)間內(nèi)完成。

優(yōu)先級(jí)反轉(zhuǎn)

圖4 優(yōu)先級(jí)反轉(zhuǎn),綠色為低優(yōu)先級(jí),黃色為中優(yōu)先級(jí),紅色為高優(yōu)先級(jí)

優(yōu)先級(jí)反轉(zhuǎn)出現(xiàn)過(guò)程如圖4所示:

  1. 綠色進(jìn)程(低優(yōu)先級(jí))先執(zhí)行,獲取信號(hào)量并執(zhí)行臨界區(qū)代碼
  2. 紅色進(jìn)程(高優(yōu)先級(jí))進(jìn)入就緒狀態(tài)(ready state),調(diào)度器搶占綠色進(jìn)程讓紅色進(jìn)程執(zhí)行
  3. 紅色進(jìn)程獲取信號(hào)量阻塞
  4. 黃色進(jìn)程(中優(yōu)先級(jí))進(jìn)入就緒狀態(tài),由于黃色進(jìn)程優(yōu)先級(jí)比綠色進(jìn)程高,且不需要獲取信號(hào)量,因此它會(huì)先執(zhí)行完
  5. 綠色進(jìn)程執(zhí)行完臨界區(qū)代碼后釋放信號(hào)量
  6. 紅色進(jìn)程繼續(xù)執(zhí)行

在該例子中,高優(yōu)先級(jí)進(jìn)程要等待中優(yōu)先級(jí)進(jìn)程執(zhí)行完后才能執(zhí)行,因此被稱作優(yōu)先級(jí)反轉(zhuǎn)。

避免優(yōu)先級(jí)反轉(zhuǎn)的一種方法是使用有優(yōu)先級(jí)繼承(priority inheritance)功能的互斥鎖,假如圖4的例子將信號(hào)量換為這種互斥鎖,當(dāng)紅色進(jìn)程獲取互斥鎖阻塞時(shí),綠色進(jìn)程的優(yōu)先級(jí)會(huì)臨時(shí)提高到和紅色進(jìn)程一樣,這樣黃色進(jìn)程就無(wú)法搶占綠色進(jìn)程,也就不會(huì)發(fā)生優(yōu)先級(jí)反轉(zhuǎn)。
Linux中的rt_mutex實(shí)現(xiàn)了優(yōu)先級(jí)繼承,可以在編譯內(nèi)核時(shí)通過(guò)配置宏CONFIG_RT_MUTEXES開啟。

避免優(yōu)先級(jí)反轉(zhuǎn)的另一種思路是設(shè)計(jì)之初就避免訪問(wèn)臨界區(qū)資源。譬如用無(wú)鎖隊(duì)列,如果有多個(gè)地方需要訪問(wèn)共享資源,可以考慮創(chuàng)建一個(gè)“server”負(fù)責(zé)直接操作共享資源并通過(guò)非阻塞的消息隊(duì)列獲取“client”操作共享資源的請(qǐng)求,而不是在每個(gè)訪問(wèn)的地方加鎖。

調(diào)度延遲

調(diào)度延遲是指從線程已準(zhǔn)備好運(yùn)行到環(huán)境切換完成可以在 CPU 上實(shí)際運(yùn)行之間的間隔時(shí)間。延遲越短越好,一旦超過(guò) 2 毫秒,就會(huì)造成音頻問(wèn)題。

非搶占內(nèi)核中,如果系統(tǒng)處于核心態(tài)并正在處理系統(tǒng)調(diào)用,那么系統(tǒng)中的其他進(jìn)程是無(wú)法奪取其CPU時(shí)間的。調(diào)度器必須等到系統(tǒng)調(diào)用執(zhí)行結(jié)束,才能選擇另一個(gè)進(jìn)程執(zhí)行。如果內(nèi)核處于相對(duì)耗時(shí)較長(zhǎng)的操作中,比如文件系統(tǒng)或內(nèi)存管理相關(guān)的任務(wù),這可能導(dǎo)致調(diào)度延遲增加。編譯內(nèi)核時(shí)打開CONFIG_PREEMPT選項(xiàng)可以啟用對(duì)內(nèi)核搶占的支持。開啟內(nèi)核搶占后,如果高優(yōu)先級(jí)進(jìn)程有事情需要完成,不僅用戶空間應(yīng)用程序可以被中斷,內(nèi)核也可以被中斷。

CONFIG_PREEMPT在一些決策上為了兼顧吞吐量仍然會(huì)犧牲延遲指標(biāo),而 CONFIG_PREEMPT_RT(Linux實(shí)時(shí)內(nèi)核補(bǔ)?。?/a>則是為追求低延遲設(shè)計(jì),它實(shí)現(xiàn)了完全可搶占特性,譬如CONFIG_PREEMPT只能搶占系統(tǒng)調(diào)用,無(wú)法搶占中斷,而CONFIG_PREEMPT_RT將中斷處理程序轉(zhuǎn)換為可搶占的內(nèi)核線程。在ARM處理器上的測(cè)試顯示CONFIG_PREEMPT_RT的最大中斷響應(yīng)時(shí)間比CONFIG_PREEMPT少了37%~72%。

降低調(diào)度延遲的另一種方法是通過(guò)isolcpus或cpuset讓音頻進(jìn)程在被隔離的CPU上運(yùn)行,這樣能保證該CPU上不會(huì)有其他的用戶進(jìn)程執(zhí)行。

長(zhǎng)時(shí)間中斷禁用

如果某個(gè)中斷處理程序運(yùn)行時(shí)間較長(zhǎng),或者中斷被長(zhǎng)時(shí)間禁用,那么音頻直接內(nèi)存訪問(wèn)(DMA)的中斷會(huì)被延遲,有可能導(dǎo)致XRUN。為避免中斷處理程序運(yùn)行時(shí)間過(guò)長(zhǎng),大部分設(shè)備驅(qū)動(dòng)程序分為兩部分,一部分是中斷處理程序,它被設(shè)計(jì)為快速完成,一般只是拷貝數(shù)據(jù)到設(shè)備指定位置,另一部分在內(nèi)核線程中完成剩余的工作。

內(nèi)存管理

Malloc申請(qǐng)內(nèi)存的耗時(shí)是不受限制的,因此音頻進(jìn)程應(yīng)該預(yù)先申請(qǐng)內(nèi)存而不是在處理過(guò)程中申請(qǐng)。由于請(qǐng)求分頁(yè)機(jī)制的存在,申請(qǐng)的虛擬內(nèi)存并不會(huì)馬上被映射到物理內(nèi)存,而是要延遲到程序第一次訪問(wèn)這些虛擬內(nèi)存的時(shí)候,類似地,程序也只是被訪問(wèn)到的部分被加載到了物理內(nèi)存。調(diào)用mlockall()接口可以確保程序被加載到了物理內(nèi)存,并且避免虛擬內(nèi)存被換出到交換文件,但是malloc申請(qǐng)的內(nèi)存仍然不會(huì)被馬上映射到物理內(nèi)存,為了確保malloc申請(qǐng)的內(nèi)存映射到物理內(nèi)存,進(jìn)程應(yīng)該在申請(qǐng)完內(nèi)存后馬上寫一遍內(nèi)存。

最后總結(jié)下避免欠載和過(guò)載的方法:

  • 音頻進(jìn)程使用SCHED_FIFO調(diào)度策略,并設(shè)置合理的優(yōu)先級(jí)
  • 使用無(wú)鎖隊(duì)列等方法避免訪問(wèn)臨界區(qū)資源
  • 打開CONFIG_PREEMPT選項(xiàng)啟用內(nèi)核搶占
  • 使用CONFIG_PREEMPT_RT Linux實(shí)時(shí)內(nèi)核補(bǔ)丁
  • 通過(guò)isolcpus或cpuset讓音頻進(jìn)程在被隔離的CPU上運(yùn)行
  • 調(diào)用mlockall()接口確保程序被加載到了物理內(nèi)存,并且避免虛擬內(nèi)存被換出到交換文件
  • 預(yù)先申請(qǐng)內(nèi)存,并申請(qǐng)完內(nèi)存后馬上寫一遍內(nèi)存

參考文章
音頻延遲的促成因素
音頻開發(fā)中常見的四個(gè)錯(cuò)誤
Introduction to Sound Programming with ALSA
RTOS debugging, part 4: Priority inversion ? when the important stuff has to wait

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容