Auxiliary Bus (보조 버스) 심화

auxiliary_bus는 Linux 5.11에서 도입된 버스 프레임워크로, 하나의 물리 디바이스(주로 PCIe)가 제공하는 여러 기능(네트워크, RDMA, crypto, vDPA 등)을 독립적인 커널 드라이버에 분배합니다. auxiliary_deviceauxiliary_driver 자료구조, modname.function.id 명명 규칙, 2단계 등록(init+add), probe/remove 생명주기, 고유 메모리 소유권 모델, sysfs 표현, MLX5·ICE·SOF 실제 활용 사례, 대안 버스 비교, 디버깅 기법까지 종합적으로 다룹니다.

커널 소스: drivers/base/auxiliary.c, include/linux/auxiliary_bus.h — Linux 5.11 (commit 7de3697e) 이후. 관련 상위 개념은 커널 버스 서브시스템 문서를 참고하세요.
전제 조건: 디바이스 드라이버커널 버스 서브시스템 문서를 먼저 읽으세요. auxiliary_bus는 Linux Device Model의 bus_type, device, device_driver 개념 위에 구축됩니다.
일상 비유: 하나의 회사 건물(PCIe 디바이스)에 여러 부서(네트워크팀, RDMA팀, 암호화팀)가 입주합니다. 각 부서는 독립적으로 운영되지만, 전기·수도 등 공용 시설(공유 레지스터, 인터럽트)은 건물 관리사무소(부모 드라이버)가 중앙 관리합니다. auxiliary_bus는 바로 이 "입주 관리 시스템"입니다.

핵심 요약

  • auxiliary_device — 부모 디바이스가 등록하는 가상 하위 디바이스입니다. 각 기능(eth, rdma, crypto 등)을 나타냅니다.
  • auxiliary_driver — 특정 기능에 바인딩되는 드라이버입니다. id_table로 매칭되며, probe/remove 콜백을 구현합니다.
  • id_table"modname.function" 형식의 문자열로 드라이버와 디바이스를 매칭합니다.
  • 2단계 등록auxiliary_device_init()으로 초기화한 후 auxiliary_device_add()로 버스에 등록합니다. 분리된 에러 경로를 제공합니다.
  • release 콜백auxiliary_device.dev.release는 반드시 설정해야 합니다. 마지막 참조가 해제될 때 메모리를 정리하며, 설정하지 않으면 커널 경고가 발생합니다.

단계별 이해

  1. 개요에서 auxiliary_bus가 왜 필요한지, MFD/platform bus의 한계를 파악합니다.
  2. 아키텍처 개요로 전체 구조(부모→버스→드라이버)를 시각적으로 이해합니다.
  3. 핵심 자료구조에서 구조체 필드를 하나씩 분석합니다.
  4. 디바이스 등록드라이버 등록으로 실제 코드 흐름을 따라갑니다.

개요 및 도입 배경

NVIDIA ConnectX, Intel ICE, SOF 오디오 등 최신 하드웨어는 하나의 PCIe 디바이스가 네트워킹, RDMA, 암호화, virtio 에뮬레이션 등 여러 독립 기능을 동시에 제공합니다. Linux 5.11 이전에는 이러한 다기능 디바이스를 분할하기 위해 platform bus나 MFD를 사용했으나, 두 접근 모두 구조적 한계가 있었습니다.

Platform Bus MFD Auxiliary Bus PCIe Device (부모) platform_device (가짜) 기능 드라이버 문제점 NUMA 노드 정보 손실 IOMMU 그룹 분리됨 DT/ACPI 없으면 부자연스러움 부모 전원 관리 단절 PCIe Device (부모) mfd_cell → platform_device 기능 드라이버 문제점 리소스 공유 모델 복잡 드라이버가 같은 모듈에 묶임 IRQ 도메인 공유 어려움 platform_device에 의존 PCIe Device (부모) auxiliary_device auxiliary_driver 장점 부모 NUMA/IOMMU 상속 독립 모듈 로드/언로드 DT/ACPI 불필요 부모 전원 관리 연동

Kconfig 설정

# drivers/base/Kconfig
config AUXILIARY_BUS
    bool
    # 대부분의 드라이버가 select AUXILIARY_BUS 로 자동 활성화

# 예: drivers/net/ethernet/mellanox/mlx5/core/Kconfig
config MLX5_CORE
    tristate "Mellanox 5th generation network adapters"
    select AUXILIARY_BUS
/* 드라이버에서 auxiliary_bus 사용 시 필요한 헤더 */
#include <linux/auxiliary_bus.h>

아키텍처 개요

auxiliary_bus는 Linux Device Model의 bus_type으로 등록됩니다. 부모 드라이버(예: mlx5_core)가 auxiliary_device를 생성하면, auxiliary_bus 코어가 등록된 auxiliary_driverid_table을 확인하여 매칭되는 드라이버의 probe()를 호출합니다.

부모 PCI 드라이버 (예: mlx5_core, ice) auxiliary_device_add() auxiliary_device mlx5_core.eth.0 auxiliary_device mlx5_core.rdma.0 auxiliary_device mlx5_core.vnet.0 auxiliary_bus (bus_type) id_table strcmp 매칭 auxiliary_driver mlx5e (이더넷) auxiliary_driver mlx5_ib (RDMA) auxiliary_driver mlx5_vnet (vDPA) → probe() 호출 → probe() 호출 → probe() 호출 부모가 디바이스 등록 → 버스가 드라이버 매칭 → probe() 실행
/* drivers/base/auxiliary.c — 버스 타입 정의 */
static const struct bus_type auxiliary_bus_type = {
    .name   = "auxiliary",
    .probe  = auxiliary_bus_probe,
    .remove = auxiliary_bus_remove,
    .match  = auxiliary_match,
    .uevent = auxiliary_uevent,
};

핵심 자료구조

auxiliary_bus의 핵심은 세 가지 구조체입니다: auxiliary_device(디바이스), auxiliary_driver(드라이버), auxiliary_device_id(매칭 테이블). 이들의 관계를 이해하면 전체 프레임워크를 파악할 수 있습니다.

auxiliary_device struct device dev; const char *name; u32 id; dev.parent → 부모 device dev.release → 필수 설정! sysfs name: "modname.function.id" auxiliary_driver int (*probe)(adev, id); void (*remove)(adev); void (*shutdown)(adev); int (*suspend)(adev, msg); int (*resume)(adev); const char *name; struct device_driver driver; const auxdev_id *id_table; auxiliary_device_id char name[32]; "modname.function" kernel_ulong_t driver_data; id_table 참조 매칭 알고리즘 (auxiliary_match) strcmp(adev_name, id->name) == 0 ? adev_name = "modname.function" (id 제외) id->name = "modname.function"

auxiliary_device 구조체

/* include/linux/auxiliary_bus.h */
struct auxiliary_device {
    struct device dev;
    const char *name;   /* "function" 부분 — 예: "eth", "rdma" */
    u32 id;              /* 인스턴스 번호 — 예: 0, 1, 88 */
};
코드 설명
  • 3행 dev — 임베디드된 struct device. dev.parent에 부모 디바이스를 설정하고, dev.release에 해제 콜백을 반드시 지정해야 합니다.
  • 4행 name — 기능 이름 문자열. 버스에서 최종 sysfs 이름은 "KBUILD_MODNAME.name.id" 형식으로 구성됩니다.
  • 5행 id — 동일 기능의 여러 인스턴스를 구별하는 번호. MLX5의 Scalable Function은 SF 번호를 id로 사용합니다.

auxiliary_driver 구조체

struct auxiliary_driver {
    int (*probe)(struct auxiliary_device *auxdev,
                const struct auxiliary_device_id *id);
    void (*remove)(struct auxiliary_device *auxdev);
    void (*shutdown)(struct auxiliary_device *auxdev);
    int (*suspend)(struct auxiliary_device *auxdev, pm_message_t state);
    int (*resume)(struct auxiliary_device *auxdev);
    const char *name;
    struct device_driver driver;
    const struct auxiliary_device_id *id_table;
};

auxiliary_device_id 구조체

struct auxiliary_device_id {
    char name[32];           /* "modname.function" 형식 */
    kernel_ulong_t driver_data; /* 드라이버별 사용자 데이터 */
};

디바이스 명명 규칙

auxiliary_device의 sysfs 이름은 세 부분으로 구성됩니다:

KBUILD_MODNAME . name . id
───────────────  ────   ──
  부모 모듈명     기능   인스턴스
                 이름    번호

예: mlx5_core.eth.0
    mlx5_core.rdma.0
    mlx5_core.sf.88
    ice.rdma.0
주의: 매칭 시에는 id 부분이 제외됩니다. 즉, id_tablename"mlx5_core.eth"처럼 "modname.function"만 포함합니다. 같은 기능의 여러 인스턴스(eth.0, eth.1)는 모두 같은 드라이버에 매칭됩니다.
부모 모듈기능 이름sysfs 디바이스명바인딩 드라이버
mlx5_coreethmlx5_core.eth.0mlx5e (이더넷)
mlx5_corerdmamlx5_core.rdma.0mlx5_ib (RDMA)
mlx5_corevnetmlx5_core.vnet.0mlx5_vnet (vDPA)
mlx5_coresfmlx5_core.sf.88mlx5_sf (Scalable Function)
icerdmaice.rdma.0irdma (Intel RDMA)
snd_sofdmasnd_sof.dma.0SOF DMA engine
idxdwqidxd.wq.0IDXD work queue

디바이스 등록 과정

디바이스 등록은 2단계로 나뉩니다: auxiliary_device_init()으로 struct device를 초기화한 후, auxiliary_device_add()로 버스에 등록합니다. 이 분리 덕분에 실패 시 정리 경로가 명확합니다.

1. 구조체 할당 kzalloc(container) 2. 필드 설정 name, id, parent, release 3. init() auxiliary_device_init() 실패 → kfree(container) 4. add() auxiliary_device_add() 실패 → auxiliary_device_uninit() (내부에서 put_device → release 호출) 등록 완료 버스에 디바이스 추가됨 해제: auxiliary_device_delete() + _uninit()
/* 부모 드라이버에서 auxiliary_device 등록 — 전체 패턴 */
struct my_aux_container {
    struct auxiliary_device auxdev;
    /* 부모와 공유할 추가 데이터 */
    struct my_parent_dev *parent;
    void __iomem *shared_regs;
};

static void my_aux_release(struct device *dev)
{
    struct auxiliary_device *auxdev = container_of(dev, struct auxiliary_device, dev);
    struct my_aux_container *c = container_of(auxdev, struct my_aux_container, auxdev);

    kfree(c);
}

static int my_parent_create_aux(struct my_parent_dev *pdev)
{
    struct my_aux_container *c;
    int ret;

    c = kzalloc(sizeof(*c), GFP_KERNEL);
    if (!c)
        return -ENOMEM;

    c->parent = pdev;
    c->shared_regs = pdev->regs;

    c->auxdev.name = "eth";
    c->auxdev.id = 0;
    c->auxdev.dev.parent = &pdev->pci_dev->dev;
    c->auxdev.dev.release = my_aux_release;

    ret = auxiliary_device_init(&c->auxdev);
    if (ret) {
        kfree(c);           /* init 실패: 직접 free */
        return ret;
    }

    ret = auxiliary_device_add(&c->auxdev);
    if (ret) {
        auxiliary_device_uninit(&c->auxdev);  /* add 실패: put_device → release */
        return ret;
    }

    pdev->aux_eth = &c->auxdev;
    return 0;
}
코드 설명
  • 2-7행 auxiliary_device를 감싸는 컨테이너 구조체. 부모와 공유할 데이터(shared_regs 등)를 같이 담습니다.
  • 9-15행 release 콜백. container_of로 컨테이너를 찾아 kfree합니다. 이 콜백이 없으면 커널이 "Device has no release() function" 경고를 출력합니다.
  • 27-30행 기능 이름("eth"), 인스턴스(0), 부모 디바이스, release 콜백을 설정합니다.
  • 32-35행 auxiliary_device_init()device_initialize()를 호출합니다. 실패 시 refcount가 올라가지 않았으므로 kfree()로 직접 해제합니다.
  • 37-40행 auxiliary_device_add()device_add()를 호출합니다. 실패 시 auxiliary_device_uninit()put_device()를 호출하고, refcount가 0이 되면 release 콜백이 메모리를 해제합니다.
/* 부모 드라이버 제거 시 — 역순으로 정리 */
static void my_parent_destroy_aux(struct my_parent_dev *pdev)
{
    auxiliary_device_delete(pdev->aux_eth);   /* device_del(): 버스에서 제거 */
    auxiliary_device_uninit(pdev->aux_eth);  /* put_device(): refcount 감소 → release */
}

드라이버 등록과 매칭

auxiliary_driverauxiliary_driver_register()로 등록합니다. 편의 매크로 module_auxiliary_driver()를 사용하면 모듈 init/exit를 자동 생성합니다.

새 디바이스 또는 드라이버 등록 auxiliary_match(dev, drv) bus_type.match 콜백 디바이스 이름 구성: "KBUILD_MODNAME.name" (id 제외) 예: "mlx5_core.eth" id_table 순회 for each id in driver->id_table: strcmp(dev_name, id->name) == 0 ? Yes probe() No 다음 id
/* 드라이버 등록 예시 — mlx5e 이더넷 */
static const struct auxiliary_device_id mlx5e_id_table[] = {
    { .name = "mlx5_core.eth" },
    {},  /* 센티넬 (빈 항목으로 종료) */
};

MODULE_DEVICE_TABLE(auxiliary, mlx5e_id_table);

static struct auxiliary_driver mlx5e_driver = {
    .name     = "eth",
    .probe    = mlx5e_probe,
    .remove   = mlx5e_remove,
    .id_table = mlx5e_id_table,
};

module_auxiliary_driver(mlx5e_driver);
코드 설명
  • 2-5행 id_table은 NULL name 센티넬로 종료되는 배열입니다. "mlx5_core.eth"라는 이름의 모든 auxiliary_device와 매칭됩니다.
  • 7행 MODULE_DEVICE_TABLE은 모듈 자동 로딩을 위한 alias를 생성합니다. modalias"auxiliary:mlx5_core.eth" 형식입니다.
  • 16행 module_auxiliary_driver()module_init/module_exit에서 auxiliary_driver_register()/_unregister()를 호출하는 코드를 자동 생성합니다.
/* drivers/base/auxiliary.c — 매칭 구현 */
static int auxiliary_match(struct device *dev, struct device_driver *drv)
{
    struct auxiliary_device *auxdev = to_auxiliary_dev(dev);
    struct auxiliary_driver *auxdrv = to_auxiliary_drv(drv);

    return !!auxiliary_match_id(auxdrv->id_table, auxdev->name);
}

static const struct auxiliary_device_id *auxiliary_match_id(
    const struct auxiliary_device_id *id, const char *name)
{
    while (id->name[0]) {
        if (!strcmp(name, id->name))
            return id;
        id++;
    }
    return NULL;
}

probe/remove 콜백

매칭된 드라이버의 probe()가 호출되면, container_of 매크로로 부모가 제공한 컨테이너 구조체에 접근하여 공유 리소스(레지스터, 인터럽트)를 사용할 수 있습니다.

/* probe 콜백 구현 패턴 */
struct my_func_priv {
    struct net_device *netdev;
    void __iomem *regs;
};

static int my_func_probe(struct auxiliary_device *auxdev,
                        const struct auxiliary_device_id *id)
{
    struct my_aux_container *c =
        container_of(auxdev, struct my_aux_container, auxdev);
    struct my_func_priv *priv;

    /* devm: 디바이스 해제 시 자동 free */
    priv = devm_kzalloc(&auxdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 부모 공유 리소스 접근 */
    priv->regs = c->shared_regs;

    /* 기능별 초기화: 예를 들어 netdev 생성 */
    priv->netdev = alloc_etherdev(0);
    if (!priv->netdev)
        return -ENOMEM;

    dev_set_drvdata(&auxdev->dev, priv);
    return register_netdev(priv->netdev);
}

static void my_func_remove(struct auxiliary_device *auxdev)
{
    struct my_func_priv *priv = dev_get_drvdata(&auxdev->dev);

    unregister_netdev(priv->netdev);
    free_netdev(priv->netdev);
    /* devm 할당은 자동 해제됨 */
}
코드 설명
  • 10-11행 container_of로 auxiliary_device를 감싸는 부모 컨테이너를 역참조합니다. 이를 통해 부모가 제공한 공유 레지스터 등에 접근합니다.
  • 15행 devm_kzalloc은 디바이스 생명주기에 바인딩된 관리 메모리 할당입니다. remove 시 자동 해제되므로 별도 free가 필요 없습니다.
  • 27행 dev_set_drvdata로 private 데이터를 디바이스에 연결합니다. remove에서 dev_get_drvdata로 꺼냅니다.
  • 32-37행 remove는 probe의 역순으로 정리합니다. netdev를 해제하고, devm 할당은 프레임워크가 자동 정리합니다.
/* module_auxiliary_driver 매크로 확장 */
#define module_auxiliary_driver(__auxiliary_driver) \
    module_driver(__auxiliary_driver, auxiliary_driver_register, \
                  auxiliary_driver_unregister)

/* 수동 등록이 필요한 경우 */
static int __init my_func_init(void)
{
    /* 추가 초기화 작업 후 드라이버 등록 */
    return auxiliary_driver_register(&my_func_driver);
}
module_init(my_func_init);

메모리 모델과 release 콜백

auxiliary_bus에서 가장 주의할 점은 메모리 소유권입니다. auxiliary_device에 내장된 struct device는 참조 카운트로 관리되므로, 메모리 해제 시점이 kfree 호출이 아닌 release 콜백에서 결정됩니다.

시간 refcount 0 1 2 0 kzalloc() device_init() device_add() probe() → get_device remove() device_del() put_device() release() → kfree(container) 잘못된 패턴 auxiliary_device_delete(adev) auxiliary_device_uninit(adev) kfree(container) ← UAF 위험! 다른 코드가 아직 참조를 보유 중이면 해제된 메모리에 접근 (Use-After-Free) 올바른 패턴 auxiliary_device_delete(adev) auxiliary_device_uninit(adev) → put_device() → refcount-- refcount == 0 → release() → kfree() 마지막 참조 해제 후에만 메모리 해제 안전하게 UAF 방지
/* release 콜백 — 반드시 설정해야 하는 이유 */
static void my_aux_release(struct device *dev)
{
    struct auxiliary_device *auxdev = container_of(dev, struct auxiliary_device, dev);
    struct my_aux_container *c = container_of(auxdev, struct my_aux_container, auxdev);

    /* refcount == 0 일 때만 호출됨 → 안전하게 해제 */
    kfree(c);
}

/* release를 설정하지 않으면 커널이 경고를 출력합니다:
 * "Device 'xxx' does not have a release() function, it is broken
 *  and must be fixed. See Documentation/core-api/kobject.rst."
 */
/* 잘못된 패턴 — 절대 하지 마세요! */
auxiliary_device_delete(adev);
auxiliary_device_uninit(adev);
kfree(container);  /* ⚠️ release()와 이중 해제 또는 UAF! */

전원 관리 통합

auxiliary_device는 부모의 struct devicedev.parent로 참조하므로, PM(Power Management) 계층이 자연스럽게 상속됩니다. 부모가 suspend되면 자식 auxiliary_device도 먼저 suspend되고, resume은 역순입니다.

/* auxiliary_driver에서 전원 관리 콜백 구현 */
static int my_func_suspend(struct auxiliary_device *auxdev, pm_message_t state)
{
    struct my_func_priv *priv = dev_get_drvdata(&auxdev->dev);

    netif_device_detach(priv->netdev);
    /* DMA 링 비우기, 인터럽트 비활성화 등 */
    return 0;
}

static int my_func_resume(struct auxiliary_device *auxdev)
{
    struct my_func_priv *priv = dev_get_drvdata(&auxdev->dev);

    /* 하드웨어 재초기화, DMA 링 복원 */
    netif_device_attach(priv->netdev);
    return 0;
}

static struct auxiliary_driver my_func_driver = {
    .name     = "eth",
    .probe    = my_func_probe,
    .remove   = my_func_remove,
    .suspend  = my_func_suspend,
    .resume   = my_func_resume,
    .id_table = my_func_id_table,
};

sysfs 표현

auxiliary_bus에 등록된 디바이스와 드라이버는 /sys/bus/auxiliary/ 아래에서 확인할 수 있습니다.

# auxiliary_bus 디바이스 목록
$ ls /sys/bus/auxiliary/devices/
mlx5_core.eth.0  mlx5_core.rdma.0  mlx5_core.vnet.0  ice.rdma.0

# 특정 디바이스 상세 정보
$ ls -la /sys/bus/auxiliary/devices/mlx5_core.eth.0/
driver -> ../../../../bus/auxiliary/drivers/mlx5_core.eth
subsystem -> ../../../../bus/auxiliary
power/
uevent

# 등록된 드라이버 목록
$ ls /sys/bus/auxiliary/drivers/
mlx5_core.eth  mlx5_core.rdma  mlx5_core.vnet

# MODALIAS 확인 (자동 모듈 로딩용)
$ cat /sys/bus/auxiliary/devices/mlx5_core.eth.0/uevent
MODALIAS=auxiliary:mlx5_core.eth

# 부모 디바이스 확인
$ readlink /sys/bus/auxiliary/devices/mlx5_core.eth.0/device
../../../0000:03:00.0

실제 활용: NVIDIA MLX5

MLX5(ConnectX-6/7, BlueField DPU)는 auxiliary_bus의 가장 대표적인 사용자입니다. 하나의 PCIe 디바이스에서 이더넷, RDMA, vDPA, Scalable Function, crypto 등 5가지 이상의 독립 기능을 제공합니다.

mlx5_core (PCIe 드라이버) 0000:03:00.0 — ConnectX-7 eth.0 mlx5e 드라이버 → enp3s0f0 (netdev) rdma.0 mlx5_ib 드라이버 → mlx5_0 (RoCE) vnet.0 mlx5_vnet 드라이버 → vDPA (virtio) sf.88 mlx5_sf 드라이버 → Scalable Function crypto.0 crypto 오프로드 → IPsec/TLS HW Scalable Function 내부 독립 mlx5_core_dev → eth + rdma + crypto devlink으로 SF 생성/삭제 devlink port function set sf 공유 리소스 (부모 관리) PCIe BAR 레지스터, FW 커맨드 인터페이스 EQ(Event Queue), Health 모니터, devlink eSwitch, Flow Steering Table
/* drivers/net/ethernet/mellanox/mlx5/core/dev.c */
/* MLX5에서 auxiliary_device 등록 */
static int mlx5_add_adev(struct mlx5_core_dev *dev, int idx)
{
    struct mlx5_adev *madev;
    int ret;

    madev = kzalloc(sizeof(*madev), GFP_KERNEL);
    if (!madev)
        return -ENOMEM;

    madev->mdev = dev;
    madev->idx = idx;

    madev->adev.name = mlx5_adev_devices[idx].suffix;  /* "eth", "rdma" 등 */
    madev->adev.id = 0;
    madev->adev.dev.parent = dev->device;
    madev->adev.dev.release = mlx5_adev_release;

    ret = auxiliary_device_init(&madev->adev);
    if (ret) {
        kfree(madev);
        return ret;
    }

    ret = auxiliary_device_add(&madev->adev);
    if (ret) {
        auxiliary_device_uninit(&madev->adev);
        return ret;
    }

    return 0;
}
/* MLX5 이더넷 드라이버의 probe */
static int mlx5e_probe(struct auxiliary_device *adev,
                      const struct auxiliary_device_id *id)
{
    struct mlx5_adev *edev = container_of(adev, struct mlx5_adev, adev);
    struct mlx5_core_dev *mdev = edev->mdev;
    struct net_device *netdev;
    struct mlx5e_priv *priv;

    netdev = mlx5e_create_netdev(mdev, 0);
    if (!netdev)
        return -ENOMEM;

    priv = netdev_priv(netdev);
    /* mdev의 공유 리소스(FW cmd, EQ, Flow Table)를 사용하여 초기화 */
    mlx5e_build_nic_params(priv, mdev);
    mlx5e_build_nic_netdev(netdev);

    return register_netdev(netdev);
}
/* MLX5 Scalable Function — auxiliary_device 위에 다시 mlx5_core를 초기화 */
static int mlx5_sf_dev_probe(struct auxiliary_device *adev,
                             const struct auxiliary_device_id *id)
{
    struct mlx5_sf_dev *sf_dev = container_of(adev, struct mlx5_sf_dev, adev);
    struct mlx5_core_dev *mdev;

    /* SF용 독립 mlx5_core_dev 생성 → 다시 eth + rdma auxiliary 등록 */
    mdev = mlx5_sf_dev_to_mdev(sf_dev);
    return mlx5_init_one(mdev);
}

실제 활용: Intel ICE 및 SOF

Intel ICE — RDMA 분리

Intel E800 시리즈(ICE 드라이버)는 이더넷 기능은 자체 처리하고, RDMA(iWARP/RoCEv2) 기능만 auxiliary_device로 노출하여 irdma 드라이버에 위임합니다.

/* drivers/net/ethernet/intel/ice/ice_idc.c */
static int ice_plug_aux_dev(struct ice_pf *pf)
{
    struct iidc_auxiliary_dev *iadev;

    iadev = kzalloc(sizeof(*iadev), GFP_KERNEL);
    iadev->pf = pf;

    iadev->adev.name = "rdma";
    iadev->adev.id = pf->aux_idx;
    iadev->adev.dev.parent = &pf->pdev->dev;
    iadev->adev.dev.release = ice_adev_release;

    auxiliary_device_init(&iadev->adev);
    return auxiliary_device_add(&iadev->adev);
}

/* irdma 드라이버 id_table */
static const struct auxiliary_device_id irdma_auxiliary_id_table[] = {
    { .name = "ice.rdma" },
    {},
};

Sound Open Firmware (SOF)

Intel SOF 오디오 드라이버는 DSP 기능을 auxiliary_device로 노출하여 DMA 엔진과 코덱 드라이버를 분리합니다. 이를 통해 같은 DSP 하드웨어에 대해 여러 오디오 파이프라인을 독립적으로 관리합니다.

/* sound/soc/sof/sof-client.c — SOF 클라이언트 등록 */
static int sof_client_dev_register(struct sof_client_dev *cdev,
                                   const char *name, u32 id)
{
    cdev->auxdev.name = name;        /* "dma", "ipc-flood" 등 */
    cdev->auxdev.id = id;
    cdev->auxdev.dev.release = sof_client_auxdev_release;
    cdev->auxdev.dev.parent = sof_parent_dev;

    auxiliary_device_init(&cdev->auxdev);
    return auxiliary_device_add(&cdev->auxdev);
}

/* Intel IDXD도 work queue를 auxiliary_device로 노출
 * → idxd.wq.0, idxd.wq.1 등으로 개별 WQ 드라이버 바인딩 */

auxiliary_bus vs 대안 비교

기준 platform bus MFD auxiliary_bus
도입 시기 Linux 2.6 초기 Linux 2.6.20 Linux 5.11
주요 대상 SoC 주변장치 (DT/ACPI) 멀티펑션 IC (PMIC 등) PCIe 다기능 디바이스
디바이스 발견 DT/ACPI/수동 부모 MFD 드라이버 부모 PCI 드라이버
IOMMU 그룹 별도 그룹 별도 그룹 (platform_device) 부모 그룹 상속
NUMA 노드 손실 가능 손실 가능 부모에서 상속
전원 관리 독립 PM 도메인 MFD 셀 통해 간접 부모 device 트리 자동 연동
모듈 분리 가능 가능하나 복잡 설계 의도 (id_table)
리소스 공유 platform_data mfd_cell resources container_of (타입 안전)
사용 예 GPIO, UART, SPI (SoC) TPS65910 PMIC, WM8994 MLX5, ICE, SOF, IDXD
다기능 디바이스 드라이버 설계 디바이스가 DT/ACPI에 기술되어 있나? Yes platform No PCIe 디바이스의 독립 기능을 분할하나? Yes auxiliary_bus No 레지스터 공간을 셀 단위로 분할하나? Yes MFD No 기능별 독립 모듈이 필요하고 부모 속성 상속이 중요하나? Yes auxiliary_bus No platform bus 또는 단일 드라이버

디버깅 및 문제 해결

sysfs를 통한 탐색

# 등록된 모든 auxiliary 디바이스 확인
$ find /sys/bus/auxiliary/devices/ -maxdepth 1 -type l | sort
/sys/bus/auxiliary/devices/ice.rdma.0
/sys/bus/auxiliary/devices/mlx5_core.eth.0
/sys/bus/auxiliary/devices/mlx5_core.rdma.0
/sys/bus/auxiliary/devices/mlx5_core.vnet.0

# 디바이스-드라이버 바인딩 상태 확인
$ for d in /sys/bus/auxiliary/devices/*/driver; do
    echo "$(basename $(dirname $d)) -> $(basename $(readlink $d))"
  done
mlx5_core.eth.0 -> mlx5_core.eth
mlx5_core.rdma.0 -> mlx5_core.rdma

# 수동 unbind/bind (디버깅용)
$ echo "mlx5_core.eth.0" > /sys/bus/auxiliary/drivers/mlx5_core.eth/unbind
$ echo "mlx5_core.eth.0" > /sys/bus/auxiliary/drivers/mlx5_core.eth/bind

dmesg 패턴 분석

# auxiliary_bus 관련 메시지 필터링
$ dmesg | grep -i auxiliary
[    2.345] auxiliary mlx5_core.eth.0: probe
[    2.346] auxiliary mlx5_core.rdma.0: probe

# release 미설정 경고 확인
$ dmesg | grep "does not have a release"
[    1.234] Device 'xxx.yyy.0' does not have a release() function

# ftrace로 probe/remove 추적
$ echo 1 > /sys/kernel/debug/tracing/events/bus/bus_probe/enable
$ echo 1 > /sys/kernel/debug/tracing/events/bus/bus_remove/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
mlx5_core-1234 [001] .... 12.345: bus_probe: ...auxiliary mlx5_core.eth.0

안티패턴과 모범 사례

주의: auxiliary_bus를 사용할 때 가장 흔한 실수와 올바른 패턴을 대비합니다.

안티패턴 1: release 누락 또는 직접 kfree

/* ❌ 잘못된 패턴: release 미설정 */
static int wrong_create(void)
{
    struct my_container *c = kzalloc(sizeof(*c), GFP_KERNEL);

    c->auxdev.name = "func";
    c->auxdev.dev.parent = parent;
    /* c->auxdev.dev.release = ???  ← 빠졌음! */
    auxiliary_device_init(&c->auxdev);
    auxiliary_device_add(&c->auxdev);
    return 0;
}

/* ❌ 잘못된 패턴: uninit 후 직접 kfree */
static void wrong_destroy(struct my_container *c)
{
    auxiliary_device_delete(&c->auxdev);
    auxiliary_device_uninit(&c->auxdev);
    kfree(c);   /* ⚠️ release()와 이중 해제! */
}
/* ✅ 올바른 패턴 */
static void correct_release(struct device *dev)
{
    struct auxiliary_device *adev = container_of(dev, struct auxiliary_device, dev);
    struct my_container *c = container_of(adev, struct my_container, auxdev);
    kfree(c);
}

static int correct_create(void)
{
    struct my_container *c = kzalloc(sizeof(*c), GFP_KERNEL);
    c->auxdev.dev.release = correct_release;  /* ✅ release 설정 */
    /* ... init + add ... */
}

static void correct_destroy(struct my_container *c)
{
    auxiliary_device_delete(&c->auxdev);
    auxiliary_device_uninit(&c->auxdev);
    /* ✅ kfree 하지 않음 — release()가 처리 */
}

안티패턴 2: init 실패 시 uninit 호출

/* ❌ init 실패 시에는 uninit을 호출하면 안 됨 */
ret = auxiliary_device_init(&c->auxdev);
if (ret) {
    auxiliary_device_uninit(&c->auxdev);  /* ❌ 잘못됨 */
    return ret;
}

/* ✅ init 실패: refcount가 올라가지 않았으므로 직접 kfree */
ret = auxiliary_device_init(&c->auxdev);
if (ret) {
    kfree(c);  /* ✅ 직접 해제 */
    return ret;
}
다음 학습: