2. PCM设备创建与销毁

任务目标

在Linux ALSA驱动中,通过snd_pcm_new()动态创建PCM设备,并在驱动卸载时通过snd_pcm_free()正确销毁,理解两者在驱动生命周期中的调用时机与配对关系。

核心概念

snd_pcm_new()在声卡初始化阶段(通常位于probe回调中)创建PCM实例,此时PCM设备尚未对用户空间可见;需再调用snd_pcm_set_ops()绑定操作集并调用snd_device_register()完成注册。销毁时,snd_pcm_free()会释放PCM实例及其关联的substream,通常在remove回调中调用。若PCM已注册,需先通过snd_device_free()注销设备,再调用snd_pcm_free()释放内存。

最小可复现实现

以下代码展示一个完整的最小驱动模块,创建单个PCM播放设备(8个substream),并在卸载时正确销毁:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <sound/core.h>
#include <sound/pcm.h>

static struct snd_card *card;
static struct snd_pcm *pcm;

static int dummy_pcm_open(struct snd_pcm_substream *substream)
{
    return 0;
}

static int dummy_pcm_close(struct snd_pcm_substream *substream)
{
    return 0;
}

static struct snd_pcm_ops dummy_pcm_ops = {
    .open = dummy_pcm_open,
    .close = dummy_pcm_close,
};

static int dummy_probe(struct platform_device *pdev)
{
    int ret;

    /* 1. 创建声卡 */
    ret = snd_card_new(&pdev->dev, -1, "dummy", THIS_MODULE, 0, &card);
    if (ret < 0)
        return ret;

    /* 2. 创建PCM设备:播放方向,8个substream */
    ret = snd_pcm_new(card, "dummy PCM", 0, 8, 0, &pcm);
    if (ret < 0) {
        snd_card_free(card);
        return ret;
    }

    /* 3. 绑定操作集 */
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &dummy_pcm_ops);

    /* 4. 注册声卡(PCM设备随之对用户空间可见) */
    ret = snd_card_register(card);
    if (ret < 0) {
        snd_pcm_free(pcm);
        snd_card_free(card);
        return ret;
    }

    return 0;
}

static void dummy_remove(struct platform_device *pdev)
{
    /* 销毁顺序:先释放PCM,再释放声卡 */
    if (pcm) {
        snd_pcm_free(pcm);
        pcm = NULL;
    }
    if (card) {
        snd_card_free(card);
        card = NULL;
    }
}

static struct platform_driver dummy_driver = {
    .probe = dummy_probe,
    .remove = dummy_remove,
    .driver = {
        .name = "dummy_pcm_drv",
    },
};

module_platform_driver(dummy_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Dummy PCM create/free example");

编译与加载

# 内核树外编译(需内核头文件与build目录)
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# 加载模块
sudo insmod dummy_pcm.ko

# 确认PCM设备已创建
cat /proc/asound/cards
ls /dev/snd/

验证方法

  • 创建验证:加载模块后,执行cat /proc/asound/cards应看到名为"dummy"的声卡;ls /dev/snd/应出现pcmC0D0p设备节点(C0=card0,D0=device0,p=playback)。
  • 销毁验证:执行sudo rmmod dummy_pcm后,再次查看/proc/asound/cards与/dev/snd/,确认声卡与PCM设备节点均已消失。
  • 错误路径验证:若snd_pcm_new失败(如内存不足),代码会调用snd_card_free回滚,不会泄漏声卡资源。

调用时机要点

  • snd_pcm_new()必须在snd_card_new()之后调用,因为PCM实例挂载在声卡上。
  • snd_pcm_free()必须在snd_card_free()之前调用,否则声卡释放时会尝试访问已释放的PCM,导致内核崩溃。
  • 若PCM已通过snd_card_register()注册,直接调用snd_pcm_free()即可——该函数内部会先注销设备再释放内存,无需手动调用snd_device_free()。
  • 在probe失败路径中,若snd_pcm_new成功但后续snd_card_register失败,需手动调用snd_pcm_free()再snd_card_free(),避免资源泄漏。

常见问题

  • 重复释放:snd_pcm_free()内部会从声卡设备链表中移除PCM,重复调用会导致空指针解引用。建议释放后将指针置NULL。
  • substream数量:snd_pcm_new第4、5参数分别指定播放和捕获方向的substream数量,本案例仅播放方向(8个),捕获方向为0。
  • 设备节点编号:PCM设备编号从0开始,由声卡内PCM实例的创建顺序决定,与snd_pcm_new的device参数(本案例为0)对应。