Auxiliary Bus (보조 버스(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 실제 활용 사례, 대안 버스 비교, 디버깅(Debugging) 기법까지 종합적으로 다룹니다.

커널 소스: 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팀, 암호화(Encryption)팀)가 입주합니다. 각 부서는 독립적으로 운영되지만, 전기·수도 등 공용 시설(공유 레지스터(Register), 인터럽트(Interrupt))은 건물 관리사무소(부모 드라이버)가 중앙 관리합니다. auxiliary_bus는 바로 이 "입주 관리 시스템"입니다.

핵심 요약

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

단계별 이해

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

개요 및 도입 배경

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를 감싸는 컨테이너(Container) 구조체. 부모와 공유할 데이터(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()로 등록합니다. 편의 매크로(Macro) 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를 감싸는 부모 컨테이너를 역참조(Dereference)합니다. 이를 통해 부모가 제공한 공유 레지스터 등에 접근합니다.
  • 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는 참조 카운트(Reference Count)로 관리되므로, 메모리 해제 시점이 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 하드웨어에 대해 여러 오디오 파이프라인(Pipeline)을 독립적으로 관리합니다.

/* 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;
}

auxiliary_device 필드 상세 분석

auxiliary_device 구조체의 각 필드는 단순해 보이지만, Linux Device Model과 밀접하게 연결되어 있습니다. 각 필드의 역할과 내부 동작을 심층적으로 분석합니다.

dev 필드 — struct device 임베딩

struct device dev는 Linux Device Model의 핵심 객체입니다. auxiliary_device가 이를 직접 임베딩(embedding)함으로써 sysfs 표현, 전원 관리, 참조 카운팅, uevent, 드라이버 바인딩 등 커널 인프라를 자동으로 활용합니다.

dev 하위 필드설정 주체역할
dev.parent부모 드라이버부모 디바이스 참조. NUMA 노드, IOMMU 그룹, PM 계층을 상속합니다.
dev.release부모 드라이버refcount가 0이 될 때 호출되는 해제 콜백. 필수 설정입니다.
dev.busauxiliary_bus 코어auxiliary_device_init()&auxiliary_bus_type으로 자동 설정합니다.
dev.kobj.nameauxiliary_bus 코어auxiliary_device_init()"modname.name.id" 형식으로 설정합니다.
dev.groups자식 드라이버probe에서 추가 sysfs 속성 그룹을 등록할 수 있습니다.
dev.driver_data자식 드라이버dev_set_drvdata()로 드라이버 private 데이터를 연결합니다.
dev.dma_mask부모에서 상속DMA 마스크가 부모 PCIe 디바이스에서 상속됩니다.
/* auxiliary_device_init() 내부 — dev 필드 초기화 과정 */
int auxiliary_device_init(struct auxiliary_device *auxdev)
{
    struct device *dev = &auxdev->dev;

    /* bus_type 설정 → sysfs, 매칭, uevent 연동 */
    dev->bus = &auxiliary_bus_type;

    /* kobject 이름 생성: "KBUILD_MODNAME.name.id" */
    dev_set_name(dev, "%s.%s.%d", auxdev->name ? : "unknown",
                 dev->parent ? dev_name(dev->parent) : "none",
                 auxdev->id);

    /* device 초기화: refcount=1, kobject 설정 */
    device_initialize(dev);
    return 0;
}

name 필드 — 기능 식별자

name"eth", "rdma", "vnet" 같은 기능 식별 문자열입니다. 이 문자열은 최종 sysfs 이름의 중간 부분이 되고, id_table 매칭의 핵심 키입니다.

name 길이 제한: auxiliary_device_id.name은 32바이트 배열이며, 여기에 "modname." 접두사가 포함되므로 실제 기능 이름은 32 - strlen(KBUILD_MODNAME) - 1자 이내여야 합니다. 이를 초과하면 매칭이 실패합니다.

id 필드 — 인스턴스 번호

id는 동일 기능의 여러 인스턴스를 구별합니다. 이 값은 매칭에 사용되지 않습니다 — 같은 기능의 모든 인스턴스가 동일 드라이버에 바인딩됩니다. id의 실제 활용 사례:

드라이버id 활용예시
MLX5 SFScalable Function 번호sf.88, sf.89 (devlink SF 인덱스)
ICEPF 인스턴스 인덱스rdma.0, rdma.1 (멀티포트 NIC)
IDXDWork Queue 번호wq.0, wq.1, wq.2
SOF오디오 파이프라인 인덱스dma.0, ipc-flood.0

auxiliary_driver 콜백 함수 상세

auxiliary_driver는 다섯 가지 콜백 함수를 지원합니다. 각 콜백의 호출 시점, 컨텍스트, 구현 규칙을 상세히 분석합니다.

auxiliary_driver 콜백 호출 시퀀스 이벤트 auxiliary_bus 코어 auxiliary_driver device_add() 또는 driver_register() auxiliary_match() → 매칭 probe(adev, id) 시스템 suspend 진입 auxiliary_bus_suspend() suspend(adev, state) 시스템 resume 복귀 auxiliary_bus_resume() resume(adev) 시스템 shutdown / reboot auxiliary_bus_shutdown() shutdown(adev) device_del() 또는 driver_unregister() auxiliary_bus_remove() remove(adev) 콜백 컨텍스트 규칙 probe/remove: process context, may sleep. RTNL lock은 드라이버가 필요시 직접 획득. suspend/resume: process context, 부모보다 먼저 suspend, 부모보다 나중에 resume (자식 우선). shutdown: reboot/halt 경로. 최소한의 정리만 수행. 시간 제한 있음 — 블로킹 I/O 최소화.

probe() 콜백 상세

probe()는 매칭 성공 후 호출되는 초기화 콜백입니다. 반환값이 0이면 바인딩 성공, 음수이면 실패입니다. 실패 시 -EPROBE_DEFER를 반환하면 커널이 나중에 재시도합니다.

패턴설명예시
container_of 역참조부모 컨테이너에서 공유 리소스 획득공유 레지스터, FW 인터페이스
devm_* 관리 API디바이스 해제 시 자동 정리devm_kzalloc, devm_request_irq
dev_set_drvdata드라이버 private 데이터 연결remove에서 dev_get_drvdata로 회수
-EPROBE_DEFER의존 리소스 미준비 시 지연(Latency)RDMA가 이더넷 초기화 대기
/* probe에서 EPROBE_DEFER 사용 패턴 */
static int rdma_aux_probe(struct auxiliary_device *adev,
                          const struct auxiliary_device_id *id)
{
    struct my_aux_container *c =
        container_of(adev, struct my_aux_container, auxdev);

    /* 이더넷 기능이 아직 초기화되지 않았으면 지연 */
    if (!c->parent->eth_ready)
        return -EPROBE_DEFER;

    /* RDMA 리소스 초기화 */
    return init_rdma_resources(c->parent);
}

remove() 콜백 상세

remove()는 디바이스 제거 또는 드라이버 언로드 시 호출됩니다. probe()의 역순으로 리소스를 정리해야 합니다. devm_* API로 할당한 리소스는 자동 해제되지만, 순서 의존성이 있는 정리는 명시적으로 수행해야 합니다.

/* remove 콜백 — 순서 의존적 정리 패턴 */
static void my_eth_remove(struct auxiliary_device *adev)
{
    struct my_eth_priv *priv = dev_get_drvdata(&adev->dev);

    /* 1. 네트워크 인터페이스 비활성화 (새 패킷 수신 차단) */
    unregister_netdev(priv->netdev);

    /* 2. NAPI 비활성화 */
    napi_disable(&priv->napi);

    /* 3. DMA 링 해제 (NAPI 비활성화 후에야 안전) */
    free_dma_rings(priv);

    /* 4. netdev 해제 */
    free_netdev(priv->netdev);

    /* devm_ 할당은 프레임워크가 자동 해제 */
}

shutdown() 콜백 상세

shutdown()은 시스템 shutdown/reboot 경로에서 호출됩니다. remove()와 달리 시간 제한이 엄격하며, 최소한의 하드웨어 정리만 수행해야 합니다. DMA 엔진을 정지하고 인터럽트를 비활성화하는 것이 핵심입니다.

/* shutdown 콜백 — 최소한의 정리 */
static void my_eth_shutdown(struct auxiliary_device *adev)
{
    struct my_eth_priv *priv = dev_get_drvdata(&adev->dev);

    /* DMA 엔진 정지 — 재부팅 시 stale DMA 방지 */
    writel(0, priv->regs + DMA_CTRL);

    /* 인터럽트 비활성화 */
    disable_irq(priv->irq);
}

매칭 알고리즘 내부 동작

auxiliary_bus의 매칭은 bus_type.match 콜백인 auxiliary_match()에서 수행됩니다. 매칭 과정에서 디바이스 이름이 구성되는 방식, modalias와 자동 모듈 로딩, uevent 전달 과정을 상세히 분석합니다.

매칭 및 자동 모듈 로딩 플로우 auxiliary_device_add() auxiliary_uevent() MODALIAS=auxiliary:mlx5_core.eth 이미 등록된 드라이버 등록된 드라이버 없음 auxiliary_match() 직접 매칭 probe() 호출 udevd → modprobe 실행 modules.alias 검색: alias auxiliary:mlx5_core.eth mlx5_ib auxiliary_driver_register() probe() 호출 auxiliary_match() 내부 상세 1. auxdev->name에서 디바이스 이름 추출 dev_name(auxdev) → "mlx5_core.eth.0" 2. 매칭 시 id 제외: "mlx5_core.eth"만 비교 3. driver->id_table 순회하며 strcmp() 4. 첫 번째 매칭 id 반환, 없으면 NULL (매칭 실패) * 동일 기능의 모든 인스턴스 (eth.0, eth.1)가 같은 드라이버에 매칭

modalias 형식과 자동 모듈 로딩

auxiliary_uevent()는 디바이스가 버스에 추가될 때 MODALIAS 환경변수를 설정합니다. 이 값은 udev가 적절한 드라이버 모듈을 자동 로드하는 데 사용됩니다.

/* drivers/base/auxiliary.c — uevent 구현 */
static int auxiliary_uevent(const struct device *dev,
                           struct kobj_uevent_env *env)
{
    const struct auxiliary_device *auxdev = to_auxiliary_dev(dev);
    const char *name = auxiliary_get_match_name(auxdev);

    return add_uevent_var(env, "MODALIAS=auxiliary:%s", name);
}

/* MODULE_DEVICE_TABLE(auxiliary, ...) 가 생성하는 alias:
 * alias auxiliary:mlx5_core.eth  mlx5_ib
 *
 * /lib/modules/$(uname -r)/modules.alias 에 기록됨
 */
# modalias 및 자동 로딩 확인
$ cat /sys/bus/auxiliary/devices/mlx5_core.eth.0/modalias
auxiliary:mlx5_core.eth

# 어떤 모듈이 로드되는지 확인
$ modprobe --show-depends auxiliary:mlx5_core.eth
insmod /lib/modules/.../mlx5_core.ko
insmod /lib/modules/.../mlx5e.ko

# modules.alias 확인
$ grep "auxiliary:" /lib/modules/$(uname -r)/modules.alias
alias auxiliary:mlx5_core.eth mlx5e
alias auxiliary:mlx5_core.rdma mlx5_ib
alias auxiliary:ice.rdma irdma

sysfs 인터페이스 상세 분석

/sys/bus/auxiliary/ 아래의 디렉터리 구조는 auxiliary_bus의 전체 상태를 실시간(Real-time)으로 반영합니다. 디바이스-드라이버 관계, 부모 계층, 전원 상태, 바인딩 제어를 포함합니다.

/sys/bus/auxiliary/ 디렉터리 구조 /sys/bus/auxiliary/ devices/ drivers/ mlx5_core.eth.0 → (symlink) mlx5_core.rdma.0 → (symlink) ice.rdma.0 → (symlink) mlx5_core.eth/ mlx5_core.rdma/ ice.rdma/ devices/mlx5_core.eth.0/ 내부 driver → ../../../drivers/mlx5_core.eth subsystem → ../../../bus/auxiliary uevent (MODALIAS=auxiliary:mlx5_core.eth) power/ (runtime PM 상태) runtime_status: active|suspended control: auto|on device → ../../../0000:03:00.0 (부모 PCIe) numa_node: 0 (부모에서 상속) drivers/mlx5_core.eth/ 내부 bind (write: 디바이스 이름 → 수동 바인딩) unbind (write: 디바이스 이름 → 수동 언바인딩) mlx5_core.eth.0 → (바인딩된 디바이스 symlink) module → ../../../module/mlx5_ib uevent new_id (write: 동적 ID 추가, 일부 버스 지원)
# sysfs를 통한 상세 탐색

# 디바이스의 부모 PCIe 정보 확인
$ readlink -f /sys/bus/auxiliary/devices/mlx5_core.eth.0/device
/sys/devices/pci0000:00/0000:00:01.0/0000:03:00.0

# NUMA 노드 확인 (부모에서 상속)
$ cat /sys/bus/auxiliary/devices/mlx5_core.eth.0/numa_node
0

# Runtime PM 상태 확인
$ cat /sys/bus/auxiliary/devices/mlx5_core.eth.0/power/runtime_status
active

# 디바이스-드라이버 바인딩 매핑 전체 조회
$ for dev in /sys/bus/auxiliary/devices/*; do
    name=$(basename $dev)
    drv=$(readlink $dev/driver 2>/dev/null | xargs basename 2>/dev/null)
    echo "$name → ${drv:-unbound}"
  done

# 수동 unbind/bind 실험
$ echo "mlx5_core.eth.0" | sudo tee /sys/bus/auxiliary/drivers/mlx5_core.eth/unbind
$ echo "mlx5_core.eth.0" | sudo tee /sys/bus/auxiliary/drivers/mlx5_core.eth/bind

추가 드라이버 케이스 스터디

Intel IDXD (Data Accelerator)

Intel IDXD(Data Streaming Accelerator)는 각 Work Queue를 auxiliary_device로 노출하여 독립적인 드라이버(dmaengine, user-space via idxd-config)에 바인딩합니다. 이를 통해 하나의 DSA 디바이스에서 커널 DMA 오프로드와 사용자 공간(User Space) 직접 접근을 동시에 지원합니다.

/* drivers/dma/idxd/init.c — IDXD WQ를 auxiliary_device로 노출 */
static int idxd_wq_create_aux(struct idxd_wq *wq)
{
    struct idxd_device *idxd = wq->idxd;

    wq->adev.name = "wq";
    wq->adev.id = wq->id;
    wq->adev.dev.parent = &idxd->pdev->dev;
    wq->adev.dev.release = idxd_wq_adev_release;

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

/* WQ별 드라이버 바인딩 예시:
 * idxd.wq.0 → kernel DMA engine driver
 * idxd.wq.1 → user-space driver (ENQCMD via mmap)
 */
# IDXD auxiliary 디바이스 확인
$ ls /sys/bus/auxiliary/devices/ | grep idxd
idxd.wq.0
idxd.wq.1
idxd.wq.2
idxd.wq.3

# WQ별 바인딩 상태
$ cat /sys/bus/auxiliary/devices/idxd.wq.0/driver/module/name
idxd_bus

Scalable I/O Virtualization (SIOV)

SIOV(Scalable IOV)는 SR-IOV를 대체하는 차세대 가상화(Virtualization) 기술입니다. SR-IOV가 PCIe 레벨에서 VF를 생성하는 반면, SIOV는 auxiliary_bus를 활용하여 소프트웨어 레벨에서 가상 디바이스를 생성합니다. MLX5의 Scalable Function이 대표적입니다.

특성SR-IOV (VF)SIOV (auxiliary_bus)
생성 단위PCIe Virtual Functionauxiliary_device (소프트웨어)
PCIe 자원VF별 config space, BAR부모 BAR 공유, doorbell만 분리
수량 제한PCIe 스펙 제한 (256 VF)사실상 무제한 (수천 개 가능)
생성 속도느림 (PCIe 열거 필요)빠름 (소프트웨어 등록만)
핫플러그(Hotplug)PCIe hotplug 필요auxiliary_device_add/delete 즉시
IOMMUVF별 독립 그룹부모 그룹 내 PASID 분리
구현 예일반 NIC VFMLX5 SF, IDXD WQ
# MLX5 Scalable Function 생성 (devlink API)
$ devlink port add pci/0000:03:00.0 flavour pcisf pfnum 0 sfnum 88
$ devlink port function set pci/0000:03:00.0/32768 hw_addr 00:11:22:33:44:55
$ devlink port function set pci/0000:03:00.0/32768 state active

# 생성된 SF의 auxiliary_device 확인
$ ls /sys/bus/auxiliary/devices/ | grep sf
mlx5_core.sf.88

# SF 내부에서 다시 auxiliary_device 생성 (재귀적 구조)
$ ls /sys/bus/auxiliary/devices/ | grep sf.88
mlx5_core.eth.1    # SF의 이더넷 기능
mlx5_core.rdma.1   # SF의 RDMA 기능

SOF (Sound Open Firmware)

Intel SOF 오디오 프레임워크는 DSP 펌웨어(Firmware)와의 통신 채널을 auxiliary_device로 분리합니다. 이를 통해 IPC(Inter-Processor Communication) 디버깅, DMA 트레이싱, 펌웨어 프로파일링(Profiling) 등 각 기능을 독립 모듈로 개발할 수 있습니다.

/* SOF 클라이언트 auxiliary_device 목록 */
static const struct sof_client_type sof_client_types[] = {
    { .name = "ipc-flood",   /* IPC 스트레스 테스트 */ },
    { .name = "ipc-msg-injector", /* IPC 메시지 주입 (디버깅) */ },
    { .name = "dma-trace",   /* DMA 트레이스 수집 */ },
    { .name = "probes",      /* 오디오 프로브 포인트 */ },
};

/* snd_sof.ipc-flood.0, snd_sof.dma-trace.0 등으로 등록 */

핫플러그 및 동적 생성/제거

auxiliary_bus의 큰 장점은 런타임 핫플러그입니다. 부모 드라이버가 필요에 따라 auxiliary_device를 동적으로 생성하고 제거할 수 있습니다. SR-IOV의 VF 생성과 달리 PCIe 열거가 필요 없으므로 밀리초 단위로 완료됩니다.

동적 Auxiliary Device 생성/제거 시나리오 시간 → 부모 probe 기본 기능 등록 초기 디바이스 eth.0, rdma.0 동적 추가 sf.88 (devlink 요청) 동적 제거 sf.88 (devlink 요청) 동적 추가 과정 1. devlink / netlink 요청 수신 2. kzalloc(container) + 필드 설정 3. auxiliary_device_init() + _add() 4. 등록된 드라이버 자동 매칭 → probe() 동적 제거 과정 1. devlink / netlink 요청 수신 2. auxiliary_device_delete() → remove() 호출 3. auxiliary_device_uninit() → put_device() 4. refcount 0 → release() → kfree() 동시성 주의사항 auxiliary_device_add/delete는 내부적으로 device_lock을 사용하므로 직렬화됩니다. 그러나 부모 드라이버의 자체 상태(리스트, 카운터)는 별도 mutex로 보호해야 합니다. probe()/remove() 콜백 내에서 다시 auxiliary_device_add/delete를 호출하면 데드락 위험이 있습니다.
/* 동적 생성 — 부모 드라이버의 콜백에서 호출 */
static int parent_create_sf(struct my_parent *parent, u32 sf_num)
{
    struct my_sf_container *sf;
    int ret;

    mutex_lock(&parent->sf_lock);

    sf = kzalloc(sizeof(*sf), GFP_KERNEL);
    if (!sf) {
        ret = -ENOMEM;
        goto unlock;
    }

    sf->auxdev.name = "sf";
    sf->auxdev.id = sf_num;
    sf->auxdev.dev.parent = &parent->pci_dev->dev;
    sf->auxdev.dev.release = sf_release;

    ret = auxiliary_device_init(&sf->auxdev);
    if (ret) {
        kfree(sf);
        goto unlock;
    }

    ret = auxiliary_device_add(&sf->auxdev);
    if (ret) {
        auxiliary_device_uninit(&sf->auxdev);
        goto unlock;
    }

    list_add_tail(&sf->list, &parent->sf_list);

unlock:
    mutex_unlock(&parent->sf_lock);
    return ret;
}

/* 동적 제거 */
static void parent_destroy_sf(struct my_parent *parent, u32 sf_num)
{
    struct my_sf_container *sf;

    mutex_lock(&parent->sf_lock);
    sf = find_sf_by_num(parent, sf_num);
    if (sf) {
        list_del(&sf->list);
        auxiliary_device_delete(&sf->auxdev);
        auxiliary_device_uninit(&sf->auxdev);
        /* kfree는 release()에서 처리 */
    }
    mutex_unlock(&parent->sf_lock);
}

에러 처리 패턴 및 디버깅

auxiliary_bus 관련 에러는 등록/매칭/프로브(Probe)/제거 각 단계에서 발생할 수 있습니다. 각 에러 유형별 진단 방법과 해결 패턴을 정리합니다.

공통 에러 유형

에러원인진단해결
"does not have a release()" dev.release 미설정 dmesg 경고 release 콜백 구현 및 설정
probe() 실패 (-ENOMEM) 메모리 부족 또는 리소스 할당 실패 dmesg, /proc/meminfo devm_ API 사용, 에러 경로 점검
드라이버 미바인딩 id_table 불일치 또는 모듈 미로드 sysfs, modalias modname.function 형식 일치 확인
이중 해제 (double free) uninit 후 직접 kfree KASAN, slub_debug release() 콜백에서만 해제
UAF (Use-After-Free) release 전 참조 접근 KASAN, lockdep get_device/put_device로 참조 관리
데드락 probe 내 auxiliary_device_add 호출 lockdep, hung_task workqueue로 비동기 생성

ftrace를 활용한 추적

# auxiliary_bus 관련 함수 추적
$ cd /sys/kernel/debug/tracing

# auxiliary_bus 코어 함수 필터링
$ echo 'auxiliary_*' > set_ftrace_filter
$ echo function > current_tracer
$ echo 1 > tracing_on

# 모듈 로드 후 로그 확인
$ modprobe mlx5_ib
$ cat trace
mlx5_core-1234 [002] d... 45.123: auxiliary_match <-driver_match_device
mlx5_core-1234 [002] d... 45.124: auxiliary_bus_probe <-really_probe
mlx5_core-1234 [002] d... 45.125: auxiliary_match <-driver_match_device

# 특정 디바이스의 probe 시간 측정
$ echo 0 > tracing_on
$ echo 'auxiliary_bus_probe' > set_ftrace_filter
$ echo function_graph > current_tracer
$ echo 1 > tracing_on
$ echo "mlx5_core.eth.0" > /sys/bus/auxiliary/drivers/mlx5_core.eth/bind
$ cat trace
 2) + 15.234 us | auxiliary_bus_probe();
# MLX5 devlink 상태 확인
$ devlink dev info pci/0000:03:00.0
pci/0000:03:00.0:
  driver mlx5_core
  serial_number MT2116X09299
  versions:
    fixed:
      fw.psid MT_0000000228
    running:
      fw 22.36.1010

# devlink health reporter (에러 진단)
$ devlink health show pci/0000:03:00.0
pci/0000:03:00.0:
  reporter fw
    state healthy error 0 recover 0
  reporter fw_fatal
    state healthy error 0 recover 0

# devlink 이벤트 실시간 모니터링
$ devlink monitor

# SF 포트 상태 확인
$ devlink port show pci/0000:03:00.0/32768
pci/0000:03:00.0/32768: type eth netdev enp3s0f0np0sf88 flavour pcisf controller 0 pfnum 0 sfnum 88
  function:
    hw_addr 00:11:22:33:44:55 state active opstate attached

KASAN/KFENCE를 활용한 메모리 버그 탐지

# KASAN 활성화된 커널에서 auxiliary_bus 메모리 버그 탐지
# Kconfig:
# CONFIG_KASAN=y
# CONFIG_KASAN_GENERIC=y

# UAF 발생 시 KASAN 출력 예시
$ dmesg | grep -A 20 "BUG: KASAN"
BUG: KASAN: use-after-free in auxiliary_bus_probe+0x5c/0x120
Read of size 8 at addr ffff888123456789 by task modprobe/1234

Call Trace:
 auxiliary_bus_probe+0x5c/0x120
 really_probe+0x1a3/0x3e0
 __driver_probe_device+0x73/0x160

Allocated by task 1230:
 kzalloc+0x1a/0x30
 my_parent_create_aux+0x45/0x120

Freed by task 1231:
 kfree+0x4a/0x60
 my_aux_release+0x30/0x40

성능 고려사항

auxiliary_bus 자체는 데이터 경로에 있지 않으므로 런타임 성능에 직접적인 영향은 없습니다. 그러나 프로브 순서, 의존성 관리, 초기화 지연이 시스템 부팅 시간과 기능 가용성에 영향을 미칩니다.

프로브 순서와 의존성

auxiliary_device는 등록 순서대로 probe됩니다. 그러나 기능 간 의존성이 있는 경우(예: RDMA가 이더넷에 의존), 프로브 순서를 제어해야 합니다.

전략구현장점단점
순차 등록 부모가 의존 순서대로 add() 단순, 예측 가능 부모가 의존성 관리
EPROBE_DEFER probe에서 조건 미충족 시 반환 커널이 재시도 관리 지연 발생, 재시도 오버헤드(Overhead)
콜백 통지 부모가 기능 준비 완료 시 통지 명시적 동기화 구현 복잡도 증가
비동기 프로브 async_schedule()로 병렬 프로브 부팅 시간 단축 레이스 컨디션 관리 필요
/* MLX5의 프로브 순서 제어 패턴 */
static const struct mlx5_adev_device mlx5_adev_devices[] = {
    [MLX5_INTERFACE_PROTOCOL_ETH]   = { .suffix = "eth",   .is_supported = &mlx5_eth_supported },
    [MLX5_INTERFACE_PROTOCOL_RDMA]  = { .suffix = "rdma",  .is_supported = &mlx5_rdma_supported },
    [MLX5_INTERFACE_PROTOCOL_VNET]  = { .suffix = "vnet",  .is_supported = &mlx5_vnet_supported },
};

/* is_supported 콜백으로 조건부 등록 */
for (i = 0; i < ARRAY_SIZE(mlx5_adev_devices); i++) {
    if (mlx5_adev_devices[i].is_supported &&
        !mlx5_adev_devices[i].is_supported(dev))
        continue;
    mlx5_add_adev(dev, i);  /* eth → rdma → vnet 순서 보장 */
}

지연 초기화 패턴

무거운 초기화(대량 DMA 버퍼(Buffer) 할당, 펌웨어 다운로드 등)를 probe 시점이 아닌 실제 사용 시점으로 지연하면 부팅 시간을 단축할 수 있습니다.

/* 지연 초기화 패턴 — 첫 open() 시에 초기화 */
static int my_eth_probe(struct auxiliary_device *adev,
                        const struct auxiliary_device_id *id)
{
    struct my_eth_priv *priv;

    priv = devm_kzalloc(&adev->dev, sizeof(*priv), GFP_KERNEL);
    priv->initialized = false;

    /* netdev만 등록, 무거운 초기화는 하지 않음 */
    priv->netdev = alloc_etherdev(0);
    priv->netdev->netdev_ops = &my_netdev_ops;

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

static int my_eth_open(struct net_device *netdev)
{
    struct my_eth_priv *priv = netdev_priv(netdev);

    if (!priv->initialized) {
        /* 첫 사용 시 무거운 초기화 수행 */
        allocate_dma_rings(priv);
        download_firmware(priv);
        priv->initialized = true;
    }

    netif_start_queue(netdev);
    return 0;
}

커널 버전별 변경 이력

auxiliary_bus는 Linux 5.11에서 도입된 이후 지속적으로 개선되고 있습니다. 주요 변경 사항을 버전별로 정리합니다.

버전변경사항커밋/패치(Patch)
5.11 auxiliary_bus 최초 도입. auxiliary_device, auxiliary_driver, auxiliary_device_id 구조체 정의. auxiliary_device_init/add/delete/uninit, auxiliary_driver_register/unregister API. commit 7de3697e9cbd4
5.12 MLX5 Scalable Function 지원 추가. SF를 auxiliary_device로 노출하여 독립 mlx5_core 인스턴스 생성. mlx5 SF 시리즈
5.14 Intel ICE 드라이버가 RDMA를 auxiliary_device로 분리. irdma 드라이버 통합. ice IDC 시리즈
5.15 SOF 오디오 프레임워크가 클라이언트 디바이스에 auxiliary_bus 채택. SOF client 시리즈
5.18 IDXD Work Queue를 auxiliary_device로 리팩토링. 개별 WQ 바인딩 지원. idxd WQ 시리즈
6.0 bus_type 구조체를 const로 변경하여 보안 강화. auxiliary_bus_type도 const로 전환. bus_type const 시리즈
6.2 devm_auxiliary_device_init() 추가 시도 — 디바이스 관리 메모리 자동화. devm 시리즈
6.4+ Intel MEI(Management Engine Interface)가 auxiliary_bus 채택. HDCP, PXP 등 보안 기능 분리. mei auxiliary 시리즈
6.6+ auxiliary_bus 사용자 확대: habanalabs AI 가속기, AMD XDNA NPU, Thunderbolt/USB4 등. 다수 드라이버 시리즈
채택 추세: 2024-2025년 기준으로 auxiliary_bus를 사용하는 커널 드라이버는 40개 이상이며, 특히 네트워킹(MLX5, ICE, nfp), 가속기(IDXD, habanalabs, XDNA), 오디오(SOF), 보안(MEI) 분야에서 de facto 표준으로 자리잡고 있습니다.

보안 고려사항

auxiliary_bus를 사용할 때 고려해야 할 보안 측면을 분석합니다. 권한 분리, IOMMU 그룹 상속의 보안 함의, 네임스페이스(Namespace) 격리(Isolation)에 대해 다룹니다.

IOMMU 그룹과 보안 경계

auxiliary_device는 부모의 IOMMU 그룹을 상속합니다. 이는 성능에는 유리하지만 보안 관점에서 주의가 필요합니다:

# IOMMU 그룹 확인 — auxiliary_device와 부모가 같은 그룹인지 확인
$ readlink /sys/bus/auxiliary/devices/mlx5_core.eth.0/iommu_group
../../../../kernel/iommu_groups/15

$ readlink /sys/bus/pci/devices/0000:03:00.0/iommu_group
../../../../kernel/iommu_groups/15
# → 같은 그룹 15

# IOMMU 그룹 내 모든 디바이스 확인
$ ls /sys/kernel/iommu_groups/15/devices/
0000:03:00.0      # 부모 PCIe
mlx5_core.eth.0   # auxiliary device
mlx5_core.rdma.0  # auxiliary device

네임스페이스와 cgroup 격리

auxiliary_device의 기능(예: 네트워크 인터페이스)은 네임스페이스에 의해 격리될 수 있습니다. 그러나 /sys/bus/auxiliary/는 초기 네임스페이스에서만 완전히 보입니다.

격리 수준지원설명
Network namespace완전 지원mlx5e netdev를 다른 netns로 이동 가능
cgroup v2부분 지원BPF 프로그램으로 리소스 제한 가능
Device namespace미지원sysfs 디바이스는 초기 netns에서만 노출
VFIO 컨테이너IOMMU 그룹 단위개별 auxiliary_device만 분리 불가 (그룹 전체)

sysfs bind/unbind 권한

/sys/bus/auxiliary/drivers/*/bindunbind 파일은 root 권한이 필요합니다. 악의적인 unbind는 서비스 중단(DoS)을 유발할 수 있으므로, 프로덕션 환경에서는 접근을 제한해야 합니다.

# bind/unbind 권한 확인
$ ls -la /sys/bus/auxiliary/drivers/mlx5_core.eth/
--w------- 1 root root 4096 ... bind
--w------- 1 root root 4096 ... unbind

# SELinux / AppArmor로 추가 제한 가능
# SELinux 예: auxiliary_bus_t 도메인 정의

버스 선택 가이드

실제 드라이버 설계에서 auxiliary_bus, platform bus, MFD 중 어떤 것을 선택할지 결정할 때 고려해야 할 구체적인 기준과 엣지 케이스를 정리합니다.

auxiliary_bus를 선택해야 하는 경우

auxiliary_bus를 피해야 하는 경우

하이브리드 접근

실제 대형 드라이버는 여러 버스 메커니즘을 혼합합니다. MLX5가 대표적입니다:

메커니즘MLX5에서의 용도
PCI 서브시스템PCIe 디바이스 열거, BAR 매핑(Mapping), MSI-X 벡터
auxiliary_buseth/rdma/vnet/sf/crypto 기능 분할
devlinkSF 생성/삭제, eSwitch 모드, health 리포터
netlink네트워크 설정 (ip link, tc, ethtool)
VFIO/mdev게스트 VM에 VF/SF 패스스루
MLX5 멀티 서브시스템 구성도 ConnectX-7 / BlueField-3 (PCIe) PCIe Gen5 x16, 400Gbps Ethernet, RDMA, Crypto PCI 서브시스템 (열거/BAR/MSI-X) mlx5_core (부모 드라이버) auxiliary_bus mlx5e (eth) netdev, TC, XDP netlink 설정 mlx5_ib (rdma) RoCE v2, RDMA verbs rdma-core 연동 mlx5_vnet (vDPA) virtio datapath VM 직접 연결 SF (Scalable Func) devlink 생성 재귀 aux 등록 devlink SF 관리, health eSwitch, rate 사용자 공간 인터페이스 ip link / ethtool (netlink) rdma / ibv_* (rdma-core) vhost-user (vDPA) devlink (netlink) tc / XDP (eBPF)

개발 체크리스트

auxiliary_bus를 사용하는 드라이버를 개발할 때 확인해야 할 항목을 부모 드라이버와 자식 드라이버로 나누어 정리합니다.

부모 드라이버 체크리스트

항목확인 사항실패 시 증상
dev.release 설정container_of + kfree 패턴커널 경고, 메모리 누수
dev.parent 설정부모 struct device 포인터NUMA/IOMMU 상속 실패
2단계 등록init → add 순서에러 경로 불일치
init 실패: kfree직접 kfree (uninit 호출 금지)이중 해제
add 실패: uninitauxiliary_device_uninit 호출refcount 누수
제거 순서delete → uninit (kfree 금지)UAF, 이중 해제
동시성 보호SF 리스트 등 자체 mutex레이스 컨디션
name 길이32 - strlen(modname) - 1 이내매칭 실패

자식 드라이버 체크리스트

항목확인 사항실패 시 증상
id_table"modname.function" 형식 + 센티넬드라이버 미바인딩
MODULE_DEVICE_TABLEauxiliary 타입으로 선언자동 모듈 로딩 실패
probe: devm_ API가능한 한 devm_ 사용remove에서 누수
probe: 에러 경로할당 역순 해제리소스 누수
remove: 역순 정리probe의 역순으로 해제의존성 버그
shutdown: 최소 정리DMA 정지, IRQ 비활성화만재부팅 지연/실패
PM 콜백suspend/resume 구현 (선택)sleep/resume 실패
Kconfigselect AUXILIARY_BUS빌드 실패

참고자료

커널 공식 문서

커널 소스 코드

외부 자료

다음 학습: