RK3588 kernel-6.1:sound/core 源码级详细分析

RK3588 kernel-6.1:sound/core 源码级详细分析

源码基准:/home/work2/SeagullYpcEncode/rk3588/kernel-6.1/sound/core/

本文在 linuxDoc/sound/rk3588-kernel-6.1-sound-core分析.md(模块清单与 Makefile 映射)之上,按 调用链、状态机、锁与资源 做更深一层的代码走读。整机 sound 与 ASoC/Rockchip 关系仍以 rk3588-kernel-6.1-sound声卡架构分析.md 为准。


1. 总体数据与控制通路

平面 典型入口 内核对象 用户可见形态
控制面 snd_ctl_* snd_kcontrolsnd_card.controls /dev/snd/controlC*SNDRV_CTL_IOCTL_*
PCM 数据面 snd_pcm_* snd_pcmsnd_pcm_substreamsnd_pcm_runtime /dev/snd/pcmC*D*i/pcmC*D*o
字符设备入口 sound.c snd_minors[]struct snd_minor major CONFIG_SND_MAJOR(116)
Compress offload compress_offload.c snd_comprsnd_compr_stream /dev/snd/comprC*D*

底层 SoC 驱动(如 sound/soc/rockchip/*)主要在 snd_pcm_ops(或 dmaengine 封装)里衔接 DMA;sound/core 负责 统一 minor、ioctl、缓冲模型、XRUN/avail 逻辑与安全断开


2. 字符设备与 minor:sound.c

2.1 打开设备的二次分发

所有 /dev/snd/* 节点共享同一 snd_fops,仅在 open 时根据 minor 取出事先注册的 struct snd_minor,再 replace_fops 成具体子系统的 file_operations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static int snd_open(struct inode *inode, struct file *file)
{
unsigned int minor = iminor(inode);
struct snd_minor *mptr = NULL;
const struct file_operations *new_fops;
int err = 0;

if (minor >= ARRAY_SIZE(snd_minors))
return -ENODEV;
mutex_lock(&sound_mutex);
mptr = snd_minors[minor];
if (mptr == NULL) {
mptr = autoload_device(minor);
if (!mptr) {
mutex_unlock(&sound_mutex);
return -ENODEV;
}
}
new_fops = fops_get(mptr->f_ops);
mutex_unlock(&sound_mutex);
if (!new_fops)
return -ENODEV;
replace_fops(file, new_fops);

if (file->f_op->open)
err = file->f_op->open(inode, file);
return err;
}

要点

  • CONFIG_MODULESautoload_device 可对 aloadC* 触发 request_module("snd-card-%d") 或对 sequencer/timer 触发 snd-seq/snd-timer,实现「打开节点再按需加载」的旧式自动装载路径。
  • CONFIG_SND_DYNAMIC_MINORSsnd_find_free_minor 跳过仍为 autoload 保留的 static minor,其余动态分配。

2.2 snd_lookup_minor_data 与声卡引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void *snd_lookup_minor_data(unsigned int minor, int type)
{
struct snd_minor *mreg;
void *private_data;

if (minor >= ARRAY_SIZE(snd_minors))
return NULL;
mutex_lock(&sound_mutex);
mreg = snd_minors[minor];
if (mreg && mreg->type == type) {
private_data = mreg->private_data;
if (private_data && mreg->card_ptr)
get_device(&mreg->card_ptr->card_dev);
} else
private_data = NULL;
mutex_unlock(&sound_mutex);
return private_data;
}

子驱动 open(如 compress)在匹配 SNDRV_DEVICE_TYPE_* 成功后持有 card 引用,须在出口 snd_card_unref,避免 snd_card_disconnect 期间 UAF


3. 声卡生命周期与热插拔:init.c

3.1 snd_card_register 末尾才真正对用户开放控制面

注释写明:在调用本函数之前,ALSA 控制接口对外阻塞;函数内先 device_add(&card->card_dev),再 snd_device_register_all,最后把 card 填入全局 snd_cards[]snd_info_card_register

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* snd_card_register - register the soundcard
* @card: soundcard structure
*
* This function registers all the devices assigned to the soundcard.
* Until calling this, the ALSA control interface is blocked from the
* external accesses. Thus, you should call this function at the end
* of the initialization of the soundcard.
*
* Return: Zero otherwise a negative error code if the registration failed.
*/
int snd_card_register(struct snd_card *card)
{
int err;

if (snd_BUG_ON(!card))
return -EINVAL;

if (!card->registered) {
err = device_add(&card->card_dev);
if (err < 0)
return err;
card->registered = true;
} else {
if (card->managed)
devm_remove_action(card->dev, trigger_card_free, card);
}

if (card->managed) {
err = devm_add_action(card->dev, trigger_card_free, card);
if (err < 0)
return err;
}

err = snd_device_register_all(card);

驱动编写含义:先 snd_pcm_new/snd_ctl_add 等把逻辑对象挂上 snd_card,最后 snd_card_register;否则会违背「控制设备尚未完整注册」的假设。

3.2 监控打开的 struct filesnd_monitor_file

init.c 维护 shutdown_files:在 snd_card_disconnect 路径上,对已打开的 file 替换为 snd_disconnect_f_ops,避免用户进程在设备释放后仍调用原 read/write/ioctl。这是 USB 声卡热插拔 / 模块卸载 不 panic 的关键之一(细节见 init.csnd_shutdownregister_shutdown_file 等)。


4. 设备链表与注册顺序:device.c

4.1 snd_device_type 决定排序与 register_all 次序

include/sound/core.h 中枚举顺序 同时定义调用顺序SNDRV_DEV_CONTROL 必须排在最后(注释),以便其它 PCM/rawmidi 等先注册, mixer 最后再挂钩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum snd_device_type {
SNDRV_DEV_LOWLEVEL,
SNDRV_DEV_INFO,
SNDRV_DEV_BUS,
SNDRV_DEV_CODEC,
SNDRV_DEV_PCM,
SNDRV_DEV_COMPRESS,
SNDRV_DEV_RAWMIDI,
SNDRV_DEV_TIMER,
SNDRV_DEV_SEQUENCER,
SNDRV_DEV_HWDEP,
SNDRV_DEV_JACK,
SNDRV_DEV_CONTROL, /* NOTE: this must be the last one */
};

snd_device_newtype 递增插入链表:

1
2
3
4
5
6
7
8
/* insert the entry in an incrementally sorted list */
list_for_each_prev(p, &card->devices, list) {
struct snd_device *pdev = list_entry(p, struct snd_device, list);
if ((unsigned int)pdev->type <= (unsigned int)type)
break;
}

list_add(&dev->list, p);

4.2 disconnect_allfree_all 的顺序

  • snd_device_disconnect_all反向遍历,先断开高层可见设备。
  • snd_device_free_all两阶段——先释放除 SNDRV_DEV_CONTROLSNDRV_DEV_LOWLEVEL 外的节点,再收尾剩余(避免 control 过早拆除引用仍在的 PCM):
1
2
3
4
5
6
7
8
9
10
11
list_for_each_entry_safe_reverse(dev, next, &card->devices, list) {
/* exception: free ctl and lowlevel stuff later */
if (dev->type == SNDRV_DEV_CONTROL ||
dev->type == SNDRV_DEV_LOWLEVEL)
continue;
__snd_device_free(dev);
}

/* free all */
list_for_each_entry_safe_reverse(dev, next, &card->devices, list)
__snd_device_free(dev);

5. PCM 核心:pcm.c / pcm_native.c / pcm_lib.c

5.1 中层职责划分(概念)

文件 职责
pcm.c 创建 snd_pcm、挂 snd_device、全局 snd_pcm_devices 链表、与 snd_minor 绑定
pcm_native.c ioctlmmappollread/write(mmap 路径为主)等 用户态 ABI
pcm_lib.c hw_ptr/appl_ptr 更新、availboundaryXRUNsnd_pcm_period_elapsed、阻塞唤醒 tsleep
pcm_memory.c + memalloc.c 环形缓冲分配、预分配、mmap 页映射
pcm_dmaengine.c 通用 dmaengine PCMdma_slave_configsnd_soc_* 常用封装

5.2 snd_pcm_period_elapsed(中断上下文典型路径)

DMA 每完成一个 period,驱动应调用 snd_pcm_period_elapsed(内部抢 substream);若在 **snd_pcm_ops 回调里已持锁,则用 snd_pcm_period_elapsed_under_stream_lock。内核注释说明了 pointer/trigger/get_time_info 可能被间接触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* snd_pcm_period_elapsed_under_stream_lock() - update the status of runtime for the next period
* under acquired lock of PCM substream.
* ...
* Developer should pay enough attention that some callbacks in &snd_pcm_ops are done by the call of
* function:
*
* - .pointer - to retrieve current position of audio data transmission by frame count or XRUN state.
* - .trigger - with SNDRV_PCM_TRIGGER_STOP at XRUN or DRAINING state.
* - .get_time_info - to retrieve audio time stamp if needed.
*
* Even if more than one periods have elapsed since the last call, you have to call this only once.
*/
void snd_pcm_period_elapsed_under_stream_lock(struct snd_pcm_substream *substream)
{
...
if (!snd_pcm_running(substream) ||
snd_pcm_update_hw_ptr0(substream, 1) < 0)
goto _end;

#ifdef CONFIG_SND_PCM_TIMER
if (substream->timer_running)
snd_timer_interrupt(substream->timer, 1);
#endif
_end:
snd_kill_fasync(runtime->fasync, SIGIO, POLL_IN);
}
...
void snd_pcm_period_elapsed(struct snd_pcm_substream *substream)
{
unsigned long flags;
...
snd_pcm_stream_lock_irqsave(substream, flags);
snd_pcm_period_elapsed_under_stream_lock(substream);
snd_pcm_stream_unlock_irqrestore(substream, flags);
}

实践要点:Rockchip I2S/TDM 一类驱动常在 DMA complete 回调snd_pcm_period_elapsed;漏报会导致 应用层 avail 不前进 / 时钟漂移感知异常

5.3 SoC 常用:pcm_dmaengine.c

提供 snd_hwparams_to_dma_slave_config 等,把 hw_params 物理位宽 映射到 dma_slave_buswidth,并与 snd_dmaengine_pcm_trigger 一类封装联动,减少每个 CPU DAI 重复的 prep/start/stop 样板代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* snd_hwparams_to_dma_slave_config - Convert hw_params to dma_slave_config
* @substream: PCM substream
* @params: hw_params
* @slave_config: DMA slave config
*
* This function can be used to initialize a dma_slave_config from a substream
* and hw_params in a dmaengine based PCM driver implementation.
*
* Return: zero if successful, or a negative error code
*/
int snd_hwparams_to_dma_slave_config(const struct snd_pcm_substream *substream,
const struct snd_pcm_hw_params *params,
struct dma_slave_config *slave_config)
{
enum dma_slave_buswidth buswidth;
int bits;

bits = params_physical_width(params);
...

6. 控制面:control.c

  • snd_ctl_addsnd_kcontrol 挂入 card,分配 numid,并在已注册声卡上通知用户态。
  • SNDRV_CTL_ELEM_IFACE_* 区分 mixer、PCM、POWER 等;ASoC DAPM 大量控件亦走此路径。
  • snd_ctl_notify/snd_ctl_notify_one:elem 值变化时 poll/epoll 就绪。
  • 模块参数 max_user_ctl_alloc_size(默认 8MiB)限制 用户态创建控件 的分配,减轻 DoS 风险。

7. Compress offload:compress_offload.c

面向 压缩码流(由 DSP/加速器解码而非 PCM ring),使用 与 PCM 类似但独立 的 ioctl 集(SNDRV_COMPRESS_*)。源码用大段注释约定 复用 snd_pcm_state 枚举语义 表达 compress 状态机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* a note on stream states used:
* we use following states in the compressed core
* SNDRV_PCM_STATE_OPEN: When stream has been opened.
* SNDRV_PCM_STATE_SETUP: When stream has been initialized. This is done by
* calling SNDRV_COMPRESS_SET_PARAMS. Running streams will come to this
* state at stop by calling SNDRV_COMPRESS_STOP, or at end of drain.
* SNDRV_PCM_STATE_PREPARED: When a stream has been written to (for
* playback only). User after setting up stream writes the data buffer
* before starting the stream.
* SNDRV_PCM_STATE_RUNNING: When stream has been started and is
* decoding/encoding and rendering/capturing data.
* SNDRV_PCM_STATE_DRAINING: When stream is draining current data. This is done
* by calling SNDRV_COMPRESS_DRAIN.
* SNDRV_PCM_STATE_PAUSED: When stream is paused. This is done by calling
* SNDRV_COMPRESS_PAUSE. It can be stopped or resumed by calling
* SNDRV_COMPRESS_STOP or SNDRV_COMPRESS_RESUME respectively.
*/

snd_compr_open 根据 O_RDONLY/O_WRONLY 区分 capture/playback,并与 compr->direction 一致性校验;失败路径须 snd_card_unref


8. 其它 core 模块(简析)

模块 文件 说明
Timer timer.c(内含 #include "timer_compat.c")、hrtimer.c snd_timer 用户接口与高精度后端;pcm_timer.c 把 PCM 与 timer 中断联动
Raw MIDI rawmidi.c /dev/snd/midiC*D*;末尾 #include "rawmidi_compat.c"
Hwdep hwdep.c 厂商 ioctl、firewire、FW 下载等旁路
Jack jack.cctljack.c 上报插头事件;可与 input 子系统联动(SND_JACK_INPUT_DEV
VMaster vmaster.c 软音量 虚拟 master,聚合多路 slider
info info.c /proc/asound 信息树(CONFIG_SND_PROC_FS
OSS sound_oss.coss/ 旧 OSS API 兼容层
Sequencer seq/ ALSA sequencer 与 seq/oss
IEC958 / ELD pcm_iec958.cpcm_drm_eld.c S/PDIF、HDMI 音频元数据
ISA DMA isadma.c 遗留 ISA 声卡

9. 与 RK3588 开发的关联方式

  1. 绝大多数板级工作不在 sound/core,而在 sound/soc/rockchip 与 codec machine;但 PCM 回调契约trigger/pointer/hw_params)与 snd_pcm_period_elapsed 出自 core。
  2. DMAEngine PCM 路径:CONFIG_SND_SOC_GENERIC_DMAENGINE_PCM + pcm_dmaengine.c 导出符号,与 rockchip_i2s_tdm.c 等平台驱动组合常见。
  3. 调试:XRUN、延迟问题通常需同时看 pcm_lib 的 avail/wait 与驱动侧 DMA 回调频率 是否匹配 period_size

10. 参考文档

文章互动

阅读 --

留言

0 条留言

正在加载留言…