4、ALSA核心数据结构:snd_card、snd_device、snd_pcm、snd_pcm_substream等关键结构体
ALSA(Advanced Linux Sound Architecture)驱动框架的核心并非一套复杂的API调用,而是围绕一组精心设计的核心数据结构展开的。对于设备驱动开发者而言,理解这些结构体之间的组织关系与生命周期,是编写正确、可维护声卡驱动的基石。本章将深入剖析 snd_card、snd_device、snd_pcm 以及 snd_pcm_substream 这四大关键结构体,阐明它们如何构成从“物理声卡”到“用户空间音频流”的完整抽象链路。
上图展示了 ALSA 核心数据结构之间的层级关系。从驱动开发视角看,snd_card 是顶层容器,snd_device 是通用挂载点,snd_pcm 是音频流接口,而 snd_pcm_substream 则是真正处理数据流的实体。下面我们将逐一拆解每个结构体的职责、关键字段与使用要点。
4.1 snd_card:声卡的根容器
核心原理:snd_card 是整个 ALSA 驱动的“根对象”,它代表一个独立的音频设备(可以是物理声卡、HDMI 音频端点或 USB 声卡)。内核通过 snd_card_new() 创建实例,通过 snd_card_free() 销毁。该结构体不仅保存设备的基本信息,还维护着所有子组件的链表。
关键字段包括:
dev:指向struct device的指针,用于与 Linux 设备模型对接。shortname/longname:设备名称,用于用户空间识别。private_data:驱动私有数据指针,通常指向自定义的芯片信息结构体。devices:struct list_head链表头,挂载所有snd_device节点。controls:控制接口链表(如音量、开关等)。
snd_card_new() 时,务必通过 this_card 参数传递私有数据大小,ALSA 会自动分配并初始化 private_data。不要手动在 snd_card 外部再分配私有结构,否则容易造成生命周期管理混乱。
4.2 snd_device:通用组件挂载点
核心原理:snd_device 是 ALSA 内部用于统一管理各类子设备(PCM、控制、定时器、MIDI 等)的通用结构体。它通过链表将不同功能的组件串联在 snd_card 上,使得声卡的注册、注销、挂起/恢复等操作可以统一遍历执行。
该结构体的核心成员:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 设备唯一标识 |
| dev | void * | 指向具体设备实例(如 snd_pcm) |
| type | enum snd_device_type | 设备类型(SNDRV_DEV_PCM、SNDRV_DEV_CONTROL 等) |
| ops | struct snd_device_ops * | 操作集(dev_register、dev_disconnect、dev_free) |
| list | struct list_head | 链表节点,用于挂入 card->devices |
操作流程:驱动开发中,通常使用 snd_device_new() 将子设备注册到声卡,或使用 snd_pcm_new() 等封装函数自动完成注册。当调用 snd_card_register() 时,ALSA 会遍历 devices 链表,依次调用每个设备的 dev_register 回调。
4.3 snd_pcm:PCM 设备实例
核心原理:snd_pcm 代表一个 PCM 设备(如声卡的模拟输出、HDMI 音频端点)。它内部包含两个 snd_pcm_str 结构(播放和捕获方向),每个方向又包含多个 snd_pcm_substream。该结构体是驱动与用户空间 ALSA-lib 交互的主要接口。
关键字段与初始化:
struct snd_pcm {
struct snd_card *card; // 所属声卡
int device; // PCM 设备编号(0,1,2...)
struct snd_pcm_str streams[2]; // 播放/捕获两个方向
struct snd_pcm_ops *ops; // 全局操作集(可选)
unsigned int nonatomic: 1; // 是否允许原子操作
// ... 其他字段
};
// 创建 PCM 设备的典型流程
static int my_pcm_new(struct snd_card *card, int device) {
struct snd_pcm *pcm;
int err = snd_pcm_new(card, "My PCM", device, 0, 1, &pcm);
if (err < 0) return err;
// 设置操作集
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &my_playback_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &my_capture_ops);
return 0;
}
snd_pcm 与 snd_pcm_substream。前者是设备级别的抽象,后者是数据流级别的抽象。一个 PCM 设备可以有多个 substream(例如支持多声道独立开合),但通常驱动只实现一个 playback 和一个 capture substream。
4.4 snd_pcm_substream:数据流的真正载体
核心原理:snd_pcm_substream 是最终与应用程序交互的实体。当用户空间打开 /dev/snd/pcmC0D0p 时,内核会创建一个对应的 substream 实例。它包含了运行时状态(runtime)、硬件参数(hw_params)、DMA 缓冲区指针等关键信息。
该结构体的核心成员:
number:子流编号(从 0 开始)。stream:方向(SNDRV_PCM_STREAM_PLAYBACK或CAPTURE)。runtime:指向snd_pcm_runtime,保存运行时状态(如 hw_ptr、appl_ptr、buffer_size 等)。pcm:反向指针,指向所属的snd_pcm。ops:指向snd_pcm_ops,包含 open、close、ioctl、hw_params、prepare、trigger 等回调。
案例拆解:多声道声卡的 substream 分配
假设设计一个 8 声道 HDMI 音频驱动,通常有两种方案:
- 单 substream 多声道:创建一个 substream,通过 hw_params 设置 channels=8。适用于大多数情况,简单高效。
- 多 substream 独立声道:创建 8 个 substream,每个对应一个声道。适用于需要独立控制每个声道的场景(如专业音频接口)。
实际开发中,方案 1 更常见。在 hw_params 回调中检查 params_channels() 的值,并据此配置 DMA 缓冲区格式即可。
4.5 数据结构间的协作与生命周期
核心原理:理解这些结构体的生命周期对于避免内存泄漏和悬空指针至关重要。典型的创建顺序为:
snd_card_new() → snd_pcm_new() → snd_device_new() → snd_card_register()
销毁顺序则相反:snd_card_free() 会依次调用所有 snd_device 的 dev_free 回调,释放 PCM、控制等子设备。
操作流程:在驱动的 probe 函数中,务必遵循以下步骤:
- 调用
snd_card_new()创建声卡。 - 创建 PCM、控制、MIDI 等子设备。
- 设置
card->driver、card->shortname等标识。 - 调用
snd_card_register()使设备对用户空间可见。
snd_pcm_new() 放在 snd_card_new() 之前调用,导致内核崩溃。原因是 snd_pcm_new() 内部需要访问 card->devices 链表,而该链表尚未初始化。正确的顺序必须严格遵循“先建卡,后建子设备”。
4.6 常见误区与总结
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 混淆 snd_pcm 与 substream | 回调中错误使用指针 | 明确区分设备级与流级操作 |
| 忽略 snd_device 的 ops | 注销时资源泄漏 | 实现 dev_free 回调释放私有数据 |
| 在 atomic 上下文调用非原子函数 | 内核调度异常 | 设置 nonatomic=1 并使用非原子锁 |
| 未检查 hw_params 的 channels | DMA 配置错误 | 在 hw_params 中验证参数 |