2. PCM设备创建与销毁

任务目标

在Linux内核驱动中,通过ALSA框架动态创建PCM设备(playback方向),实现基本的open/release回调,并在用户空间验证设备节点可被正常打开。本案例聚焦于PCM设备生命周期管理的最小可复现实现。

环境依赖

  • Linux内核源码树(本案例基于5.15版本验证)
  • 内核编译工具链(gcc、make、libncurses-dev等)
  • 开发板或虚拟机,支持加载内核模块
  • 用户空间工具:aplay(alsa-utils)或直接使用cat验证设备节点存在性

核心实现

以下代码创建一个ALSA声卡设备,并在其上注册一个PCM playback设备。模块加载时动态创建,卸载时销毁。PCM的open/release回调仅维护一个打开计数器,用于验证用户空间访问路径。

// pcm_create_destroy.c
#include <linux/module.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/pcm.h>

#define DRIVER_NAME "pcm_demo"

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

static int pcm_demo_open(struct snd_pcm_substream *substream)
{
    open_count++;
    pr_info("pcm_demo: open called, count=%d\n", open_count);
    return 0;
}

static int pcm_demo_close(struct snd_pcm_substream *substream)
{
    open_count--;
    pr_info("pcm_demo: close called, count=%d\n", open_count);
    return 0;
}

static const struct snd_pcm_ops pcm_demo_ops = {
    .open = pcm_demo_open,
    .close = pcm_demo_close,
};

static int __init pcm_demo_init(void)
{
    int ret;

    /* 第一步:创建声卡 */
    ret = snd_card_new(NULL, -1, NULL, THIS_MODULE, 0, &card);
    if (ret < 0) {
        pr_err("pcm_demo: snd_card_new failed: %d\n", ret);
        return ret;
    }

    /* 第二步:在声卡上创建PCM设备 */
    ret = snd_pcm_new(card, "pcm_demo_pcm", 0, 1, 0, &pcm);
    if (ret < 0) {
        pr_err("pcm_demo: snd_pcm_new failed: %d\n", ret);
        goto fail_pcm;
    }

    /* 第三步:绑定PCM操作集 */
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &pcm_demo_ops);

    /* 第四步:注册声卡,设备节点出现在/dev/snd/ */
    ret = snd_card_register(card);
    if (ret < 0) {
        pr_err("pcm_demo: snd_card_register failed: %d\n", ret);
        goto fail_register;
    }

    pr_info("pcm_demo: initialized, card#%d, PCM device 0-0\n", card->number);
    return 0;

fail_register:
    snd_pcm_free(pcm);
fail_pcm:
    snd_card_free(card);
    return ret;
}

static void __exit pcm_demo_exit(void)
{
    /* 销毁顺序:先释放PCM,再释放声卡 */
    if (pcm)
        snd_pcm_free(pcm);
    if (card)
        snd_card_free(card);
    pr_info("pcm_demo: exited\n");
}

module_init(pcm_demo_init);
module_exit(pcm_demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALSA Demo");
MODULE_DESCRIPTION("PCM device create/destroy demo");

Makefile

obj-m := pcm_create_destroy.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

编译与加载

  1. 将上述两个文件放入同一目录,执行make编译,生成pcm_create_destroy.ko。
  2. 加载模块:sudo insmod pcm_create_destroy.ko
  3. 检查内核日志确认初始化成功:dmesg | tail,应看到pcm_demo: initialized, card#N, PCM device 0-0。

用户空间验证

模块加载后,ALSA框架会在/dev/snd/下生成设备节点。执行以下命令验证PCM设备可被打开:

# 查看生成的设备节点(card编号以实际dmesg输出为准)
ls -l /dev/snd/

# 使用aplay列出PCM设备(应能看到pcm_demo设备)
aplay -l

# 直接打开设备节点验证open回调被触发
cat /dev/snd/pcmC0D0p &
sleep 0.1
dmesg | tail -5   # 应看到 "pcm_demo: open called, count=1"
kill %1
dmesg | tail -5   # 应看到 "pcm_demo: close called, count=0"

通过判定标准:

  • aplay -l能列出名为pcm_demo的playback设备;
  • 打开/dev/snd/pcmC0D0p时内核日志出现open called,关闭后出现close called;
  • 卸载模块sudo rmmod pcm_create_destroy后,设备节点消失,日志显示pcm_demo: exited。

常见问题

  • 加载失败提示No such device:确认内核已启用CONFIG_SND和CONFIG_SND_PCM,且声卡驱动框架可用。
  • 设备节点未生成:检查dmesg中是否有注册错误,确认snd_card_register成功返回。
  • 打开设备节点时阻塞:由于未实现hw_params/prepare等回调,应用层调用snd_pcm_hw_params会失败,但open()系统调用本身应正常返回。本案例仅验证open/release路径,不涉及音频数据传输。