4. snd_card创建与管理

在 ALSA 音频驱动框架中,snd_card 是声卡设备的核心抽象结构体。每一个物理声卡(或虚拟声卡)在驱动层都对应一个 struct snd_card 实例。本章将深入剖析 snd_card 的创建、注册、销毁全流程,帮助开发者理解 card 实例从出生到消亡的完整生命周期。

4.1 snd_card 结构体概览

struct snd_card 定义在 include/sound/core.h 中,是 ALSA 驱动中最大的管理结构体之一。它聚合了声卡的所有子组件:PCM 设备、控制接口、MIDI 设备、AC97 编解码器、电源管理状态等。下表列出了其关键字段:

字段名 类型 说明
number int 声卡编号,由 ALSA 核心分配,对应 /dev/snd/controlC0 中的数字
id char[64] 声卡标识字符串,如 "HDA-Intel",用于用户空间识别
driver char[32] 驱动名称,通常与模块名一致
shortname char[32] 声卡短名称,显示在 /proc/asound/cards 中
longname char[80] 声卡长名称,包含总线位置等详细信息
devices struct device Linux 设备模型中的 device 结构体
controls_list struct list_head 控制链表头,挂载所有 snd_kcontrol 实例
pcm_devs int 已注册的 PCM 设备数量
power_state unsigned int 电源管理状态,用于 D0/D3 切换
关键认知: snd_card 不仅是声卡的逻辑容器,更是 ALSA 与 Linux 设备模型之间的桥梁。通过 card->devices,声卡被挂载到 sysfs 文件系统中,用户空间可以通过 /sys/class/sound/card0/ 访问其属性。

4.2 snd_card_new():声卡对象的诞生

snd_card_new() 是创建 snd_card 实例的入口函数。驱动通常在探测函数(probe)中调用它。其函数原型如下:

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、Platform 设备)
  • idx:声卡编号索引,传 -1 表示由 ALSA 核心自动分配
  • xid:声卡 ID 字符串,可为 NULL
  • module:所属内核模块,通常传 THIS_MODULE
  • extra_size:额外私有数据大小,ALSA 会将其与 snd_card 一起分配
  • card_ret:输出参数,返回创建的 snd_card 指针
最佳实践: 使用 extra_size 参数将驱动私有数据与 snd_card 绑定在同一内存块中,避免二次分配。通过 snd_card_get_private() 宏可以方便地获取私有数据指针。

调用 snd_card_new() 后,ALSA 核心会执行以下步骤:

  1. 通过 kzalloc() 分配 sizeof(struct snd_card) + extra_size 大小的内存
  2. 初始化 card 内部的链表头(controls、pcm、rawmidi 等)
  3. 初始化电源管理相关字段
  4. 将 card 加入 ALSA 核心的全局 card 链表
  5. 分配声卡编号(如果 idx 为 -1)
  6. 设置 card 的 parent 设备指针

4.3 snd_card_register():声卡对外可见

创建 snd_card 并添加完所有子组件(PCM、控制、MIDI 等)后,驱动必须调用 snd_card_register() 使声卡对用户空间可见。函数原型:

int snd_card_register(struct snd_card *card);

注册过程的核心动作:

  • 在 sysfs 中创建声卡设备节点
  • 在 /dev/snd/ 下创建设备文件(如 controlC0)
  • 在 /proc/asound/ 中创建 card 信息文件
  • 触发所有已添加子组件的注册回调(如 PCM 设备的 snd_pcm_dev_register())
常见错误: 在调用 snd_card_register() 之前,必须确保所有子组件(PCM、控制、MIDI 等)已经通过对应的 snd_device_new() 或专用 API 添加到 card 中。注册后添加的组件可能无法正确暴露给用户空间。

4.4 snd_card_free():声卡的销毁

当声卡被移除(如 PCI 热拔插、驱动卸载)时,驱动调用 snd_card_free() 释放所有资源。函数原型:

int snd_card_free(struct snd_card *card);

销毁流程按逆序执行:

  1. 从 ALSA 全局 card 链表中移除该 card
  2. 调用所有已注册设备的 free 回调,释放子组件资源
  3. 移除 sysfs 设备节点和 /dev/snd/ 下的设备文件
  4. 释放 snd_card 结构体内存
异步安全: ALSA 提供了 snd_card_free_when_closed() 变体,它会等待所有打开的文件句柄关闭后才真正释放 card。这在热拔插场景中非常有用,避免在用户空间仍在访问时销毁设备。

4.5 card 实例的生命周期 SVG 图

下图展示了 snd_card 从创建到销毁的完整生命周期,以及关键 API 的调用时机:

snd_card 生命周期流程图 snd_card 生命周期与关键 API 调用流程 snd_card_new() 分配内存 & 初始化 添加子组件 PCM / 控制 / MIDI snd_card_register() 暴露给用户空间 正常运行 用户空间访问 snd_card_free() 释放所有资源 生命周期关键约束 snd_card_new() → 添加子组件 → snd_card_register() → 正常运行 → snd_card_free()

4.6 生命周期管理对比表

下表总结了三个核心 API 的职责、调用时机和注意事项:

API 职责 调用时机 注意事项
snd_card_new() 分配并初始化 card 结构体 probe 函数最开头 可指定 extra_size 绑定私有数据;失败时返回负错误码
snd_card_register() 将 card 注册到 ALSA 核心和 sysfs 所有子组件添加完成后 注册后不能再添加新的子组件;调用前必须设置 shortname 和 longname
snd_card_free() 释放 card 及其所有子组件资源 remove 函数中 确保没有正在进行的 I/O 操作;考虑使用 snd_card_free_when_closed()

4.7 典型驱动代码模板

以下代码展示了在 PCI 驱动 probe/remove 函数中使用 snd_card 的标准模式:

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

    // 步骤1: 创建 snd_card,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(card);
    chip->card = card;

    // 步骤2: 添加子组件(PCM、控制、MIDI 等)
    err = snd_pcm_new(card, "my_pcm", 0, 1, 1, &chip->pcm);
    if (err < 0)
        goto error;

    // 步骤3: 设置 card 名称
    strcpy(card->shortname, "My Audio Card");
    sprintf(card->longname, "My Audio Card at %s", pci_name(pci));

    // 步骤4: 注册 card
    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 mycard_remove(struct pci_dev *pci)
{
    struct snd_card *card = pci_get_drvdata(pci);
    snd_card_free(card);
}
错误处理要点: 在 probe 函数中,任何子组件创建失败后,必须调用 snd_card_free() 释放已分配的资源。ALSA 核心会递归释放所有已注册的子组件,驱动无需手动逐个释放。

4.8 多声卡场景与编号管理

当系统中有多个声卡时,ALSA 核心通过 snd_card 的 number 字段区分不同声卡。编号分配策略如下:

  • 自动分配:idx = -1,ALSA 核心从 0 开始查找第一个未使用的编号
  • 固定编号:指定具体编号(如 idx = 2),如果该编号已被占用则返回错误
  • 模块参数覆盖:驱动可通过模块参数 index 数组让用户指定编号

编号冲突是驱动开发中的常见问题。建议在 probe 函数中打印 card 编号以便调试:

dev_info(card->dev, "Card created: number=%d, id=%s\n",
         card->number, card->id);

4.9 生命周期中的状态迁移 SVG 图

下图从状态机角度展示 snd_card 的内部状态变化:

snd_card 状态迁移图 snd_card 内部状态迁移 SND_CARD_STATE_NEW 刚创建,未注册 SND_CARD_STATE_REGISTERED 已注册,可访问 SND_CARD_STATE_DISCONNECTED 正在释放资源 snd_card_register() snd_card_free() 状态迁移说明 • SND_CARD_STATE_NEW:card 已分配内存,但未对用户空间可见。此时可添加子组件。 • SND_CARD_STATE_REGISTERED:card 已注册,用户空间可通过设备文件访问。此时不能再添加子组件。 • SND_CARD_STATE_DISCONNECTED:card 正在被销毁,所有 I/O 操作应返回错误。 • 状态迁移是单向的:NEW → REGISTERED → DISCONNECTED,不可逆。

4.10 常见问题与调试技巧

内存泄漏风险: 如果在 snd_card_new() 之后、snd_card_register() 之前 probe 失败,必须调用 snd_card_free()。ALSA 核心不会自动清理未注册的 card。建议在 probe 函数中使用 goto 错误处理模式。

调试 snd_card 相关问题时,以下方法非常有效:

  • 查看 /proc/asound/cards:列出所有已注册的声卡及其编号、名称
  • 查看 /proc/asound/cardX/:每个 card 的详细 proc 信息
  • 启用 ALSA 调试:通过 #define CONFIG_SND_DEBUG 和 snd_printd() 输出调试信息
  • 使用 ls -l /dev/snd/:确认设备文件是否创建成功
进阶技巧: 在 snd_card_new() 中指定 extra_size 时,私有数据结构体应包含 struct snd_card *card 字段,方便在回调函数中反向获取 card 指针。这种双向引用模式是 ALSA 驱动的标准设计。