4. 声卡创建与注册

在 ALSA 框架中,声卡(Card)是音频设备的顶层抽象。一个物理声卡对应一个 struct snd_card 实例,它管理着该声卡下的所有 PCM 设备、控制接口、MIDI 设备、AC97 编解码器等子组件。本章深入剖析声卡生命周期的三个核心 API:创建(snd_card_new)、注册(snd_card_register) 和 释放(snd_card_free),并揭示其内部数据结构流转与内核交互细节。

4.1 声卡核心数据结构:struct snd_card

每个声卡在内核中由 struct snd_card 表示,该结构体是 ALSA 驱动开发者必须掌握的核心对象。下表列出其关键字段及作用:

字段类型说明
numberint声卡编号,由 ALSA 核心分配,对应 /dev/snd/controlC0 中的数字
idchar[64]声卡标识字符串,如 "HDA-Intel",用于 /proc/asound/cards 显示
driverchar[32]驱动名称,通常与模块名一致
shortnamechar[32]声卡短名称,用于用户空间识别
longnamechar[80]声卡长名称,包含更详细的硬件描述
devicesstruct list_head挂载该声卡下所有 ALSA 设备(PCM、Control、MIDI 等)的链表头
controlsstruct list_head声卡级控制项的链表头
private_datavoid *驱动私有数据指针,通常指向芯片特定的结构体
power_stateunsigned int电源管理状态,如 SNDRV_CTL_POWER_D0(正常工作)
关键理解: struct snd_card 是声卡的"容器",它不直接操作硬件,而是通过挂载其下的 PCM 设备、控制设备等子组件与硬件交互。驱动开发者的首要任务就是正确创建并初始化这个容器。

4.2 声卡创建:snd_card_new

snd_card_new 是声卡生命周期的起点。它在内核堆中分配 struct snd_card 结构体,并完成基础初始化。其函数原型如下:

int snd_card_new(struct device *parent, int idx, const char *xid,
                 struct module *module, int extra_size,
                 struct snd_card **card_ret);

参数详解:

  • parent:父设备指针,通常是 PCI 或平台设备的 &pdev->dev,用于设备模型关联。
  • idx:声卡索引号,传 -1 表示由 ALSA 核心自动分配。
  • xid:声卡标识字符串,可为 NULL。
  • module:驱动所属的内核模块,通常传 THIS_MODULE。
  • extra_size:额外私有数据的大小,ALSA 核心会将 struct snd_card 和私有数据一次性分配,提升缓存局部性。
  • card_ret:输出参数,返回创建的 struct snd_card * 指针。

内部执行流程:

  1. 调用 kzalloc 分配 sizeof(struct snd_card) + extra_size 大小的内存。
  2. 初始化链表头:devices、controls、mixer_list 等。
  3. 初始化读写锁、电源管理相关字段。
  4. 如果 idx 为 -1,调用 snd_card_assign_id 自动分配编号。
  5. 将声卡加入全局声卡链表 snd_cards,并设置 card->number。
  6. 将 extra_size 对应的内存地址赋值给 card->private_data。
  7. 返回 0 表示成功,负值表示失败。
最佳实践: 使用 extra_size 参数将芯片私有数据与 snd_card 绑定分配,避免二次分配。访问私有数据时使用 snd_card_get_private_data(card) 宏。

4.3 声卡注册:snd_card_register

当声卡下的所有子设备(PCM、Control、MIDI 等)都已创建并添加到声卡后,驱动调用 snd_card_register 将声卡正式暴露给用户空间。函数原型:

int snd_card_register(struct snd_card *card);

注册过程的核心步骤:

  • 创建设备节点:在 /dev/snd/ 下创建 controlC0、pcmC0D0p 等设备文件。
  • 注册到设备模型:将声卡作为设备挂载到 Linux 设备模型树中。
  • 创建 proc 文件:在 /proc/asound/ 下创建 card0 目录,包含 id、pcm0p 等状态文件。
  • 通知用户空间:通过 kobject_uevent 发送 ADD 事件,触发 udev 加载 ALSA 用户空间库。
  • 遍历子设备:对挂载在 card->devices 链表上的每个子设备调用其注册回调。
重要约束: 必须在调用 snd_card_register 之前完成所有子设备的创建和添加。注册后添加的设备不会自动暴露给用户空间,需要手动触发重新注册或使用 snd_device_register。

4.4 声卡释放:snd_card_free

当驱动卸载或发生致命错误时,调用 snd_card_free 释放声卡及其所有子设备资源。函数原型:

int snd_card_free(struct snd_card *card);

释放流程(按顺序执行):

  1. 设置断开标志:将 card->shutdown 置为 1,阻止新的 I/O 操作。
  2. 通知用户空间:发送 REMOVE 事件,关闭所有打开的文件句柄。
  3. 释放子设备:遍历 card->devices 链表,对每个设备调用其析构回调(snd_device_free)。
  4. 释放控制项:释放所有声卡级的控制项(snd_ctl_free)。
  5. 移除 proc 文件:删除 /proc/asound/card0 目录及其内容。
  6. 从全局链表移除:将声卡从 snd_cards 链表中摘除。
  7. 释放内存:调用 kfree 释放 struct snd_card 及私有数据。
异步释放: 如果在原子上下文(如中断处理)中需要释放声卡,应使用 snd_card_free_when_closed,它会等待所有文件句柄关闭后再执行实际释放。

4.5 声卡生命周期 SVG 流程图

下图展示了从 snd_card_new 到 snd_card_register 再到 snd_card_free 的完整生命周期,以及各阶段驱动的典型操作:

声卡生命周期流程图 snd_card_new 分配 struct snd_card 添加子设备 snd_pcm_new / snd_ctl_add snd_card_register 暴露给用户空间 正常运行 PCM 读写 / 控制操作 snd_card_free 释放所有子设备 + 内存 驱动操作要点 ① 先 new,再添加子设备 ② 最后 register ③ 错误时调用 free 回滚

4.6 典型驱动代码模板

以下代码展示了在 PCI 驱动 probe 函数中使用这三个 API 的标准模式:

static int my_pci_probe(struct pci_dev *pci, const struct pci_device_id *id)
{
    struct snd_card *card;
    struct my_chip *chip;
    int err;

    /* 步骤1: 创建声卡,extra_size 包含私有数据 */
    err = snd_card_new(&pci->dev, -1, NULL, THIS_MODULE,
                       sizeof(struct my_chip), &card);
    if (err < 0)
        return err;

    chip = snd_card_get_private_data(card);
    chip->card = card;

    /* 步骤2: 初始化硬件,创建 PCM/Control 等子设备 */
    err = snd_my_pcm_new(card, chip);
    if (err < 0)
        goto error;

    err = snd_my_mixer_new(card, chip);
    if (err < 0)
        goto error;

    /* 步骤3: 注册声卡,暴露给用户空间 */
    err = snd_card_register(card);
    if (err < 0)
        goto error;

    pci_set_drvdata(pci, card);
    return 0;

error:
    snd_card_free(card);  /* 统一错误回滚 */
    return err;
}

static void my_pci_remove(struct pci_dev *pci)
{
    struct snd_card *card = pci_get_drvdata(pci);
    snd_card_free(card);  /* 模块卸载时释放 */
}

4.7 常见错误与调试建议

  • 重复注册:对同一个 snd_card 调用两次 snd_card_register 会导致内核警告。应在注册前检查 card->registered 标志。
  • 内存泄漏:如果在 snd_card_new 之后、snd_card_register 之前发生错误,必须调用 snd_card_free 释放已分配的资源,不能直接返回。
  • 子设备未添加:注册后用户空间无法看到任何 PCM 或控制设备,通常是因为忘记调用 snd_device_new 将子设备挂载到声卡。
  • 调试手段:检查 /proc/asound/cards 确认声卡是否注册成功;使用 cat /proc/asound/card0/pcm0p/sub0/hw_params 查看 PCM 设备状态。
核心总结: snd_card_new 分配容器,snd_card_register 暴露接口,snd_card_free 清理回滚。三者构成声卡驱动的"三段式"生命周期管理,是所有 ALSA 驱动的基础骨架。