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 与子设备的关系图

snd_card 与子设备关系 snd_card snd_pcm (设备0) snd_pcm (设备1) snd_control snd_rawmidi snd_timer 更多子设备...

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 内部结构

snd_pcm 内部结构 snd_pcm streams[0] (播放) streams[1] (捕获) substream 0 substream 1 substream 0 substream 1

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 状态机

snd_pcm_substream 状态机 SNDRV_PCM_STATE_OPEN SETUP PREPARED RUNNING DRAINING SUSPENDED XRUN

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调试日志。