Linux 驱动之 ALSA(七)XRUN

XRUN 是缓冲区不足或溢出,X 代表不足或溢出。在这两种情况下,都表明系统速度不够快,未能及时处理来自 ALSA 音频缓冲区的数据,因此丢失了一些数据。当我们以非常小的缓冲区大小运行时,声卡应该非常快地处理传入缓冲区的数据,否则就溢出 overrun 了。有些芯片无法适应较小的缓冲区大小,因此我们必须增加缓冲区长度以减轻声音芯片的工作量。通常,xruns 可以听到爆裂声或爆裂声。

在录音例子中,如果应用程序读取数据不够快,循环缓存区将会被新的数据覆盖。这种数据的丢失被称为"over run" 在回放例子中,如果应用程序写入数据到缓存区中的速度不够快,缓存区将会"饿死"。这样的错误被称为"under run"

/Proc

ALSA 提供了一种通过 proc 记录和调试 xrun 的方法:

1
2
3
Device Drivers  --->  <*> Sound card support  --->  <*>   Advanced Linux Sound Architecture  ---> [*]   Sound Proc FS Support

Device Drivers ---> <*> Sound card support ---> <*> Advanced Linux Sound Architecture ---> [*] Debug ---> [*] Enable PCM ring buffer overrun/underrun debugging

/proc/asound/card#/pcm0p/xrun_debug

将“#”替换为 card number。该 proc 文件可以启用各种调试工具。必须在内核中启用 CONFIG_SND_PCM_XRUN_DEBUG、CONFIG_SND_VERBOSE_PROCFS、CONFIG_SND_DEBUG 选项(如果 xrun_debug proc 文件存在 - 该功能已启用)

1
2
3
4
5
6
7
8
1   Basic debugging - show xruns in ksyslog interface
2 Dump stack - dump stack for basic debugging
4 Jiffies check - compare the position with kernel jiffies (a sort of in-kernel monotonic clock),
show what's changed when basic debugging is enabled
8 Dump positions on each period update call
16 Dump positions on each hardware pointer update call
32 Enable logging of last 10 ring buffer positions
64 Show the last 10 ring buffer position only once (when first error situation occured)

To enable more features just do sum values of above (for example 1+2=3).

Some good value combinations:

1
2
3
# Enable basic debugging and dump stack
# Usefull to just see, if PCM stream is stopped for a reason (usually wrong audio process timing from scheduler)
echo 3 > /proc/asound/card0/pcm0p/xrun_debug
1
2
3
4
# Enable basic debugging and dump stack, check hardware pointer on the period update
# Usefull to just see, if PCM stream is stopped for a reason (usually wrong audio process timing from scheduler)
# And to check the values from driver
echo 11 > /proc/asound/card0/pcm0p/xrun_debug
1
2
3
4
# Enable basic debugging and dump stack, check hardware pointer on all updates
# Usefull to just see, if PCM stream is stopped for a reason (usually wrong audio process timing from scheduler)
# And to do the exact check the values from driver
echo 27 > /proc/asound/card0/pcm0p/xrun_debug
1
2
3
# Enable basic debugging, do jiffies check and enable one shot dump of last 10 ring buffer positions
# Usefull, when the position is broken only after some of time (to reduce ksyslog messages)
echo 101 > /proc/asound/card0/pcm0p/xrun_debug
1
2
3
# Enable basic debugging, do jiffies check and dump position on each period and hardware pointer update calls
# Usefull when the lowlevel (specific) hardware driver is somehow broken
echo 29 > /proc/asound/card0/pcm0p/xrun_debug

默认情况下,在进入 XRUN 状态时会停止 DMA 传输,直到有 available 空间可写入(overrun 时),或者直到有数据写入(underrun 时)。 但是用户空间可以通过配置 silence_threshold 来继续播放缓冲区的重复的音频数据或静音数据(silence_size 为填充的大小),当空余空间超过 silence threshold 时,就 hardware buffer 写入 silence。

Trace

  1. 在 menuconfig 中开启以下选项

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Device Drivers  
    ---> <*> Sound card support
    ---> <*> Advanced Linux Sound Architecture
    ---> [*] Sound Proc FS Support

    Device Drivers
    ---> <*> Sound card support
    ---> <*> Advanced Linux Sound Architecture
    ---> [*] Debug
    ---> [*] Enable PCM ring buffer overrun/underrun debugging

  2. 在设备中挂在 debugfs 和使能 audio 相关 trace

1
2
3
4
mount -t debugfs none /sys/kernel/debug/
echo 1 > /sys/kernel/debug/tracing/events/snd_pcm/hwptr/enable
echo 1 > /sys/kernel/debug/tracing/events/snd_pcm/applptr/enable
echo 1 > /sys/kernel/debug/tracing/events/snd_pcm/xrun/enable
  1. 执行 arecord,出现 xrun 后:
1
cat /sys/kernel/debug/tracing/trace
  1. 以下是执行
1
arecord -Dhw:2,0 -r 44100 -c 8 -f S32_LE  /data/record.wav

复现到的一次 xrun 问题:

这里 hwptr 的 old 就是 hwptr 指针,applptr 的 current 就是 applptr 指针位置,可以看到发生 xrun 前面 hwptr=2781184,applyptr=2779136。系统中 buffer_size = 2048(frames), 而这里 hwptr 指针领先 applptr 指针 2048 个 frame,说明 runtime 里的 dma buffer 已经满了(overrun),应用没有及时取走数据。下一步就可以分析应用为什么没有及时取走数据了。

关于 HW Buffer

当 app 在调用 snd_pcm_writei 时,alsa core 将 app 传来的数据搬到 HW buffer(即 DMA buffer)中,alsa driver 从 HW buffer 中读取数据传输到硬件播放。

ALSA buffer 是采用 ring buffer 来实现的。ring buffer 有多个 HW buffer 组成。

HW buffer 一般是在 alsa driver 的 hw_params 函数中分配的一块大小为 buffer size 个 frames 的 DMA buffer.

之所以采用多个 HW buffer 来组成 ring buffer, 是防止读写指针的前后位置频繁的互换(即写指针到达 HW buffer 边界时,就要回到 HW buffer 起始点)。

ring buffer = n * HW buffer. 通常这个 n 比较大,在数据读写的过程中,很少会出现读写指针互换的情况。

下图是 ALSA buffer 的实现以及读写指针更新的方法:

  • hw_ptr_base 是当前 HW buffer 在 Ring buffer 中的起始位置。当读指针到达 HW buffer 尾部时,hw_ptr_base 按 buffer size 移动
  • hw_ptr 即 HW buffer 的 driver 操作指针
  • appl_ptr 即 HW buffer 的应用操作指针
  • boundary 即 Ring buffer 边界。
  • hw_ofs 是 driver 指针在当前 HW buffer 中的位置。由 alsa driver 的 pointer() 返回
  • appl_ofs 是应用操作指针在当前 HW buffer 中的位置
  • hw_ptr 的更新是通过调用 snd_pcm_update_hw_ptr0 完成。此函数在 app 写数据时会调用,也会在硬件中断时通过 snd_pcm_peroid_elapsed 调用

实验

1
2
arecord -Dhw:2,0 -c 8 -r 44100 -f S32_LE /dev/null
arecord -Dhw:2,0 -r 44100 -c 8 -f S32_LE | aplay -Dhw:2,0 -c 8 -r 44100 -f S32_LE

以上两种方式都不会出现 overrun,这种一般都是 IO 性能问题。

cat meminfo 节点,发现系统会在缓存 41M 左右的时候进行刷 dirty page。

对于 44100 采样率、8channel、32bit 位深,每秒钟产生的数据量为 4410084Byte = 1411200Byte = 1.345MByte

可以通过修改刷 dirty page 的频率来降低出现 overrun 的概率:

1
2
3
4
echo 7056000 > /proc/sys/vm/dirty_background_bytes
echo 70560000 > /proc/sys/vm/dirty_bytes
echo 2000 > /proc/sys/vm/dirty_expire_centisecs
echo 500 > /proc/sys/vm/dirty_writeback_centisecs

修改成这样大概 5s 左右刷一次数据,这样 overrun 概率大大降低。

常见原因

  • Linux CFS(完全公平的调度程序)
  • 具有 SCHED_FIFO 调度的高优先级线程
  • 优先级倒置
  • 长时间调度延迟
  • 长时间运行的中断处理程序
  • 长时间中断禁用
  • 电源管理
  • 安全内核

参考文献

https://source.android.com/docs/core/audio/latency/contrib?hl=zh-cn
https://www.alsa-project.org/wiki/XRUN_Debug