4. snd_card 创建与管理:snd_card_new()、snd_card_register()、snd_card_free()、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 核心会执行以下步骤:
- 通过
kzalloc()分配sizeof(struct snd_card) + extra_size大小的内存 - 初始化 card 内部的链表头(controls、pcm、rawmidi 等)
- 初始化电源管理相关字段
- 将 card 加入 ALSA 核心的全局 card 链表
- 分配声卡编号(如果 idx 为 -1)
- 设置 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);
销毁流程按逆序执行:
- 从 ALSA 全局 card 链表中移除该 card
- 调用所有已注册设备的
free回调,释放子组件资源 - 移除 sysfs 设备节点和
/dev/snd/下的设备文件 - 释放
snd_card结构体内存
snd_card_free_when_closed() 变体,它会等待所有打开的文件句柄关闭后才真正释放 card。这在热拔插场景中非常有用,避免在用户空间仍在访问时销毁设备。
4.5 card 实例的生命周期 SVG 图
下图展示了 snd_card 从创建到销毁的完整生命周期,以及关键 API 的调用时机:
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);
}
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 的内部状态变化:
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 驱动的标准设计。