NTB (Non-Transparent Bridge)
PCIe NTB 아키텍처: Scratchpad/Doorbell/MW 메커니즘, ntb_hw_intel/ntb_hw_amd/switchtec 드라이버, ntb_transport/ntb_netdev/ntb_perf, DMA 통합, NUMA 최적화, 멀티호스트 클러스터
핵심 요약
- 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의 주요 사용 사례:
- 멀티-호스트 시스템: 두 대 이상의 서버가 PCIe 패브릭을 통해 직접 통신
- HA(고가용성) 클러스터링: 장애 발생 시 빠른 페일오버를 위한 저지연 인터커넥트
- 스토리지 어레이: 듀얼 컨트롤러 스토리지 시스템에서 컨트롤러 간 데이터 미러링
- 텔레콤 블레이드: ATCA/MicroTCA 플랫폼에서 블레이드 간 고속 통신
- 임베디드 시스템: SoC 간 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 하드웨어 구현체
리눅스 커널에서 지원하는 주요 NTB 하드웨어:
| 하드웨어 | 커널 드라이버 | 특징 |
|---|---|---|
| Intel Xeon (Sandy Bridge ~ Skylake) | ntb_hw_intel | Intel QPI/UPI 기반 NTB, B2B(Back-to-Back) 및 RP(Root Port) 모드 지원 |
| AMD | ntb_hw_amd | AMD Data Fabric 기반 NTB, Zen 아키텍처 지원 |
| IDT 89HPESxx | ntb_hw_idt | IDT(현 Renesas) PCIe 스위치 칩, 다중 NT 포트 지원, 유연한 토폴로지 |
| Microsemi/Microchip Switchtec | ntb_hw_switchtec | Switchtec PCIe 스위치 기반 NTB, 핫플러그 지원, 높은 대역폭 |
| AMD EPYC | ntb_hw_epyc | AMD EPYC 서버 프로세서 전용 NTB, NUMA 인식 최적화 |
NTB 통신 흐름
Linux NTB 서브시스템 아키텍처
리눅스 커널의 NTB 서브시스템(drivers/ntb/)은 3계층 구조로 설계되어 있습니다:
- NTB 하드웨어 드라이버 계층: 벤더별 하드웨어 추상화 (
ntb_hw_intel,ntb_hw_amd,ntb_hw_idt,ntb_hw_switchtec) - NTB 코어 (
ntb.ko): 통합 API를 제공하는 버스 프레임워크, 하드웨어 드라이버와 클라이언트 드라이버를 매칭 - NTB 클라이언트 드라이버:
ntb_transport— 링 버퍼(Ring Buffer) 기반 메시지 전달 계층ntb_netdev— NTB 위의 가상 이더넷 인터페이스ntb_perf— DMA 성능 벤치마크 도구ntb_tool— debugfs 기반 디버깅 인터페이스
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를 링 버퍼로 분할하여 사용합니다:
- TX 링 버퍼: 상대 호스트의 Memory Window에 매핑되어 데이터를 쓰는 영역
- RX 링 버퍼: 자신의 Memory Window에 할당되어 상대가 쓴 데이터를 읽는 영역
- Doorbell 알림: 데이터 기록 후 Doorbell을 울려 상대에게 새 데이터 도착을 알림
- 플로우 컨트롤: 링 버퍼의 head/tail 포인터로 오버플로우를 방지
/* 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_netdev: 가상 이더넷 인터페이스
ntb_netdev는 ntb_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 관련 설정이 필요한 경우가 많습니다:
- Split Root Complex 모드: Intel 플랫폼에서는 IIO 설정에서 PCIe 포트를 NTB 모드로 전환해야 합니다
- BAR 크기 설정: BIOS에서 NTB BAR의 크기를 충분히 크게 설정 (Memory Window 크기에 직접 영향)
- ACPI/DSDT: NTB 디바이스가 ACPI 테이블에 정의되어 플랫폼별 자원(인터럽트, 전원 관리)을 할당받음
- Device Tree (ARM/임베디드): DT 바인딩으로 NTB 컨트롤러의 레지스터 영역, 인터럽트, BAR 매핑 정의
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 엔진 데이터 흐름
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를 고려해야 하는 핵심 사항입니다:
- 인바운드 버퍼:
ntb_mw_set_trans()로 설정하는 수신 버퍼는 NTB 디바이스와 같은 NUMA 노드에 할당해야 합니다. 원격 호스트의 MMIO 쓰기가 NTB를 통해 로컬 메모리에 도달할 때, 같은 NUMA 노드의 메모리 컨트롤러를 사용하면 지연이 줄어듭니다. - 아웃바운드 전송:
memcpy_toio()를 호출하는 CPU도 NTB가 속한 NUMA 노드의 CPU여야 합니다. 원격 NUMA 노드의 CPU에서 MMIO 쓰기를 수행하면 QPI/UPI 홉이 추가되어 지연이 증가합니다. - DMA 엔진 선택: DMA 채널도 NUMA 노드를 인식하여 선택해야 합니다. Intel IOAT/CBDMA 채널은 특정 소켓에 바인딩되어 있으므로, NTB와 같은 소켓의 DMA 채널을 사용해야 합니다.
/* 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_amd | ntb_hw_idt, ntb_hw_switchtec |
| MW/Doorbell | 단일 피어(Peer) | 피어별 독립 할당 |
| 대역폭 | PCIe 링크 전체 | 스위치 내부 대역폭 공유 |
| 활용 사례 | 듀얼 컨트롤러 스토리지 | 멀티-노드 클러스터, 분산 시스템 |
3-호스트 스타 토폴로지
IDT PCIe 스위치는 여러 개의 NT(Non-Transparent) 포트를 제공합니다. 각 호스트는 스위치의 NT 포트에 연결되며, 스위치 내부에서 주소 변환이 이루어집니다. 아래 다이어그램은 IDT 스위치를 중심으로 한 3-호스트 스타 토폴로지입니다:
멀티-포트 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 대안 기술 비교
| 특성 | NTB | CXL | RDMA (InfiniBand/RoCE) | 공유 메모리 (SMP) |
|---|---|---|---|---|
| 물리 계층 | PCIe | PCIe 물리 계층 | 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 + Doorbell | load/store (캐시 코히런트) | verb API / RDMA ops | 일반 메모리 접근 |
| 주요 용도 | 듀얼 컨트롤러, HA | 메모리 풀링, 가속기 | HPC, 데이터센터 | 단일 시스템 |
| 리눅스 지원 | drivers/ntb/ | drivers/cxl/ | drivers/infiniband/ | 기본 내장 |
- 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.c | NTB 코어: 버스 등록, 디바이스/클라이언트 매칭, 통합 API |
drivers/ntb/ | ntb_transport.c | 링 버퍼 기반 메시지 전달 계층 |
drivers/ntb/hw/intel/ | ntb_hw_intel.c | Intel Xeon NTB 하드웨어 드라이버 |
drivers/ntb/hw/amd/ | ntb_hw_amd.c | AMD NTB 하드웨어 드라이버 |
drivers/ntb/hw/idt/ | ntb_hw_idt.c | IDT PCIe 스위치 NTB 드라이버 (멀티-포트 지원) |
drivers/ntb/hw/mscc/ | ntb_hw_switchtec.c | Microsemi Switchtec NTB 드라이버 |
drivers/ntb/hw/epf/ | ntb_hw_epf.c | PCIe Endpoint Function NTB 드라이버 |
drivers/ntb/test/ | ntb_tool.c | debugfs 기반 NTB 테스트/디버깅 도구 |
drivers/ntb/test/ | ntb_perf.c | NTB DMA 성능 벤치마크 모듈 |
drivers/net/ | ntb_netdev.c | NTB 가상 이더넷 드라이버 (net 서브시스템에 위치) |
include/linux/ | ntb.h | NTB 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 하드웨어 드라이버와 클라이언트 드라이버는 서로에 대한 의존성 없이 독립적으로 로드/언로드할 수 있습니다. 하드웨어 드라이버가 먼저 로드되어 디바이스를 등록한 후 클라이언트가 로드되어도, 반대 순서여도 정상적으로 매칭이 이루어집니다.
참고자료
- Linux Kernel NTB Documentation
- drivers/ntb/ 커널 소스 트리
- PCI-SIG — PCIe 표준 규격
- Intel NTB Technical Reference
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.