2. ALSA核心数据结构:snd_card、snd_pcm、snd_pcm_substream详解
ALSA音频子系统的核心由三个层次递进的数据结构支撑:声卡实例(snd_card)、PCM设备(snd_pcm)和PCM数据流(snd_pcm_substream)。理解这三者的关系、生命周期和内存布局,是掌握ALSA内存管理与缓存策略的前提。
核心要点: snd_card 是顶层容器,snd_pcm 是逻辑设备,snd_pcm_substream 是实际数据通道。内存分配和缓存策略最终作用于 substream 层面。
2.1 snd_card:声卡实例的根容器
snd_card 是ALSA中代表一块物理或虚拟声卡的核心结构体。它作为整个声卡资源的根节点,管理着该声卡下的所有组件(PCM、控制、MIDI、定时器等)。
2.1.1 结构体定义与关键字段
struct snd_card {
int number; /* 声卡编号(0 ~ 31) */
char id[16]; /* 声卡标识字符串 */
char driver[16]; /* 驱动名称 */
char shortname[32]; /* 短名称 */
char longname[80]; /* 长名称 */
struct list_head devices; /* 设备链表头 */
struct list_head controls; /* 控制元素链表 */
struct list_head files; /* 文件句柄链表 */
struct device *dev; /* 内核设备指针 */
struct module *module; /* 所属模块 */
int components; /* 组件位掩码 */
size_t total_pcm_devices; /* PCM设备计数 */
void *private_data; /* 驱动私有数据 */
struct snd_card *next; /* 全局声卡链表 */
};
2.1.2 生命周期与内存分配
| 阶段 | 函数/操作 | 内存行为 |
|---|---|---|
| 创建 | snd_card_new() |
分配 snd_card 结构体内存,初始化链表头,设置编号 |
| 组件注册 | snd_device_new() |
将PCM、控制等设备挂入 devices 链表 |
| 完成注册 | snd_card_register() |
向内核注册声卡,创建设备文件节点 |
| 释放 | snd_card_free() |
递归释放所有子设备内存,最后释放 snd_card 自身 |
最佳实践: 驱动应在
probe() 中先调用 snd_card_new(),再创建PCM设备,最后调用 snd_card_register()。释放时使用 snd_card_free() 自动清理所有子结构。
2.1.3 snd_card 与子设备的关系图
2.2 snd_pcm:PCM逻辑设备
snd_pcm 结构体代表一个PCM设备实例,通常对应一个音频接口(如播放设备、录音设备)。它封装了硬件参数、操作函数集和流管理信息。
2.2.1 结构体关键字段
struct snd_pcm {
struct snd_card *card; /* 所属声卡 */
struct list_head list; /* 声卡设备链表节点 */
int device; /* 设备索引号 */
unsigned int info_flags; /* 信息标志位 */
struct snd_pcm_ops *ops; /* 硬件操作函数集 */
struct snd_pcm_str streams[2]; /* 播放/捕获两个方向 */
struct snd_pcm_hardware hw; /* 硬件能力描述 */
size_t buffer_bytes_max; /* 最大缓冲区大小 */
size_t period_bytes_max; /* 最大周期大小 */
int periods_max; /* 最大周期数 */
void *private_data; /* 驱动私有数据 */
};
2.2.2 播放与捕获双方向
snd_pcm 内部通过 streams[2] 数组管理两个方向:
- streams[0] (SNDRV_PCM_STREAM_PLAYBACK):播放方向,数据从用户空间流向硬件。
- streams[1] (SNDRV_PCM_STREAM_CAPTURE):捕获方向,数据从硬件流向用户空间。
每个 snd_pcm_str 结构体包含该方向下的所有 substream 实例。
2.2.3 snd_pcm 内部结构
2.3 snd_pcm_substream:实际数据流通道
snd_pcm_substream 是ALSA中最核心的数据流结构体,它代表一个独立的PCM数据流通道。每个substream对应一个打开的文件描述符,拥有独立的硬件参数、运行时状态和缓冲区。
2.3.1 结构体关键字段
struct snd_pcm_substream {
struct snd_pcm *pcm; /* 所属PCM设备 */
struct snd_pcm_str *pstr; /* 所属方向(播放/捕获) */
int number; /* substream编号 */
unsigned int stream; /* 流方向 */
struct snd_pcm_ops *ops; /* 操作函数集 */
struct snd_pcm_runtime *runtime; /* 运行时状态(核心!) */
struct snd_pcm_hardware hw; /* 硬件约束 */
struct snd_dma_buffer dma_buffer; /* DMA缓冲区描述 */
struct snd_pcm_mmap_status *mmap_status; /* mmap状态区 */
struct snd_pcm_mmap_control *mmap_control; /* mmap控制区 */
struct fasync_struct *fasync; /* 异步通知 */
void *private_data; /* 驱动私有数据 */
};
关键注意:
runtime 字段是substream的运行时心脏,它包含了缓冲区指针、hw_ptr、appl_ptr、状态机、中断计数等所有动态信息。内存管理和缓存策略的绝大部分操作都围绕 runtime 展开。
2.3.2 snd_pcm_runtime:运行时状态详解
struct snd_pcm_runtime {
struct snd_pcm_substream *substream; /* 反向引用 */
snd_pcm_state_t state; /* 流状态 */
snd_pcm_uframes_t buffer_size; /* 缓冲区大小(帧数) */
snd_pcm_uframes_t period_size; /* 周期大小(帧数) */
snd_pcm_uframes_t hw_ptr; /* 硬件指针 */
snd_pcm_uframes_t appl_ptr; /* 应用指针 */
snd_pcm_uframes_t start_threshold; /* 启动阈值 */
snd_pcm_uframes_t stop_threshold; /* 停止阈值 */
struct snd_pcm_hw_params hw_params; /* 硬件参数 */
struct snd_pcm_sw_params sw_params; /* 软件参数 */
struct snd_dma_buffer *dma_buffer_p; /* DMA缓冲区指针 */
unsigned int periods; /* 周期数 */
void *dma_area; /* DMA虚拟地址 */
dma_addr_t dma_addr; /* DMA物理地址 */
struct snd_pcm_mmap_status *status; /* mmap状态 */
struct snd_pcm_mmap_control *control; /* mmap控制 */
};
2.3.3 指针追踪机制
substream 通过两个指针驱动数据流:
| 指针 | 含义 | 更新者 | 作用 |
|---|---|---|---|
| hw_ptr | 硬件已处理位置 | DMA中断处理函数 | 指示硬件已经消费/采集到的帧位置 |
| appl_ptr | 应用已处理位置 | 用户空间读写操作 | 指示应用程序已经写入/读取到的帧位置 |
当 appl_ptr - hw_ptr < buffer_size 时,缓冲区有可用空间;当差值接近 buffer_size 时,可能发生 XRUN(欠载/过载) 错误。
2.3.4 substream 状态机
2.4 三层结构的内存布局总结
| 层级 | 结构体 | 内存分配时机 | 释放时机 | 关键内存区域 |
|---|---|---|---|---|
| 顶层 | snd_card | probe() → snd_card_new() | remove() → snd_card_free() | 结构体自身 + private_data |
| 中间层 | snd_pcm | snd_pcm_new() | snd_card_free() 自动释放 | 结构体 + streams[2] 数组 |
| 底层 | snd_pcm_substream | snd_pcm_new() 时创建 | snd_card_free() 自动释放 | runtime + dma_buffer + mmap区域 |
内存管理要点:
- snd_card 是内存分配的总入口,所有子设备的内存最终由 snd_card_free() 统一回收。
- snd_pcm_substream 的 runtime 和 dma_buffer 是内存消耗大户,驱动需在 hw_params 回调中分配,在 hw_free 回调中释放。
- DMA缓冲区通常使用
snd_pcm_lib_malloc_pages()分配,确保物理连续。
2.5 驱动开发中的典型初始化流程
static int my_pcm_probe(struct platform_device *pdev)
{
struct snd_card *card;
struct snd_pcm *pcm;
int err;
/* 1. 创建声卡 */
err = snd_card_new(&pdev->dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
THIS_MODULE, sizeof(struct my_chip), &card);
if (err < 0)
return err;
/* 2. 创建PCM设备:1个播放substream + 1个捕获substream */
err = snd_pcm_new(card, "my_pcm", 0, 1, 1, &pcm);
if (err < 0)
goto err_free_card;
/* 3. 设置操作函数集 */
pcm->ops = &my_pcm_ops;
pcm->private_data = card->private_data;
/* 4. 设置硬件能力 */
pcm->info_flags = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED;
pcm->hw = my_pcm_hardware;
/* 5. 注册声卡 */
err = snd_card_register(card);
if (err < 0)
goto err_free_card;
return 0;
err_free_card:
snd_card_free(card);
return err;
}
调试建议: 使用
cat /proc/asound/cards 查看声卡列表,使用 cat /proc/asound/pcm 查看PCM设备及substream信息。内核启动参数 snd_debug=1 可开启ALSA调试日志。