NTB (Non-Transparent Bridge)

PCIe NTB 아키텍처: Scratchpad/Doorbell/MW 메커니즘, ntb_hw_intel/ntb_hw_amd/switchtec 드라이버, ntb_transport/ntb_netdev/ntb_perf, DMA 통합, NUMA 최적화, 멀티호스트 클러스터

전제 조건: PCI / PCIe 서브시스템 문서를 먼저 읽으세요. NTB는 PCIe 토폴로지, BAR, MSI-X, DMA 매핑 등 PCIe 기본 개념을 전제로 합니다.
일상 비유: NTB는 국경 검문소와 비슷합니다. 두 나라(호스트)가 각자의 도로 체계(PCIe 주소 공간)를 독립적으로 유지하면서, 검문소(NTB 디바이스)를 통해 물자(데이터)를 주고받는 것과 같습니다. Scratchpad는 통관 서류, Doorbell은 도착 알림 벨, Memory Window는 물류 창고입니다.

핵심 요약

  • NTB란? — 두 개의 독립적인 PCIe 계층(hierarchy)을 연결하는 특수 PCIe 브리지 디바이스입니다. 각 호스트에게는 독립적인 PCIe 엔드포인트로 보입니다.
  • 3대 통신 메커니즘 — Scratchpad(초기 핸드셰이크), Doorbell(인터럽트 알림), Memory Window(대용량 데이터 전송)가 핵심입니다.
  • 소프트웨어 스택 — NTB 하드웨어 드라이버 → NTB 코어(ntb.ko) → 클라이언트 드라이버(ntb_transport, ntb_netdev, ntb_perf) 3계층 구조입니다.
  • 주요 활용 — 듀얼 컨트롤러 스토리지, HA 클러스터링, 멀티-호스트 PCIe 통신에 사용됩니다.
  • 커널 소스drivers/ntb/ 디렉토리에서 전체 NTB 서브시스템 코드를 확인할 수 있습니다.

개요

NTB(Non-Transparent Bridge)는 두 개의 독립적인 PCIe 계층(hierarchy)을 연결하는 특수 PCIe 브리지 디바이스입니다. 일반적인 PCIe 투명 브리지(Transparent Bridge)와 달리, NTB는 양쪽 호스트 각각에게 독립적인 PCIe 엔드포인트로 보이며, 각 호스트가 자체 PCI 버스 번호와 메모리 주소 공간(Address Space)을 독립적으로 관리할 수 있게 합니다.

NTB의 주요 사용 사례:

Host A CPU A MEM Root Complex A NTB Endpoint (Host A 관점) Host A가 보는 NTB 리소스 BAR0: Doorbell Regs BAR2: Scratchpad Regs BAR4: Memory Window 주소 변환 (Inbound) MW 쓰기 → NTB 변환 → Host B 메모리에 도달 통신 메커니즘 1. Doorbell → 상대 호스트 인터럽트 2. Scratchpad → 초기 핸드셰이크 3. MW → 대용량 데이터 전송 NTB Device Primary Side Secondary Side Address Translation Unit Doorbell Regs Doorbell Regs Scratchpad Scratchpad Memory Window A Memory Window B Link Status / Control PCIe Host B CPU B MEM Root Complex B NTB Endpoint (Host B 관점) Host B가 보는 NTB 리소스 BAR0: Doorbell Regs BAR2: Scratchpad Regs BAR4: Memory Window 주소 변환 (Inbound) MW 쓰기 → NTB 변환 → Host A 메모리에 도달 통신 메커니즘 1. Doorbell → 상대 호스트 인터럽트 2. Scratchpad → 초기 핸드셰이크 3. MW → 대용량 데이터 전송 PCIe

NTB 하드웨어 구성요소

NTB 디바이스는 두 호스트 간의 통신을 위해 다음과 같은 하드웨어 리소스를 제공합니다:

구성요소크기용도설명
Doorbell비트 단위 (보통 16~64비트)호스트 간 인터럽트한 호스트가 특정 비트를 설정하면 상대 호스트에 MSI/MSI-X 인터럽트가 발생합니다. 이벤트 알림, 데이터 수신 통지 등에 사용됩니다.
Scratchpad수십 바이트 (레지스터 수 × 4/8B)소량 공유 레지스터양쪽 호스트가 읽기/쓰기 가능한 작은 레지스터 세트입니다. 초기 핸드셰이크, 메모리 윈도우 주소 교환, 상태 플래그 전달에 사용됩니다.
Memory Window (MW)수 MB ~ 수 GB (BAR 크기에 의존)대용량 주소 변환 영역한 호스트의 MMIO 영역에 쓰면 NTB의 주소 변환 유닛(ATU)이 이를 상대 호스트의 물리 메모리(Physical Memory) 주소로 변환합니다. 대용량 데이터 전송의 핵심입니다.
Link Status레지스터링크 상태 모니터링NTB 양쪽의 PCIe 링크가 정상적으로 연결되었는지 확인합니다. 링크 업/다운 이벤트를 인터럽트로 통지받을 수 있습니다.
NTB Doorbell 시그널링과 Memory Window 매핑 Host A CPU A (드라이버) MW BAR MMIO 영역 메모리 A DMA 타겟 ntb_peer_db_set(bit) → Host B에 인터럽트 전달 ntb_mw_set_trans() → MW BAR에 메모리 A 주소 등록 NTB 주소 변환 Doorbell Scratchpad Link Status Host B 메모리 B DMA 타겟 MW BAR MMIO 영역 CPU B (드라이버) Doorbell (인터럽트) Memory Window 주소 변환 흐름 Host B: MW BAR에 쓰기 writel(data, mw_base + offset) NTB ATU 주소 변환 MW 주소 → Host A 물리주소 Host A 메모리에 도달 data → Host A RAM[addr] Doorbell → Host A 알림 ntb_peer_db_set() → IRQ 초기 핸드셰이크: Scratchpad로 MW 물리주소 교환 Host A: ntb_spad_write(0, phys_addr_a) → Host B: ntb_peer_spad_read(0) → ntb_mw_set_trans(mw_idx, phys_addr_a, size)

NTB 하드웨어 구현체

리눅스 커널에서 지원하는 주요 NTB 하드웨어:

하드웨어커널 드라이버특징
Intel Xeon (Sandy Bridge ~ Skylake)ntb_hw_intelIntel QPI/UPI 기반 NTB, B2B(Back-to-Back) 및 RP(Root Port) 모드 지원
AMDntb_hw_amdAMD Data Fabric 기반 NTB, Zen 아키텍처 지원
IDT 89HPESxxntb_hw_idtIDT(현 Renesas) PCIe 스위치 칩, 다중 NT 포트 지원, 유연한 토폴로지
Microsemi/Microchip Switchtecntb_hw_switchtecSwitchtec PCIe 스위치 기반 NTB, 핫플러그 지원, 높은 대역폭
AMD EPYCntb_hw_epycAMD EPYC 서버 프로세서 전용 NTB, NUMA 인식 최적화

NTB 통신 흐름

NTB 통신 흐름 — 3단계 1단계: Scratchpad 핸드셰이크 (초기화) Host A MW 기본주소 기록 spad_write(0, mw_addr) NTB Scratchpad 주소 교환 레지스터 peer_spad_read(0) Host B 상대 MW 주소 획득 2단계: Doorbell 인터럽트 (이벤트 알림) Host A 데이터 준비 완료 peer_db_set(BIT(0)) NTB Doorbell MSI-X 인터럽트 생성 IRQ → db_event() Host B 인터럽트 수신 3단계: Memory Window 데이터 전송 (대용량) Host A memcpy_toio(mw, data, len) MMIO Write (PCIe TLP) NTB ATU 주소 변환: HostA local → HostB phys 변환된 주소로 전달 Host B 메모리 데이터 도착 (DMA 가능) Scratchpad으로 주소 교환 → Memory Window 설정 → 데이터 전송 → Doorbell로 완료 알림

Linux NTB 서브시스템 아키텍처

리눅스 커널의 NTB 서브시스템(drivers/ntb/)은 3계층 구조로 설계되어 있습니다:

애플리케이션 계층 (Application Layer) SSH / SCP NFS / SMB TCP/IP socket Custom App debugfs / sysfs iperf3 / perf ─── 커널 공간 (Kernel Space) ─── NTB 클라이언트 드라이버 ntb_netdev 가상 이더넷 (ntb0) ntb_transport 링 버퍼 메시지 전달 (ntb_netdev가 사용) ntb_perf DMA 벤치마크 ntb_tool debugfs 디버깅 NTB Core (ntb.ko) NTB 버스 프레임워크 — ntb_register_device / ntb_register_client 매칭 통합 API: ntb_db_*, ntb_spad_*, ntb_mw_*, ntb_peer_*, ntb_link_* NTB 하드웨어 드라이버 ntb_hw_intel Intel Xeon NTB ntb_hw_amd AMD / EPYC NTB ntb_hw_idt IDT PCIe Switch ntb_hw_switchtec Microsemi Switchtec PCIe Subsystem (pci_register_driver, pci_iomap, MSI-X, DMA) NTB Hardware (PCIe Endpoint) Doorbell Regs | Scratchpad Regs | Memory Windows | Address Translation Unit

NTB API 코드 예제

NTB 클라이언트 드라이버를 작성하기 위한 핵심 API:

#include <linux/ntb.h>
#include <linux/module.h>

struct my_ntb_ctx {
    struct ntb_dev *ntb;
    void __iomem *mw_base;     /* Memory Window 가상 주소 */
    resource_size_t mw_size;   /* Memory Window 크기 */
    dma_addr_t mw_phys;        /* DMA 물리 주소 */
    void *rx_buf;              /* 수신 버퍼 */
};

/* Doorbell 이벤트 콜백 — 상대 호스트가 Doorbell을 울릴 때 호출 */
static void my_db_event(void *ctx, int vec)
{
    struct my_ntb_ctx *c = ctx;
    u64 db_bits;

    /* 어떤 Doorbell 비트가 설정되었는지 읽기 */
    db_bits = ntb_db_read(c->ntb);
    pr_info("NTB doorbell event: 0x%llx\n", db_bits);

    /* Doorbell 비트 클리어 */
    ntb_db_clear(c->ntb, db_bits);

    /* 수신 데이터 처리 */
    process_received_data(c);
}

/* 링크 상태 변경 콜백 */
static void my_link_event(void *ctx)
{
    struct my_ntb_ctx *c = ctx;

    if (ntb_link_is_up(c->ntb, NULL, NULL) == 1)
        pr_info("NTB link is UP\n");
    else
        pr_info("NTB link is DOWN\n");
}

static const struct ntb_ctx_ops my_ntb_ops = {
    .link_event = my_link_event,
    .db_event   = my_db_event,
};

/* NTB 디바이스 probe — NTB 코어가 매칭된 디바이스 발견 시 호출 */
static int my_ntb_probe(struct ntb_client *self,
                         struct ntb_dev *ntb)
{
    struct my_ntb_ctx *ctx;
    int rc, mw_count, spad_count;
    resource_size_t mw_size;

    ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
    if (!ctx)
        return -ENOMEM;
    ctx->ntb = ntb;

    /* 리소스 확인 */
    mw_count = ntb_mw_count(ntb, 0);         /* Memory Window 개수 */
    spad_count = ntb_spad_count(ntb);         /* Scratchpad 레지스터 수 */
    pr_info("NTB: %d MWs, %d SPADs\n", mw_count, spad_count);

    /* Memory Window 크기 조회 및 매핑 설정 */
    rc = ntb_mw_get_align(ntb, 0, 0, &mw_size, NULL, NULL, NULL);
    if (rc)
        goto err_free;

    /* 수신 버퍼 할당 (DMA 가능) */
    ctx->rx_buf = dma_alloc_coherent(&ntb->pdev->dev, mw_size,
                                      &ctx->mw_phys, GFP_KERNEL);
    if (!ctx->rx_buf) {
        rc = -ENOMEM;
        goto err_free;
    }

    /* Memory Window 변환 설정: 상대 호스트의 쓰기가 rx_buf에 도착 */
    rc = ntb_mw_set_trans(ntb, 0, 0, ctx->mw_phys, mw_size);
    if (rc)
        goto err_dma;

    /* Scratchpad에 준비 완료 플래그 기록 (상대 호스트가 읽음) */
    ntb_peer_spad_write(ntb, 0, 0, NTB_READY_MAGIC);

    /* Doorbell 마스크 해제 — 인터럽트 수신 활성화 */
    ntb_db_set_mask(ntb, 0);       /* 먼저 모든 비트 마스크 */
    ntb_db_clear_mask(ntb, 0x1);  /* 비트 0만 언마스크 */

    /* 컨텍스트 등록 및 링크 활성화 */
    ntb_set_ctx(ntb, ctx, &my_ntb_ops);
    ntb_link_enable(ntb, NTB_SPEED_AUTO, NTB_WIDTH_AUTO);

    return 0;

err_dma:
    dma_free_coherent(&ntb->pdev->dev, mw_size, ctx->rx_buf, ctx->mw_phys);
err_free:
    kfree(ctx);
    return rc;
}

static void my_ntb_remove(struct ntb_client *self,
                           struct ntb_dev *ntb)
{
    struct my_ntb_ctx *ctx = ntb_get_ctx(ntb);

    ntb_link_disable(ntb);
    ntb_clear_ctx(ntb);
    ntb_mw_clear_trans(ntb, 0, 0);
    dma_free_coherent(&ntb->pdev->dev, ctx->mw_size,
                      ctx->rx_buf, ctx->mw_phys);
    kfree(ctx);
}

static struct ntb_client my_ntb_client = {
    .ops = {
        .probe  = my_ntb_probe,
        .remove = my_ntb_remove,
    },
};
module_ntb_client(my_ntb_client);

ntb_transport: 링 버퍼 기반 메시지 전달

ntb_transport는 NTB Memory Window 위에 신뢰성 있는 메시지 전달 계층을 구축합니다. 내부적으로 각 Memory Window를 링 버퍼로 분할하여 사용합니다:

/* ntb_transport 링 버퍼 디스크립터 (간략화) */
struct ntb_transport_qp {             /* Queue Pair */
    struct ntb_transport_ctx *transport;
    struct ntb_dev *ndev;

    void __iomem *tx_mw;               /* 상대 호스트 MW에 매핑 (쓰기용) */
    dma_addr_t tx_mw_phys;
    unsigned int tx_index;
    unsigned int tx_max_entry;
    unsigned int tx_max_frame;

    void *rx_buff;                      /* 자신의 수신 버퍼 */
    unsigned int rx_index;
    unsigned int rx_max_entry;
    unsigned int rx_max_frame;

    void (*rx_handler)(struct ntb_transport_qp *qp,
                       void *data, int len);
    u64 db_bit;                        /* 이 QP용 Doorbell 비트 */
};

/* 메시지 전송 흐름 */
static int ntb_transport_tx(struct ntb_transport_qp *qp,
                            void *data, unsigned int len)
{
    struct ntb_payload_header *hdr;
    void __iomem *dest;

    /* TX 링에 빈 슬롯이 있는지 확인 */
    if (ntb_transport_tx_free_entry(qp) == 0)
        return -EAGAIN;

    /* 상대 호스트의 Memory Window에 직접 쓰기 */
    dest = qp->tx_mw + qp->tx_index * qp->tx_max_frame;
    memcpy_toio(dest + sizeof(*hdr), data, len);

    /* 헤더 기록 (길이, 플래그) */
    hdr = (struct ntb_payload_header __iomem *)dest;
    iowrite32(len, &hdr->len);
    iowrite32(NTB_PAYLOAD_VALID, &hdr->flags);

    /* 인덱스 갱신 */
    qp->tx_index = (qp->tx_index + 1) % qp->tx_max_entry;

    /* Doorbell로 상대 호스트에 알림 */
    ntb_peer_db_set(qp->ndev, qp->db_bit);
    return 0;
}
ntb_transport Queue Pair 링 버퍼 구조 Host A TX Ring (→ Host B MW에 쓰기) slot 0 slot 1 tx_idx slot 3 slot 4 ... RX Ring (← Host B가 여기에 쓰기) slot 0 rx_idx slot 2 slot 3 slot 4 ... 각 슬롯: [ntb_payload_header | payload data] header: len, flags(VALID), ver 슬롯 크기: tx_max_frame (기본 ~64KB) Host B (대칭 구조) RX Ring (← Host A TX 도착) Host A의 TX 데이터가 여기에 도착 (MW를 통해) TX Ring (→ Host A RX로 전송) Host B의 TX 데이터 → Host A MW를 통해 전달 대칭: 각 호스트가 TX/RX 링 쌍을 보유 TX는 상대 MW에 쓰고, RX는 자기 MW에서 읽기 Doorbell: 데이터 기록 완료 시 상대에게 IRQ MW

ntb_netdev: 가상 이더넷 인터페이스

ntb_netdevntb_transport 위에 구현된 가상 네트워크 드라이버로, NTB 링크를 표준 이더넷 인터페이스(ntb0)로 노출합니다. 이를 통해 TCP/IP 스택, SSH, NFS 등 일반적인 네트워크 애플리케이션을 NTB 위에서 직접 사용할 수 있습니다.

# NTB 하드웨어 드라이버 로드 (Intel 예시)
modprobe ntb_hw_intel
# NTB 전송 계층 로드
modprobe ntb_transport
# NTB 네트워크 인터페이스 생성
modprobe ntb_netdev

# Host A에서 IP 설정
ip addr add 10.0.0.1/24 dev ntb0
ip link set ntb0 up

# Host B에서 IP 설정
ip addr add 10.0.0.2/24 dev ntb0
ip link set ntb0 up

# 연결 테스트
ping 10.0.0.2
# 대역폭 측정
iperf3 -s               # Host B (서버)
iperf3 -c 10.0.0.2      # Host A (클라이언트)

ntb_perf: DMA 성능 벤치마크

ntb_perf는 NTB Memory Window를 통한 DMA 전송 성능을 측정하는 벤치마크 모듈입니다. sysfs 인터페이스를 통해 제어합니다:

# ntb_perf 모듈 로드
modprobe ntb_perf

# 전송 크기 설정 (바이트)
echo 1048576 > /sys/kernel/ntb_perf/0000:03:00.0/run

# 결과 확인
cat /sys/kernel/ntb_perf/0000:03:00.0/run
# 출력 예: "1048576 bytes in 524 usecs, 16.0 Gbps"

# DMA 엔진을 사용한 전송 (CPU 오프로드)
echo 1 > /sys/kernel/ntb_perf/0000:03:00.0/use_dma
echo 4194304 > /sys/kernel/ntb_perf/0000:03:00.0/run

# 여러 스레드로 병렬 벤치마크
echo 4 > /sys/kernel/ntb_perf/0000:03:00.0/threads
echo 1048576 > /sys/kernel/ntb_perf/0000:03:00.0/run

ntb_tool: debugfs 디버깅 인터페이스

ntb_tool은 NTB 하드웨어의 저수준 레지스터에 직접 접근할 수 있는 debugfs 기반 디버깅 도구입니다:

# ntb_tool 모듈 로드
modprobe ntb_tool

# debugfs 마운트 확인
mount -t debugfs none /sys/kernel/debug 2>/dev/null

# NTB 디바이스의 debugfs 경로
ls /sys/kernel/debug/ntb_tool/0000:03:00.0/

# Scratchpad 레지스터 읽기/쓰기
cat /sys/kernel/debug/ntb_tool/0000:03:00.0/spad
echo "0:0xDEADBEEF" > /sys/kernel/debug/ntb_tool/0000:03:00.0/peer_spad

# Doorbell 상태 확인
cat /sys/kernel/debug/ntb_tool/0000:03:00.0/db
# Doorbell 설정 (상대 호스트에 인터럽트 전송)
echo 0x1 > /sys/kernel/debug/ntb_tool/0000:03:00.0/peer_db

# Memory Window 정보
cat /sys/kernel/debug/ntb_tool/0000:03:00.0/mw_trans0

# 링크 상태
cat /sys/kernel/debug/ntb_tool/0000:03:00.0/link

NTB 디바이스 검색 및 BIOS 설정

NTB 디바이스는 표준 PCIe 열거(enumeration) 과정에서 검색됩니다. BIOS/UEFI에서 NTB 관련 설정이 필요한 경우가 많습니다:

NTB 디바이스 열거 확인 (lspci):

$ lspci -vv -d 8086:6f0d
03:00.0 Bridge: Intel Corporation Xeon NTB
  Control: I/O- Mem+ BusMaster+ ...
  Region 0: Memory at ... [size=64K]   ← Doorbell/Scratchpad
  Region 2: Memory at ... [size=1M]    ← Memory Window 0
  Region 4: Memory at ... [size=256M]  ← Memory Window 1
  Capabilities: [40] Express Endpoint, MSI-X

Device Tree 바인딩 예시 (ARM 임베디드 플랫폼):

ntb@10000000 {
    compatible = "vendor,pcie-ntb";
    reg = <0x10000000 0x10000   // 제어 레지스터
           0x10100000 0x100000  // Memory Window 0
           0x10200000 0x1000000>; // Memory Window 1
    interrupts = <GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH>;
    num-mws = <2>;
    num-spads = <16>;
};

NTB DMA 통합

NTB Memory Window를 통한 데이터 전송에는 두 가지 방식이 있습니다. CPU 주도 복사(CPU-driven memcpy)DMA 엔진 오프로드(DMA Engine Offload)입니다. CPU 주도 방식은 memcpy_toio()를 사용하여 CPU가 직접 MMIO 쓰기를 수행하는 반면, DMA 엔진 방식은 시스템의 DMA 컨트롤러(예: Intel IOAT/CBDMA)가 데이터를 비동기적으로 복사합니다.

CPU 복사 vs DMA 엔진

항목CPU 복사 (memcpy_toio)DMA 엔진 오프로드
방식CPU가 MMIO 쓰기 직접 수행DMA 컨트롤러가 백그라운드에서 복사
CPU 사용률높음 (100% 점유)낮음 (디스크립터 제출만)
지연 시간낮음 (~1 us, 소량 데이터)중간 (~5-10 us, 디스크립터 오버헤드)
처리량 (대용량)~6-8 GB/s (단일 코어 한계)~12-14 GB/s (DMA 엔진 전체 대역폭)
최적 시나리오64B ~ 4KB 소량 전송64KB 이상 대용량 전송
배리어 처리수동 wmb()/rmb() 필요DMA 엔진이 순서 보장

DMA 엔진을 활용한 NTB 데이터 전송

커널의 dmaengine API를 사용하여 NTB Memory Window로의 데이터 전송을 DMA 엔진에 오프로드할 수 있습니다. 아래는 DMA 채널 할당부터 비동기 전송, 콜백 완료 처리까지의 흐름입니다:

#include <linux/dmaengine.h>
#include <linux/ntb.h>

struct ntb_dma_ctx {
    struct ntb_dev *ntb;
    struct dma_chan *dma_chan;        /* DMA 채널 */
    dma_addr_t src_phys;              /* 소스 버퍼 물리 주소 */
    phys_addr_t mw_phys;              /* MW 대상 물리 주소 */
    struct completion dma_done;       /* 완료 대기 */
};

/* DMA 전송 완료 콜백 */
static void ntb_dma_callback(void *data)
{
    struct ntb_dma_ctx *ctx = data;

    /* 전송 완료 시그널 */
    complete(&ctx->dma_done);

    /* Doorbell로 상대 호스트에 데이터 도착 알림 */
    ntb_peer_db_set(ctx->ntb, BIT(0));
}

/* DMA 엔진을 통한 NTB MW 데이터 전송 */
static int ntb_dma_transfer(struct ntb_dma_ctx *ctx,
                             size_t len)
{
    struct dma_async_tx_descriptor *tx;
    dma_cookie_t cookie;

    /* DMA 디스크립터 준비: src → MW(dst) 복사 */
    tx = dmaengine_prep_dma_memcpy(ctx->dma_chan,
                                    ctx->mw_phys,     /* 대상: NTB MW */
                                    ctx->src_phys,     /* 소스: 로컬 버퍼 */
                                    len,
                                    DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
    if (!tx)
        return -ENOMEM;

    /* 완료 콜백 설정 */
    tx->callback = ntb_dma_callback;
    tx->callback_param = ctx;

    /* DMA 트랜잭션 제출 */
    init_completion(&ctx->dma_done);
    cookie = dmaengine_submit(tx);
    if (dma_submit_error(cookie))
        return -EIO;

    /* DMA 엔진 실행 시작 */
    dma_async_issue_pending(ctx->dma_chan);

    /* 완료 대기 (타임아웃 5초) */
    if (!wait_for_completion_timeout(&ctx->dma_done,
                                     msecs_to_jiffies(5000))) {
        pr_err("NTB DMA transfer timeout\n");
        return -ETIMEDOUT;
    }

    return 0;
}

/* DMA 채널 할당 (probe 시 호출) */
static int ntb_dma_init(struct ntb_dma_ctx *ctx)
{
    dma_cap_mask_t mask;

    /* DMA_MEMCPY 기능을 지원하는 채널 요청 */
    dma_cap_zero(mask);
    dma_cap_set(DMA_MEMCPY, mask);

    ctx->dma_chan = dma_request_channel(mask, NULL, NULL);
    if (!ctx->dma_chan) {
        pr_warn("No DMA channel available, falling back to CPU copy\n");
        return -ENODEV;
    }

    pr_info("NTB DMA: using channel %s\n",
            dma_chan_name(ctx->dma_chan));
    return 0;
}

DMA 엔진 데이터 흐름

Host A (송신측) CPU 디스크립터 제출만 DMA Engine IOAT / CBDMA submit Source Buffer dma_alloc_coherent() read NTB Memory Window BAR4 MMIO 영역 write callback → complete() CPU에 완료 알림 ntb_peer_db_set(BIT(0)) 상대 호스트에 알림 DMA 엔진 장점 CPU 자유 — 디스크립터 제출 후 다른 작업 수행 가능 대용량(64KB+) 전송 시 CPU 복사 대비 2~3배 처리량 향상 NTB ATU 주소변환 PCIe Host B (수신측) Physical Memory MW 인바운드 변환 대상 Doorbell IRQ → db_event() 데이터 수신 확인 수신 처리 흐름 1. IRQ 수신 → 2. db_clear() → 3. 데이터 처리 4. rmb() 배리어 후 버퍼 읽기

NTB NUMA 최적화

NTB 디바이스는 특정 NUMA 노드의 PCIe 루트 컴플렉스에 연결됩니다. 최적의 성능을 달성하려면 NTB가 연결된 NUMA 노드를 인식하고, 버퍼 할당과 IRQ 처리를 해당 노드에서 수행해야 합니다.

NUMA 인식 버퍼 할당

NTB Memory Window에 사용할 버퍼를 할당할 때, NTB 디바이스가 속한 NUMA 노드에서 메모리를 할당하면 PCIe 트랜잭션이 로컬 메모리 컨트롤러를 통과하여 지연 시간이 줄어듭니다:

/* NUMA 인식 NTB 버퍼 할당 */
static int ntb_numa_alloc_buffer(struct ntb_dev *ntb,
                                  void **buf, size_t size)
{
    int node = dev_to_node(&ntb->pdev->dev);

    pr_info("NTB device on NUMA node %d\n", node);

    /* NTB 디바이스와 같은 NUMA 노드에서 할당 */
    *buf = kmalloc_node(size, GFP_KERNEL, node);
    if (!*buf)
        return -ENOMEM;

    return 0;
}

/* DMA 코히런트 버퍼의 NUMA 인식 할당 */
static int ntb_numa_alloc_dma(struct ntb_dev *ntb,
                               void **buf, dma_addr_t *dma_addr,
                               size_t size)
{
    struct device *dev = &ntb->pdev->dev;
    int node = dev_to_node(dev);

    /* set_dev_node()로 디바이스 NUMA 노드 보장 */
    set_dev_node(dev, node);

    /* dma_alloc_coherent는 디바이스 NUMA 노드를 참조 */
    *buf = dma_alloc_coherent(dev, size, dma_addr, GFP_KERNEL);
    if (!*buf)
        return -ENOMEM;

    return 0;
}

IRQ 어피니티 설정

NTB Doorbell 인터럽트(MSI-X)를 NTB 디바이스와 같은 NUMA 노드의 CPU에서 처리하도록 IRQ 어피니티를 설정하면 인터럽트 처리 지연을 최소화할 수 있습니다:

# NTB 디바이스의 NUMA 노드 확인
cat /sys/bus/pci/devices/0000:03:00.0/numa_node
# 출력 예: 0

# 해당 NUMA 노드의 CPU 목록 확인
cat /sys/devices/system/node/node0/cpulist
# 출력 예: 0-15

# NTB MSI-X 벡터의 IRQ 번호 확인
cat /proc/interrupts | grep ntb
#  45:  ... IR-PCI-MSI 49152-edge  ntb_hw_intel

# IRQ 어피니티를 NUMA 노드 0의 CPU(0-15)로 설정
echo 0000ffff > /proc/irq/45/smp_affinity

# 또는 irqbalance 데몬에 NTB IRQ 힌트 제공
# /etc/sysconfig/irqbalance에 IRQBALANCE_BANNED_CPULIST 설정

Memory Window NUMA 고려사항

Memory Window 매핑 시 NUMA를 고려해야 하는 핵심 사항입니다:

/* NUMA 인식 DMA 채널 선택 */
static bool ntb_dma_filter(struct dma_chan *chan, void *data)
{
    int target_node = *(int *)data;
    int chan_node = dev_to_node(chan->device->dev);

    /* NTB와 같은 NUMA 노드의 DMA 채널만 선택 */
    return chan_node == target_node;
}

static struct dma_chan *ntb_get_numa_dma_chan(struct ntb_dev *ntb)
{
    dma_cap_mask_t mask;
    int node = dev_to_node(&ntb->pdev->dev);

    dma_cap_zero(mask);
    dma_cap_set(DMA_MEMCPY, mask);

    return dma_request_channel(mask, ntb_dma_filter, &node);
}

멀티-포트 NTB 토폴로지

일반적인 NTB는 두 호스트 간의 점대점(Point-to-Point, B2B) 연결입니다. 그러나 IDT 89HPESxx나 Microsemi Switchtec 같은 PCIe 스위치 기반 NTB를 사용하면 3개 이상의 호스트를 스타(Star) 토폴로지로 연결할 수 있습니다.

B2B vs 스위치 기반 NTB

특성B2B (Back-to-Back)스위치 기반 (IDT/Switchtec)
호스트 수정확히 2개2~8개 이상
토폴로지점대점스타 (스위치 중심)
하드웨어CPU 내장 NTB 또는 EP 디바이스외부 PCIe 스위치 칩
커널 드라이버ntb_hw_intel, ntb_hw_amdntb_hw_idt, ntb_hw_switchtec
MW/Doorbell단일 피어(Peer)피어별 독립 할당
대역폭PCIe 링크 전체스위치 내부 대역폭 공유
활용 사례듀얼 컨트롤러 스토리지멀티-노드 클러스터, 분산 시스템

3-호스트 스타 토폴로지

IDT PCIe 스위치는 여러 개의 NT(Non-Transparent) 포트를 제공합니다. 각 호스트는 스위치의 NT 포트에 연결되며, 스위치 내부에서 주소 변환이 이루어집니다. 아래 다이어그램은 IDT 스위치를 중심으로 한 3-호스트 스타 토폴로지입니다:

Host A CPU A Root Complex ntb_hw_idt (Port 0) MW0→B, MW1→C, DB[0:7]→B, DB[8:15]→C peer_count=2 Host B CPU B Root Complex ntb_hw_idt (Port 2) MW0→A, MW1→C, DB[0:7]→A, DB[8:15]→C peer_count=2 Host C CPU C Root Complex ntb_hw_idt (Port 4) IDT 89HPES24NT6AG2 PCIe Switch with NT Ports NT Port 0 NT Port 4 NT Port 2 Internal Crossbar / Address Translation PCIe x4 PCIe x4 PCIe x4 각 호스트는 peer_count=2로 나머지 두 호스트와 독립적으로 통신 가능

멀티-포트 NTB API

멀티-포트 NTB에서는 피어(Peer) 인덱스를 지정하여 특정 호스트와 통신합니다. 커널의 NTB API는 pidx(peer index) 매개변수를 통해 이를 지원합니다:

/* 멀티-포트 NTB에서 피어 수 확인 */
int peer_count = ntb_peer_port_count(ntb);
pr_info("NTB: %d peers connected\n", peer_count);
/* IDT 스위치 3-호스트 구성에서: peer_count = 2 */

/* 각 피어의 포트 번호 확인 */
for (int i = 0; i < peer_count; i++) {
    int port = ntb_peer_port_number(ntb, i);
    pr_info("  Peer %d: port %d\n", i, port);
}

/* 특정 피어의 Memory Window 설정 (pidx=0 → 첫 번째 피어) */
ntb_mw_set_trans(ntb, 0,    /* pidx: 피어 인덱스 */
                 0,         /* widx: MW 인덱스 */
                 phys_addr,  /* 인바운드 변환 대상 물리 주소 */
                 mw_size);

/* 특정 피어의 Scratchpad에 쓰기 */
ntb_peer_spad_write(ntb, 1,  /* pidx: 두 번째 피어 */
                    0,       /* sidx: Scratchpad 인덱스 */
                    0xCAFE); /* 값 */

/* 특정 피어에게 Doorbell 전송 */
ntb_peer_db_set(ntb, BIT(0)); /* 모든 피어에게 브로드캐스트 */

NTB vs 대안 기술 비교

특성NTBCXLRDMA (InfiniBand/RoCE)공유 메모리 (SMP)
물리 계층PCIePCIe 물리 계층InfiniBand / Ethernet메모리 버스
지연 시간~1-5 us~200-400 ns~1-3 us~100 ns
대역폭PCIe 세대에 의존 (최대 ~64 GB/s)PCIe 세대에 의존최대 ~400 Gbps메모리 채널 대역폭
캐시 코히런시없음 (소프트웨어 관리)하드웨어 지원없음하드웨어 지원
호스트 수2~수 개 (스위치 기반 시 확장)다수 (CXL 스위치)수백~수천2~8 (NUMA)
CPU 부하중간 (memcpy 필요)낮음 (load/store)낮음 (RDMA 오프로드)낮음 (load/store)
프로그래밍 모델Memory Window + Doorbellload/store (캐시 코히런트)verb API / RDMA ops일반 메모리 접근
주요 용도듀얼 컨트롤러, HA메모리 풀링, 가속기HPC, 데이터센터단일 시스템
리눅스 지원drivers/ntb/drivers/cxl/drivers/infiniband/기본 내장
NTB 사용 시 주의사항:
  • BAR 크기 제한: Memory Window 크기는 NTB 디바이스의 BAR 크기에 의존합니다. BIOS에서 BAR 크기를 충분히 크게 설정하지 않으면 데이터 전송 효율이 저하됩니다. 일부 플랫폼은 최대 BAR 크기가 256MB로 제한됩니다.
  • IOMMU 상호작용: IOMMU(VT-d/AMD-Vi)가 활성화된 환경에서는 NTB Memory Window에 대한 DMA 매핑이 올바르게 설정되어야 합니다. intel_iommu=on 상태에서 NTB가 동작하지 않으면 IOMMU 도메인 설정을 확인하세요.
  • DMA 코히런시: 두 호스트 간에는 하드웨어 캐시 코히런시가 없습니다. 데이터를 기록한 후에는 반드시 wmb()(쓰기 배리어) 또는 dma_wmb()를 사용하고, 읽기 전에는 rmb() 또는 dma_rmb()를 사용해야 합니다.
  • 바이트 순서(Byte Order): 두 호스트의 엔디안(Endianness)이 다를 수 있으므로, 공유 데이터 구조는 고정 엔디안(le32_to_cpu() 등)을 사용해야 합니다.
  • 오류 복구: 상대 호스트가 리부팅되면 NTB 링크가 끊어집니다. 링크 이벤트 콜백을 반드시 구현하여 재연결 로직을 처리하세요.
실전 활용 및 성능 기대치:
  • 스토리지 듀얼 컨트롤러: NTB를 통해 두 스토리지 컨트롤러가 메타데이터와 캐시를 동기화하면 Active-Active 구성이 가능합니다. NVRAM 미러링에 NTB DMA를 활용하면 마이크로초 단위 동기화가 가능합니다.
  • 성능 기대치: PCIe Gen3 x16 기준 약 12-14 GB/s, PCIe Gen4 x16 기준 약 25-28 GB/s의 단방향 처리량(Throughput)을 기대할 수 있습니다. ntb_perf로 실측하여 확인하세요.
  • ntb_netdev 성능: TCP/IP 오버헤드가 추가되므로 원시 NTB 대역폭의 약 60-80%를 활용합니다. 최대 성능이 필요하면 ntb_transport를 직접 사용하거나 커스텀 클라이언트 드라이버를 작성하세요.
  • IDT 스위치 활용: IDT 89HPESxx 시리즈 스위치는 다중 NT 포트를 지원하여 3개 이상의 호스트를 NTB로 연결할 수 있습니다. 이는 멀티-노드 클러스터링에 유용합니다.

NTB 커널 소스 분석

소스 트리 구조

NTB 서브시스템의 커널 소스는 drivers/ntb/ 디렉토리에 위치합니다:

경로파일설명
drivers/ntb/ntb.cNTB 코어: 버스 등록, 디바이스/클라이언트 매칭, 통합 API
drivers/ntb/ntb_transport.c링 버퍼 기반 메시지 전달 계층
drivers/ntb/hw/intel/ntb_hw_intel.cIntel Xeon NTB 하드웨어 드라이버
drivers/ntb/hw/amd/ntb_hw_amd.cAMD NTB 하드웨어 드라이버
drivers/ntb/hw/idt/ntb_hw_idt.cIDT PCIe 스위치 NTB 드라이버 (멀티-포트 지원)
drivers/ntb/hw/mscc/ntb_hw_switchtec.cMicrosemi Switchtec NTB 드라이버
drivers/ntb/hw/epf/ntb_hw_epf.cPCIe Endpoint Function NTB 드라이버
drivers/ntb/test/ntb_tool.cdebugfs 기반 NTB 테스트/디버깅 도구
drivers/ntb/test/ntb_perf.cNTB DMA 성능 벤치마크 모듈
drivers/net/ntb_netdev.cNTB 가상 이더넷 드라이버 (net 서브시스템에 위치)
include/linux/ntb.hNTB API 헤더: 구조체, 함수 프로토타입, 인라인 래퍼

핵심 자료구조

NTB 서브시스템의 핵심 자료구조는 include/linux/ntb.h에 정의되어 있습니다:

/* include/linux/ntb.h — NTB 디바이스 구조체 */
struct ntb_dev {
    struct device dev;              /* 리눅스 디바이스 모델 */
    struct pci_dev *pdev;           /* 하위 PCI 디바이스 */
    const struct ntb_dev_ops *ops; /* HW 드라이버 오퍼레이션 */
    struct ntb_ctx_ops *ctx_ops;   /* 클라이언트 콜백 */
    void *ctx;                      /* 클라이언트 컨텍스트 */
    struct ntb_client *client;     /* 바인딩된 클라이언트 */
    enum ntb_topo topo;             /* B2B, RP, Switch 등 */
};

/* NTB 하드웨어 드라이버가 구현하는 오퍼레이션 테이블 */
struct ntb_dev_ops {
    /* 포트 관련 */
    int (*port_number)(struct ntb_dev *ntb);
    int (*peer_port_count)(struct ntb_dev *ntb);
    int (*peer_port_number)(struct ntb_dev *ntb, int pidx);

    /* 링크 제어 */
    int (*link_enable)(struct ntb_dev *ntb, enum ntb_speed max_speed,
                       enum ntb_width max_width);
    int (*link_disable)(struct ntb_dev *ntb);
    u64 (*link_is_up)(struct ntb_dev *ntb, enum ntb_speed *speed,
                      enum ntb_width *width);

    /* Doorbell */
    u64 (*db_read)(struct ntb_dev *ntb);
    int (*db_clear)(struct ntb_dev *ntb, u64 db_bits);
    int (*peer_db_set)(struct ntb_dev *ntb, u64 db_bits);

    /* Scratchpad */
    int (*spad_count)(struct ntb_dev *ntb);
    u32 (*spad_read)(struct ntb_dev *ntb, int sidx);
    int (*spad_write)(struct ntb_dev *ntb, int sidx, u32 val);
    int (*peer_spad_write)(struct ntb_dev *ntb, int pidx,
                           int sidx, u32 val);

    /* Memory Window */
    int (*mw_count)(struct ntb_dev *ntb, int pidx);
    int (*mw_get_align)(struct ntb_dev *ntb, int pidx, int widx,
                        resource_size_t *size, ...);
    int (*mw_set_trans)(struct ntb_dev *ntb, int pidx, int widx,
                        dma_addr_t addr, resource_size_t size);
};

/* NTB 클라이언트 구조체 */
struct ntb_client {
    struct device_driver drv;      /* 드라이버 모델 */
    struct ntb_client_ops ops;    /* probe/remove 콜백 */
};

/* 클라이언트 이벤트 콜백 */
struct ntb_ctx_ops {
    void (*link_event)(void *ctx);
    void (*db_event)(void *ctx, int vec);
};

ntb_register_device() 흐름

NTB 하드웨어 드라이버가 디바이스를 등록하면, NTB 코어가 등록된 클라이언트와 매칭을 수행합니다. 이 과정은 리눅스 디바이스 모델의 버스 매칭 메커니즘을 활용합니다:

/* drivers/ntb/ntb.c — 디바이스 등록 흐름 (간략화) */

/* 1. HW 드라이버의 probe에서 호출 */
int ntb_register_device(struct ntb_dev *ntb)
{
    /* ntb_bus 타입으로 디바이스 등록 */
    ntb->dev.bus = &ntb_bus;
    ntb->dev.parent = &ntb->pdev->dev;
    ntb->dev.release = ntb_dev_release;

    /* 디바이스 이름: "ntbN" (예: ntb0, ntb1) */
    dev_set_name(&ntb->dev, "ntb%d", ntb->pdev->devfn);

    /* device_register()가 내부적으로 bus->match()를 호출하여
     * 등록된 모든 ntb_client와 매칭 시도 */
    return device_register(&ntb->dev);
}
EXPORT_SYMBOL(ntb_register_device);

/* 2. NTB 버스 매칭 — 항상 true (모든 클라이언트가 모든 디바이스 수락) */
static int ntb_bus_match(struct device *dev,
                         struct device_driver *drv)
{
    return 1; /* 무조건 매칭: 클라이언트가 probe에서 거부 가능 */
}

/* 3. 매칭 후 클라이언트의 probe 호출 */
static int ntb_bus_probe(struct device *dev)
{
    struct ntb_dev *ntb = dev_ntb(dev);
    struct ntb_client *client = drv_ntb_client(dev->driver);

    /* 클라이언트의 probe 콜백 호출
     * (ntb_transport, ntb_perf, ntb_tool 등) */
    return client->ops.probe(client, ntb);
}

ntb_register_client() 매칭 메커니즘

클라이언트 드라이버가 ntb_register_client()를 호출하면, 이미 등록된 모든 NTB 디바이스와의 매칭이 시도됩니다:

/* drivers/ntb/ntb.c — 클라이언트 등록 */
int ntb_register_client(struct ntb_client *client)
{
    /* NTB 버스에 드라이버 등록 */
    client->drv.bus = &ntb_bus;

    /* driver_register()가 내부적으로
     * ntb_bus에 이미 등록된 디바이스와 매칭 시도 */
    return driver_register(&client->drv);
}
EXPORT_SYMBOL(ntb_register_client);

/* module_ntb_client() 매크로 — 간편 등록 */
#define module_ntb_client(__ntb_client) \
    module_driver(__ntb_client, ntb_register_client, \
                  ntb_unregister_client)

이러한 버스 기반 매칭 덕분에 NTB 하드웨어 드라이버와 클라이언트 드라이버는 서로에 대한 의존성 없이 독립적으로 로드/언로드할 수 있습니다. 하드웨어 드라이버가 먼저 로드되어 디바이스를 등록한 후 클라이언트가 로드되어도, 반대 순서여도 정상적으로 매칭이 이루어집니다.

참고자료

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.