본문으로 건너뛰기
Kernel Debugging · 9/9

drivers/cxl 코드 분석 — 진입점부터 sysfs까지

· Hawk · 8분 읽기
cxl kernel-source drivers sysfs cxl-core code-walkthrough

#디렉터리 구조

mainline(v7.3-rc6 기준) drivers/cxl/ 주요 파일:

파일·디렉터리모듈역할
acpi.ccxl_acpiACPI CEDT 파싱, CXL root·root decoder 등록
pci.ccxl_pciPCI 드라이버, 레지스터 매핑·mailbox·memdev 생성
mem.ccxl_memmemdev 드라이버, endpoint port 연결
pmem.c·security.ccxl_pmempersistent memory·보안 명령
port.ccxl_portport 드라이버
core/port.ccxl_coreport·dport·decoder 객체, CXL bus
core/region.ccxl_coreregion 생성·target attach·commit
core/memdev.ccxl_corememdev 객체·char device
core/hdm.ccxl_coreHDM decoder 레지스터 읽기·프로그래밍
core/mbox.ccxl_coremailbox 명령 공통부
core/regs.ccxl_corecomponent·device 레지스터 탐색·매핑
core/ras.ccxl_coreRAS capability, PCI error handler 본체
core/pmem.ccxl_coreLIBNVDIMM 연결용 nvdimm bridge·nvdimm 객체
core/suspend.csuspend.o (CONFIG_CXL_SUSPEND)활성 CXL 메모리 카운터(cxl_mem_active()). cxl_core와 별도 object

#모듈 구성과 순서

cxl_core가 공통 기반이고 나머지는 그 위에 올라갑니다. drivers/cxl/Makefile은 built-in일 때의 순서를 주석으로 밝혀 둡니다.

순서대상Makefile 주석이 밝힌 이유
1core/ (cxl_core)기본 초기화
2cxl_portCXL root port를 바로 enable하려고 platform root 드라이버(acpi)보다 먼저
3cxl_acpiplatform root
4cxl_pmem, cxl_memendpoint 드라이버보다 먼저, memdev를 바로 enable
5cxl_pci마지막. 하드웨어 열거 계층과 같은 순서

모듈로 빌드했다면 modprobe가 심볼 의존성(cxl_core 등)을 알아서 먼저 올립니다. cxl_acpi는 MODULE_SOFTDEP("pre: cxl_port")로 cxl_port를 먼저 올리게 해 둡니다.

#진입점 — 모듈별 init

각 모듈의 초기화 함수(발췌):

drivers/cxl/acpi.c
static int __init cxl_acpi_init(void)
{
return platform_driver_register(&cxl_acpi_driver);
}
subsys_initcall_sync(cxl_acpi_init);
// drivers/cxl/pci.c
static int __init cxl_pci_driver_init(void)
{
int rc;
rc = pci_register_driver(&cxl_pci_driver);
if (rc)
return rc;
rc = cxl_cper_register_work(&cxl_cper_work);
if (rc)
pci_unregister_driver(&cxl_pci_driver);
return rc;
}
module_init(cxl_pci_driver_init);
// drivers/cxl/mem.c
module_cxl_driver(cxl_mem_driver);

각 모듈이 다른 bus에 드라이버를 등록합니다 — cxl_acpi는 platform bus, cxl_pci는 PCI bus, cxl_mem·cxl_port는 CXL bus(cxl_driver_register()). cxl_acpi는 module_init()이 아니라 subsys_initcall_sync()를 씁니다. 소스 주석은 dax_hmem이 ‘Soft Reserved’ CXL 범위를 보기 전에 올라와야 하기 때문이라고 설명합니다.

#핵심 자료 구조

주요 struct와 정의 위치(mainline v7.3-rc6):

Struct위치주요 멤버
cxl_portdrivers/cxl/cxl.hdev, dports·endpoints·regions (xarray), nr_dports, commit_end
cxl_decoderdrivers/cxl/cxl.hhpa_range, interleave_ways, interleave_granularity, region, commit()·reset()
cxl_root_decoderdrivers/cxl/cxl.hres, qos_class, regions_lock, cxlsd
cxl_endpoint_decoderdrivers/cxl/cxl.hcxld, dpa_res, part, pos
cxl_regiondrivers/cxl/cxl.hcxlrd, mode, params(targets[]·nr_targets·state)
cxl_memdevdrivers/cxl/cxlmem.hdev, cdev, cxlds, endpoint
cxl_dev_stateinclude/cxl/cxl.hregs, part[], serial, cxl_mbox
cxl_mailboxinclude/cxl/mailbox.hpayload_size, mbox_mutex, mbox_wait, mbox_send()

#probe 흐름 추적

cxl_pci_probe()(drivers/cxl/pci.c)가 부르는 주요 함수, 소스 순서대로:

단계함수동작
1pcim_enable_device()PCI 디바이스 enable
2pci_find_dvsec_capability()CXL Device DVSEC 찾기. 없으면 경고만 하고 CXL.mem 초기화를 건너뜀
3cxl_memdev_state_create()상태 구조체 할당
4cxl_pci_setup_regs()레지스터 블록 찾아 매핑
5cxl_await_media_ready()미디어 준비 대기
6cxl_alloc_irq_vectors()MSI/MSI-X 할당
7cxl_pci_setup_mailbox()mailbox 초기화
8cxl_enumerate_cmds()지원 명령 목록 조회
9cxl_set_timestamp()·cxl_poison_state_init()시각 설정, poison 상태 초기화
10cxl_dev_state_identify()Identify Memory Device 명령
11cxl_mem_dpa_fetch()·cxl_dpa_setup()DPA 파티션 정보
12devm_cxl_add_classdev()memdev 생성, sysfs 등록
13cxl_event_config()이벤트 로그·인터럽트 설정

PCI 오류 처리는 probe에서 등록하지 않고, 드라이버 구조체의 정적 cxl_error_handlers가 맡습니다. ftrace function_graph로 이 흐름을 볼 수 있습니다(Ch 8).

#HDM Decoder commit 코드

drivers/cxl/core/hdm.c의 cxl_decoder_commit() (발췌):

static int cxl_decoder_commit(struct cxl_decoder *cxld)
{
struct cxl_port *port = to_cxl_port(cxld->dev.parent);
struct cxl_hdm *cxlhdm = dev_get_drvdata(&port->dev);
void __iomem *hdm = cxlhdm->regs.hdm_decoder;
int id = cxld->id, rc;
if (cxld->flags & CXL_DECODER_F_ENABLE)
return 0;
if (cxl_num_decoders_committed(port) != id) {
dev_dbg(&port->dev,
"%s: out of order commit, expected decoder%d.%d\n",
dev_name(&cxld->dev), port->id,
cxl_num_decoders_committed(port));
return -EBUSY;
}
/* (sanitize 진행 중인 endpoint decoder 거부 — 생략) */
scoped_guard(rwsem_read, &cxl_rwsem.dpa)
setup_hw_decoder(cxld, hdm);
rc = cxld_await_commit(hdm, cxld->id);
if (rc) {
dev_dbg(&port->dev, "%s: error %d committing decoder\n",
dev_name(&cxld->dev), rc);
return rc;
}
port->commit_end++;
cxld->flags |= CXL_DECODER_F_ENABLE;
return 0;
}

읽을 점:

  • 한 port의 decoder는 번호 순서대로 commit해야 합니다. 순서가 어긋나면 -EBUSY.
  • 레지스터 쓰기는 setup_hw_decoder()가 하고, cxld_await_commit()이 Committed 비트를 기다립니다. 1 ms 간격으로 최대 COMMIT_TIMEOUT_MS(20)회 확인하고, Commit Error 비트가 서면 -EIO, 시간이 다 되면 -ETIMEDOUT.
  • cxl_rwsem.region은 이 함수가 아니라 호출 쪽(region commit 경로)이 쓰기 잠금으로 잡습니다.

#Region 생성 sysfs path

사용자가 cxl create-region 했을 때 cxl-cli가 sysfs에 쓰는 순서와 커널 쪽 경로(drivers/cxl/core/region.c):

단계사용자 동작커널 쪽
1root decoder의 create_ram_region을 읽어 새 region 이름을 얻고, 그 이름을 다시 씀create_ram_region_store() → __create_region() → devm_cxl_add_region() → cxl_region_alloc()
2region의 interleave_ways·interleave_granularity·size 설정각 attribute의 store 함수
3target0, target1, …에 endpoint decoder 이름 쓰기__attach_target() → cxl_region_attach()
4commit에 1 쓰기commit_store() → __commit() → cxl_region_decode_commit()
5(커널) endpoint부터 root 쪽으로 각 port의 decoder commitcommit_decoder() → cxld->commit() = cxl_decoder_commit()
6region을 cxl_region 드라이버에 bindcxl_region_probe() → RAM이면 devm_cxl_add_dax_region()

endpoint decoder의 DPA 할당(dpa_size·mode)은 3단계 전에 해 둬야 하고, cxl-cli가 이를 대신해 줍니다. region을 System RAM으로 올리는 것은 commit이 아니라 6단계 뒤 dax·kmem 드라이버입니다.

#Mailbox API 구현

공통 진입점은 drivers/cxl/core/mbox.c의 cxl_internal_send_cmd()입니다.

int cxl_internal_send_cmd(struct cxl_mailbox *cxl_mbox,
struct cxl_mbox_cmd *mbox_cmd)
{
size_t out_size, min_out;
int rc;
if (mbox_cmd->size_in > cxl_mbox->payload_size ||
mbox_cmd->size_out > cxl_mbox->payload_size)
return -E2BIG;
out_size = mbox_cmd->size_out;
min_out = mbox_cmd->min_out;
rc = cxl_mbox->mbox_send(cxl_mbox, mbox_cmd);
/* ... return code → errno 변환, 출력 크기 검사 ... */
}

실제 하드웨어 접근은 mbox_send에 꽂힌 cxl_pci_mbox_send()(drivers/cxl/pci.c)가 합니다.

  • mbox_mutex를 잡고 __cxl_pci_mbox_send_cmd()를 부릅니다. 모든 명령이 이 mutex로 직렬화됩니다.
  • payload를 쓰고 doorbell을 울린 뒤, doorbell이 내려가길 polling합니다. 한도는 CXL_MAILBOX_TIMEOUT_MS로, 정의는 (2 * HZ)라 jiffies 단위 2초입니다.
  • 디바이스가 background 명령으로 응답하면 명령별 poll_interval_ms·poll_count만큼 기다립니다. Sanitize는 예외로, 비동기로 돌리고 사용자 공간이 poll(2)로 완료를 기다립니다.

자주 쓰는 opcode (drivers/cxl/cxlmem.h):

Opcode명령
0x0100Get Event Records
0x0200Get FW Info
0x0201Transfer FW
0x4000Identify (Memory Device)
0x4102Get LSA (Label Storage Area)
0x4103Set LSA
0x4200Get Health Info
0x4300Get Poison List

#에러 처리 — pci_error_handlers

drivers/cxl/pci.c의 PCI error handler:

static const struct pci_error_handlers cxl_error_handlers = {
.error_detected = cxl_error_detected,
.slot_reset = cxl_slot_reset,
.resume = cxl_error_resume,
.cor_error_detected = cxl_cor_error_detected,
.reset_done = cxl_reset_done,
};

cxl_error_detected()(drivers/cxl/core/ras.c)는 RAS capability 레지스터를 읽어 기록한 뒤 채널 상태로 나눕니다.

채널 상태반환
pci_channel_io_normaluncorrectable이면 memdev 드라이버 해제 후 NEED_RESET, 아니면 CAN_RECOVER
pci_channel_io_frozen“disable CXL.mem” 경고, memdev 드라이버 해제 후 NEED_RESET
pci_channel_io_perm_failureDISCONNECT

memdev에 드라이버가 붙어 있지 않으면 처리를 포기하고 DISCONNECT를 돌려줍니다.

#NUMA 통합 — dax와 kmem

region이 바로 System RAM이 되지는 않습니다. RAM region은 devm_cxl_add_dax_region()으로 dax region이 되고, 메모리로 붙이는 건 drivers/dax/kmem.c가 합니다.

단계위치동작
1drivers/dax/cxl.cphys_to_target_node()로 region 시작 주소의 NUMA 노드 결정
2drivers/dax/kmem.cmt_calc_adistance()로 abstract distance 계산, memory tier 타입 지정
3drivers/dax/kmem.c__add_memory_driver_managed()로 메모리 hot-add

dax 장치가 system-ram 모드면 kmem이 붙어 numactl --hardware에 노드가 보입니다. daxctl reconfigure-device -m system-ram이 이 전환입니다. cxl_region_probe()는 이미 System RAM으로 올라간 범위면 dax region을 만들지 않습니다.

#동시성 — Lock 사용

Lock위치보호 대상
cxl_rwsem.regioncore/core.hregion 구성·decoder commit (쓰기), 조회 (읽기)
cxl_rwsem.dpacore/core.hDPA 할당
cxlrd->regions_lockcxl_root_decoderregion 발견·생성·삭제 동기화
cxl_mbox->mbox_mutexcxl_mailboxmailbox 직렬화
mds->event.log_lockcxlmem.hevent 버퍼·로그 사용

lockdep_assert_held_write(&cxl_rwsem.region)이 region.c 곳곳에 있어, lockdep을 켜면 잠금 없이 들어온 경로가 경고로 드러납니다.

#자주 만나는 함정

증상원인
cxl_decoder_commit() -EBUSY같은 port의 decoder를 번호 순서대로 commit하지 않음 (“out of order commit”)
cxl_decoder_commit() -ETIMEDOUTCommitted 비트가 COMMIT_TIMEOUT_MS(20)회·1 ms 간격 안에 서지 않음
cxl_decoder_commit() -EIOdecoder의 Commit Error 비트가 섬
Lockdep WARNINGcxl_rwsem.region을 잡지 않고 region 경로에 들어옴 (lockdep_assert_held_write)
commit에 0 쓰기가 -EPERM플랫폼이 잠근 region(CXL_REGION_F_LOCK)
dmesg “mailbox timeout (opcode: …)”doorbell이 2초(CXL_MAILBOX_TIMEOUT_MS) 안에 내려가지 않음

#정리

  • drivers/cxl/는 cxl_core를 기반으로 cxl_port·cxl_acpi·cxl_pmem·cxl_mem·cxl_pci가 올라가는 구조입니다.
  • 진입점은 모듈별 초기화 함수이고, 각각 *다른 bus(platform·PCI·CXL)*에 드라이버를 등록합니다.
  • 핵심 자료구조는 cxl_port·cxl_decoder·cxl_region·cxl_memdev·cxl_mailbox 등 8가지입니다.
  • HDM Decoder commit은 port마다 번호 순서로 진행하고, cxld_await_commit()이 Committed 비트를 기다립니다. cxl_rwsem.region은 호출 쪽이 잡습니다.
  • Region 생성은 create_ram_region → 파라미터 → targetN attach → commit → probe → dax → kmem. System RAM 추가는 commit이 아니라 dax·kmem 단계입니다.
  • Mailbox는 mbox_mutex로 직렬화되고, doorbell 대기는 공통 2초입니다. 명령별로 다른 것은 background 명령의 poll 간격과 횟수입니다.
  • PCI error handler는 채널 상태에 따라 CAN_RECOVER·NEED_RESET·DISCONNECT를 돌려줍니다.

#다음 장 예고

Kernel Debugging 시리즈의 CXL 관련 추가 챕터는 여기까지. 다음 깊이는 Memory Diagnostics와 Postmortem Debugging에 분산된 챕터로 자연 연결.

#관련 항목