CPU 캐시 심화 (CPU Cache)
CPU 캐시는 프로세서와 메인 메모리 사이의 속도 차이(수백 배)를 완화하기 위한 고속 SRAM 버퍼입니다. 리눅스 커널은 캐시 라인 정렬, 코히런시, TLB 관리, 프리페칭 등 다양한 수준에서 캐시를 인식하고 활용합니다. 이 페이지에서는 하드웨어 캐시의 원리부터 커널의 캐시 API, 실전 진단까지 종합적으로 다룹니다.
핵심 요약
- L1/L2/L3 — 캐시 계층. L1이 가장 빠르고 작으며(32–64KB), L3가 가장 크고 느립니다(수십 MB).
- 캐시 라인 — 64바이트 단위로 데이터를 캐시에 적재합니다. 연속 메모리 접근이 빠른 이유입니다.
- MESI 프로토콜 — 멀티코어 환경에서 캐시 일관성(coherency)을 유지하는 프로토콜입니다.
- TLB — 페이지 테이블 변환 결과를 캐싱하는 특수 캐시로, 가상→물리 주소 변환을 가속합니다.
- False Sharing — 서로 다른 변수가 같은 캐시 라인에 있어 불필요한 동기화가 발생하는 성능 문제입니다.
단계별 이해
- 캐시 확인 —
lscpu또는getconf -a | grep CACHE로 시스템의 캐시 크기와 라인 크기를 확인합니다./sys/devices/system/cpu/cpu0/cache/에서 상세 정보를 볼 수 있습니다. - 적중/미스 이해 — 데이터가 캐시에 있으면 Hit(수 ns), 없으면 Miss(수십~수백 ns)입니다.
perf stat -e cache-misses,cache-references ./app으로 캐시 효율을 측정합니다. - 코히런시 이해 — 코어 A가 데이터를 수정하면, 코어 B의 같은 캐시 라인이 무효화(Invalidate)됩니다.
이것이 MESI 프로토콜의 핵심이며, 멀티스레드 프로그램 성능에 큰 영향을 줍니다.
- 최적화 기법 — 데이터를 캐시 라인에 정렬하고, False Sharing을 피하면 성능이 크게 향상됩니다.
커널에서는
____cacheline_aligned매크로로 구조체 필드를 정렬합니다.
1. 캐시 기본 원리
캐시 라인
캐시의 최소 전송 단위는 캐시 라인(cache line)으로, 현대 x86/ARM 프로세서에서 대부분 64바이트입니다. 메모리 주소를 캐시 라인 크기로 나눈 몫이 같은 바이트들은 항상 함께 캐시에 올라옵니다. 따라서 구조체의 핫 필드를 같은 캐시 라인에 배치하면 하나의 캐시 미스로 여러 필드를 동시에 읽을 수 있습니다.
/* include/linux/cache.h */
#ifndef L1_CACHE_BYTES
#define L1_CACHE_BYTES (1 << L1_CACHE_SHIFT) /* x86: 64 */
#endif
#define ____cacheline_aligned __attribute__((__aligned__(L1_CACHE_BYTES)))
/* 핫 필드를 캐시 라인 경계에 정렬 */
struct net_device {
char name[IFNAMSIZ];
/* ... 핫 패스 필드 ... */
unsigned long state;
/* 콜드 필드는 별도 캐시 라인으로 분리 */
struct list_head dev_list ____cacheline_aligned_in_smp;
};
공간적 / 시간적 지역성
캐시가 효과적인 이유는 프로그램의 지역성(locality) 때문입니다.
- 시간적 지역성(Temporal Locality): 최근 접근한 데이터는 곧 다시 접근될 가능성이 높습니다. 예: slab 캐시에서 자주 할당/해제되는 객체.
- 공간적 지역성(Spatial Locality): 접근한 주소 근처의 데이터도 곧 접근될 가능성이 높습니다. 예: 페이지 테이블 워크에서 연속된 PTE 읽기.
히트와 미스
캐시 히트(hit)는 요청한 데이터가 캐시에 존재하는 경우이고, 미스(miss)는 존재하지 않아 하위 메모리 계층에서 가져와야 하는 경우입니다. 미스는 세 가지로 분류됩니다:
- Compulsory (cold) miss: 데이터를 처음 접근할 때. 프리페칭으로 완화 가능.
- Capacity miss: 캐시 용량이 부족하여 이전 데이터가 축출. 더 큰 캐시나 작업 집합(working set) 축소로 완화.
- Conflict miss: 연관도(associativity)가 부족하여 같은 셋에 매핑된 라인끼리 충돌. 더 높은 연관도로 완화.
cold 페이지는 캐시에 없을 가능성이 높은 페이지, hot 페이지는 캐시에 있을 가능성이 높은 페이지를 의미합니다. 페이지 할당자의 per-CPU 리스트는 hot/cold 페이지를 구분하여 캐시 효율을 높입니다.
2. 캐시 계층 구조
L1 캐시
L1 캐시는 CPU 코어에 가장 가까운 캐시로, 명령어 캐시(L1I)와 데이터 캐시(L1D)로 분리(Harvard 구조)되어 있습니다. 접근 지연은 약 4~5 사이클이며, 코어당 독립적으로 존재합니다.
L2 캐시
L2 캐시는 명령어와 데이터를 통합(unified)하여 저장하며, 접근 지연은 약 12~14 사이클입니다. 대부분의 현대 프로세서에서 코어별 또는 코어 클러스터별로 할당됩니다.
L3 / LLC (Last-Level Cache)
L3 캐시는 패키지 내 여러 코어가 공유하는 LLC(Last-Level Cache)입니다. 접근 지연은 약 30~40 사이클이며, 용량은 수 MB에서 수백 MB(AMD 3D V-Cache)까지 다양합니다. Intel은 LLC를 코어별 슬라이스로 분산하고 링 버스/메시 인터커넥트로 연결하며, AMD는 CCX 단위로 L3를 공유합니다.
포함 / 배제 / NINE 정책
| 정책 | 특성 | 예시 |
|---|---|---|
| Inclusive | L3가 L2/L1 내용을 모두 포함. 스누프가 L3만 확인하면 되어 코히런시 간단. | Intel Broadwell 이전 |
| Exclusive | 각 레벨에 데이터가 한 곳에만 존재. 총 유효 용량이 L1+L2+L3. | AMD Zen1~Zen3 |
| NINE | Non-Inclusive Non-Exclusive. L3 축출이 L2를 무효화하지 않음. | Intel Skylake-SP+, AMD Zen4+ |
3. 캐시 연관도
직접 사상 (Direct-Mapped)
각 메모리 블록이 캐시의 정확히 한 위치에만 매핑됩니다. 구현이 간단하고 접근이 빠르지만 conflict miss가 빈번합니다. 현대 프로세서에서는 거의 사용되지 않습니다.
N-Way 집합 연관 (Set-Associative)
현대 캐시의 표준 방식입니다. 캐시를 여러 셋(set)으로 나누고, 각 셋에 N개의 웨이(way)를 둡니다. 메모리 주소는 하나의 셋에 매핑되지만, 그 셋 내 N개 웨이 중 아무 곳에나 저장될 수 있습니다.
셋 인덱스 계산:
셋 수 = 캐시 크기 / (캐시 라인 크기 × 연관도)
셋 인덱스 = (주소 / 캐시 라인 크기) % 셋 수
완전 연관 (Fully-Associative)
메모리 블록이 캐시의 어느 위치에나 저장될 수 있습니다. Conflict miss가 없지만 검색 비용이 높아 TLB 같은 소규모 캐시에 주로 사용됩니다.
태그 / 셋 / 오프셋 비트 분해
물리 주소는 세 영역으로 분해됩니다:
| 필드 | 비트 수 (예: 32KB 8-way, 64B line) | 용도 |
|---|---|---|
| Offset | 6 (log2(64)) | 캐시 라인 내 바이트 위치 |
| Set Index | 6 (log2(64 sets)) | 캐시 셋 선택 |
| Tag | 나머지 비트 | 캐시 라인 식별 |
/* arch/x86/kernel/cpu/cacheinfo.c — ci_leaf_init() */
static void ci_leaf_init(struct cacheinfo *this_leaf,
struct _cpuid4_info_regs *base)
{
this_leaf->level = base->eax.split.level;
this_leaf->type = base->eax.split.type;
this_leaf->coherency_line_size = base->ebx.split.coherency_line_size + 1;
this_leaf->ways_of_associativity = base->ebx.split.ways_of_associativity + 1;
this_leaf->size = this_leaf->number_of_sets *
this_leaf->coherency_line_size *
this_leaf->ways_of_associativity;
}
/sys/devices/system/cpu/cpu0/cache/index0/에서 ways_of_associativity, number_of_sets, coherency_line_size, size 등을 확인할 수 있습니다.
4. 캐시 교체 정책
LRU / Pseudo-LRU
캐시 셋이 가득 찼을 때 어떤 라인을 축출할지 결정하는 정책입니다.
- LRU (Least Recently Used): 가장 오래 사용되지 않은 라인을 축출. 이상적이지만 높은 연관도에서 상태 추적 비용이 큼.
- Pseudo-LRU (PLRU): 트리 기반의 근사 LRU. 8-way 셋에서 3비트 트리로 축출 후보를 O(1)에 결정. 현대 x86 프로세서에서 가장 널리 사용.
적응형 교체
- Intel Adaptive Replacement: Skylake 이후 LLC에서 접근 빈도와 최근성을 모두 고려하는 적응형 정책 사용.
- AMD: L3에서 워크로드에 따라 동적으로 교체 정책을 조정.
- ARM: 일부 구현에서 Random 교체를 사용하여 병적(pathological) 패턴 방지.
5. 쓰기 정책
Write-Back
대부분의 현대 프로세서가 기본으로 사용하는 정책입니다. 쓰기 시 캐시만 갱신하고, 더티(dirty) 비트를 설정합니다. 캐시 라인이 축출될 때만 메모리에 기록하므로 메모리 대역폭을 절약합니다.
Write-Through
쓰기 시 캐시와 메모리를 동시에 갱신합니다. 코히런시 관리가 간단하지만 쓰기 대역폭을 많이 소모합니다. 일부 임베디드 시스템이나 특수 용도에서 사용됩니다.
Write-Allocate / No-Write-Allocate
- Write-Allocate (Fetch on Write): 쓰기 미스 시 캐시 라인을 먼저 로드한 후 쓰기. Write-Back과 조합이 일반적.
- No-Write-Allocate: 쓰기 미스 시 메모리에 직접 기록하고 캐시에 올리지 않음. Write-Through와 조합.
Write-Combining (WC)
WC는 캐시를 거치지 않고 쓰기 결합 버퍼(WC buffer)에 쓰기를 모아서 버스트 전송합니다. 프레임버퍼, MMIO 영역 등 순서가 중요하지 않은 비캐시 가능 영역에 적합합니다.
| 정책 | 쓰기 시 동작 | 캐시 가능 | 용도 |
|---|---|---|---|
| Write-Back (WB) | 캐시만 갱신, 축출 시 기록 | O | 일반 메모리 (기본) |
| Write-Through (WT) | 캐시 + 메모리 동시 기록 | O | 특수 코히런시 요구 |
| Write-Combining (WC) | WC 버퍼에 모아서 버스트 | X | 프레임버퍼, MMIO |
| Uncacheable (UC) | 직접 메모리 접근 | X | 디바이스 레지스터 |
/* 프레임버퍼를 Write-Combining으로 설정 */
int set_memory_wc(unsigned long addr, int numpages);
int set_memory_wb(unsigned long addr, int numpages);
/* arch/x86/mm/pat/set_memory.c */
int set_memory_wc(unsigned long addr, int numpages)
{
return change_page_attr_set(&addr, numpages,
cachemode2pgprot(_PAGE_CACHE_MODE_WC),
0);
}
ioremap_wc()와 set_memory_wc()를 혼용하면 PAT(Page Attribute Table) 엔트리가 충돌할 수 있습니다. 항상 매핑 해제 후 새 타입으로 재매핑하세요.
6. 캐시 코히런시 프로토콜
멀티코어 시스템에서 여러 코어의 캐시가 동일 메모리 주소의 다른 값을 가지면 안 됩니다. 캐시 코히런시 프로토콜은 각 캐시 라인의 상태를 추적하여 일관성을 보장합니다.
MESI 프로토콜
가장 기본적인 코히런시 프로토콜로, 각 캐시 라인은 네 가지 상태 중 하나입니다:
- Modified (M): 이 코어만 가지고 있으며, 메모리보다 새로운 값 (dirty).
- Exclusive (E): 이 코어만 가지고 있으며, 메모리와 같은 값 (clean).
- Shared (S): 여러 코어가 가지고 있으며, 메모리와 같은 값.
- Invalid (I): 유효하지 않은 라인 (빈 슬롯과 동일).
MOESI (AMD)
AMD는 MESI에 Owned (O) 상태를 추가한 MOESI 프로토콜을 사용합니다. Owned 상태의 코어는 다른 코어들과 데이터를 공유하면서도 수정된 값의 책임을 집니다 — 메모리에 쓰기를 지연시키면서 스누프 요청에 직접 응답할 수 있어, Modified→Shared 전환 시 불필요한 메모리 쓰기를 회피합니다.
MESIF (Intel)
Intel은 MESI에 Forward (F) 상태를 추가한 MESIF 프로토콜을 사용합니다. Shared 상태의 여러 코어 중 하나가 Forward로 지정되어 스누프 요청에 응답하는 역할을 합니다. 이를 통해 Shared 상태에서 여러 코어가 동시에 응답하는 중복을 방지합니다.
스누핑 vs 디렉토리 기반
| 메커니즘 | 원리 | 확장성 | 구현 예 |
|---|---|---|---|
| 스누핑 | 모든 코어가 버스 트래픽을 감시(snoop) | 낮음 (~8코어) | 초기 SMP |
| Snoop Filter | LLC에 태그 디렉토리 유지, 불필요한 스누프 억제 | 중간 | Intel Skylake-SP+ |
| Probe Filter | AMD의 LLC 기반 디렉토리. HT Assist(Probe Filter)로 스누프 트래픽 감소 | 중간 | AMD Zen |
| 디렉토리 기반 | 중앙 디렉토리가 캐시 라인 위치를 추적 | 높음 (수백 코어) | ARM CHI HN-F, Intel CXL |
smp_wmb()/smp_rmb() 같은 메모리 배리어는 코히런시 프로토콜이 전파를 완료하기 전에 다른 코어가 순서가 뒤바뀐 값을 관찰하는 것을 방지합니다.
7. TLB (Translation Lookaside Buffer)
TLB는 가상→물리 주소 변환 결과를 캐싱하는 특수 캐시로, 페이지 테이블 워크 비용(수십~수백 사이클)을 1~2 사이클로 줄입니다.
TLB 계층
| 레벨 | 유형 | 엔트리 수 (예: Intel Golden Cove) | 연관도 |
|---|---|---|---|
| L1 DTLB | 데이터 | 4K: 96, 2M: 32, 1G: 8 | 완전 연관 |
| L1 ITLB | 명령어 | 4K: 256, 2M/4M: 8 | 8-way |
| L2 STLB | 통합 | 4K+2M: 2048 | 16-way |
Hugepage와 TLB Reach
TLB reach는 TLB가 커버할 수 있는 최대 가상 주소 범위입니다:
TLB reach = TLB 엔트리 수 × 페이지 크기
4K × 2048 = 8MB /* L2 STLB, 4K 페이지 */
2M × 2048 = 4GB /* L2 STLB, 2M hugepage */
Hugepage(2MB/1GB)를 사용하면 동일한 TLB 엔트리 수로 훨씬 넓은 범위를 커버하여 TLB 미스를 크게 줄일 수 있습니다. 커널의 THP(Transparent Huge Pages)는 이를 자동으로 활용합니다.
TLB Shootdown
페이지 테이블을 변경한 후 다른 코어의 TLB에 남아 있는 오래된 매핑을 무효화해야 합니다. 이를 TLB shootdown이라 하며, IPI(Inter-Processor Interrupt)를 사용합니다.
/* mm/tlb.c — TLB 일괄 플러시 */
void flush_tlb_mm_range(struct mm_struct *mm,
unsigned long start, unsigned long end,
unsigned int stride_shift, bool freed_tables)
{
/* 로컬 CPU 플러시 */
if (cpumask_any_but(mm_cpumask(mm), smp_processor_id()) < nr_cpu_ids)
flush_tlb_others(mm_cpumask(mm), &info); /* IPI 전송 */
else
local_flush_tlb();
}
/* PCID (Process Context ID): TLB 태그로 프로세스 구분 → 컨텍스트 스위치 시 전체 플러시 불필요 */
/* ASID (ARM): 동일 목적, 8~16비트 태그 */
munmap()이나 메모리 해제 시 빈번히 발생하므로, 지나치게 잦은 VMA 조작은 성능 저하의 원인이 됩니다. PCID/ASID를 활용하면 전체 TLB 플러시 대신 선택적 무효화가 가능합니다.
8. 캐시 프리페칭
하드웨어 프리페처
현대 프로세서는 메모리 접근 패턴을 감지하여 자동으로 데이터를 미리 캐시에 로드합니다:
- Stride prefetcher: 일정한 간격(stride)의 접근 패턴을 감지. 배열 순회에 효과적.
- Stream prefetcher: 연속 주소 접근을 감지하여 다음 캐시 라인을 미리 로드.
- Spatial prefetcher: 캐시 라인 쌍(pair)을 함께 로드. Intel L2 Adjacent Cache Line Prefetcher.
- Intel L2 Streamer: L2 미스를 모니터링하여 최대 20 캐시 라인 앞까지 프리페치.
소프트웨어 프리페치
커널은 명시적 프리페치 명령으로 하드웨어 프리페처를 보완합니다:
/* include/linux/prefetch.h */
#define prefetch(x) __builtin_prefetch(x, 0, 3) /* 읽기, 높은 시간적 지역성 */
#define prefetchw(x) __builtin_prefetch(x, 1, 3) /* 쓰기, 높은 시간적 지역성 */
/*
* __builtin_prefetch(addr, rw, locality)
* rw: 0 = 읽기, 1 = 쓰기
* locality: 0 = NTA(비시간적), 1 = T2, 2 = T1, 3 = T0(가장 가까운 캐시)
*/
| x86 명령어 | GCC locality | 동작 |
|---|---|---|
| PREFETCHT0 | 3 | L1 + L2 + L3로 프리페치 |
| PREFETCHT1 | 2 | L2 + L3로 프리페치 |
| PREFETCHT2 | 1 | L3로 프리페치 |
| PREFETCHNTA | 0 | 비시간적(Non-Temporal), 캐시 오염 최소화 |
커널 사용 사례
네트워크 스택에서 sk_buff의 다음 패킷을 미리 프리페치하여 캐시 미스를 줄이는 패턴:
/* net/core/dev.c — NAPI 폴링에서 프리페치 */
static void skb_defer_free_flush(struct softnet_data *sd)
{
struct sk_buff *skb, *next;
llist_for_each_entry_safe(skb, next, ...) {
prefetch(next); /* 다음 skb를 미리 캐시에 로드 */
__kfree_skb(skb);
}
}
perf stat으로 효과를 측정한 후 유지 여부를 결정하세요.
9. 캐시 파티셔닝 — Intel RDT
Intel RDT(Resource Director Technology)는 LLC와 메모리 대역폭을 태스크/컨테이너 단위로 파티셔닝하는 하드웨어 기능입니다.
CAT (Cache Allocation Technology)
CAT는 LLC(L3) 또는 L2를 CBM(Capacity Bitmask)으로 파티셔닝합니다. 각 비트가 캐시 웨이 그룹을 나타내며, CLOSID(Class of Service ID)별로 다른 CBM을 할당합니다.
# resctrl 마운트
mount -t resctrl resctrl /sys/fs/resctrl
# CBM 구조 확인 (11비트 = 11개 웨이 그룹)
cat /sys/fs/resctrl/info/L3/cbm_mask
# 7ff (0b11111111111)
# 실시간 태스크용 파티션 생성 (상위 4개 웨이 독점)
mkdir /sys/fs/resctrl/rt_group
echo "L3:0=f00" > /sys/fs/resctrl/rt_group/schemata
echo $RT_PID > /sys/fs/resctrl/rt_group/tasks
CDP (Code and Data Prioritization)
CDP는 CAT를 확장하여 코드(명령어)와 데이터에 별도의 CBM을 할당합니다. 코드가 큰 워크로드(JIT 컴파일러 등)에서 데이터 캐시 오염을 방지할 수 있습니다.
# CDP 활성화
mount -t resctrl resctrl /sys/fs/resctrl -o cdp
# 코드에 웨이 0-3, 데이터에 웨이 4-7 할당
echo "L3:0=00f;0=0f0" > /sys/fs/resctrl/jit_group/schemata
MBA (Memory Bandwidth Allocation)
MBA는 메모리 대역폭을 백분율로 제한합니다. noisy neighbor 문제를 완화하여 지연 민감 워크로드를 보호합니다.
resctrl 파일시스템
| 경로 | 용도 |
|---|---|
/sys/fs/resctrl/info/ | 하드웨어 RDT 기능 정보 (CBM 폭, CLOSID 수) |
/sys/fs/resctrl/schemata | 기본 그룹의 캐시/대역폭 할당 |
/sys/fs/resctrl/tasks | 기본 그룹 소속 PID 목록 |
/sys/fs/resctrl/<group>/ | 사용자 정의 CLOSID 그룹 |
/sys/fs/resctrl/mon_data/ | LLC 점유율 / 메모리 대역폭 모니터링(CMT/MBM) |
10. False Sharing
발생 메커니즘
두 코어가 같은 캐시 라인에 있는 서로 다른 변수를 독립적으로 수정하면, 코히런시 프로토콜이 캐시 라인 전체를 반복적으로 무효화합니다. 논리적으로 공유가 없지만 물리적으로 캐시 라인을 공유하여 성능이 극심하게 저하됩니다.
/* 문제: counter_a와 counter_b가 같은 캐시 라인에 위치 */
struct shared_counters {
atomic_t counter_a; /* CPU 0이 수정 */
atomic_t counter_b; /* CPU 1이 수정 → false sharing! */
};
/* 수정: 패딩으로 각 카운터를 별도 캐시 라인에 배치 */
struct shared_counters_fixed {
atomic_t counter_a;
char __pad[L1_CACHE_BYTES - sizeof(atomic_t)];
atomic_t counter_b;
} ____cacheline_aligned;
탐지
perf c2c는 false sharing을 탐지하는 가장 강력한 도구입니다:
# false sharing 프로파일링
perf c2c record -a -- sleep 10
perf c2c report --stdio
# 출력에서 HITM(Hit in Modified) 비율이 높은 캐시 라인 확인
# Shared Data Cache Line Table에서 문제 변수와 소스 위치 표시
완화
__cacheline_aligned_in_smp: SMP 빌드에서만 캐시 라인 정렬 (UP에서는 낭비 방지).- per-CPU 변수:
DEFINE_PER_CPU()로 코어마다 독립 복사본을 유지하면 공유 자체가 없음. - pahole: 구조체의 캐시 라인 레이아웃을 시각화하여 false sharing 후보를 탐지.
# pahole로 구조체 레이아웃 확인
pahole --class_name task_struct vmlinux | head -50
# 각 필드의 오프셋과 캐시 라인 경계를 표시
perf stat에서 L1-dcache-load-misses가 비정상적으로 높고 perf c2c에서 HITM이 집중되는 캐시 라인이 있다면 false sharing을 의심하세요.
11. 캐시 관리 명령어
CLFLUSH / CLFLUSHOPT
CLFLUSH는 지정된 주소의 캐시 라인을 모든 캐시 계층에서 무효화하고, 더티 라인이면 메모리에 기록합니다. CLFLUSHOPT는 CLFLUSH의 최적화 버전으로 순서 제약이 느슨하여 병렬 플러시가 가능합니다.
CLWB (Cache Line Write Back)
CLWB는 더티 캐시 라인을 메모리에 기록하되, 캐시에서 무효화하지 않습니다. Persistent Memory(PMEM)에서 데이터를 영속 매체에 기록하면서도 캐시 성능을 유지하는 데 핵심적입니다.
WBINVD / INVD
- WBINVD: CPU의 전체 캐시를 메모리에 기록 후 무효화. 매우 느리고 모든 코어에 영향. 리셋, 절전 진입 시 사용.
- INVD: 전체 캐시를 기록 없이 무효화. 데이터 손실 위험. BIOS/펌웨어 전용.
Non-Temporal 스토어
Non-Temporal 스토어(MOVNTI, MOVNTPS 등)는 캐시를 우회하여 메모리에 직접 기록합니다. 대량 데이터 복사 시 캐시 오염을 방지합니다.
| 명령어 | 동작 | 캐시 무효화 | 순서 보장 | 용도 |
|---|---|---|---|---|
| CLFLUSH | Write-back + Invalidate | O | 직렬화 | 일반 캐시 플러시 |
| CLFLUSHOPT | Write-back + Invalidate | O | 느슨 (SFENCE 필요) | 병렬 플러시 |
| CLWB | Write-back only | X (힌트) | 느슨 (SFENCE 필요) | PMEM 영속 |
| WBINVD | 전체 Write-back + Invalidate | O (전체) | 직렬화 | 리셋, S3 진입 |
| MOVNTI | WC 버퍼 경유 스토어 | 해당 없음 | 느슨 (SFENCE 필요) | 대량 복사 |
/* arch/x86/include/asm/special_insns.h */
static inline void clflush(volatile void *__p)
{
asm volatile("clflush %0" : "+m" (*(volatile char *)__p));
}
static inline void clflushopt(volatile void *__p)
{
alternative_io(".byte 0x3e; clflush %0",
".byte 0x66; clflush %0",
X86_FEATURE_CLFLUSHOPT,
"+m" (*(volatile char *)__p));
}
static inline void clwb(volatile void *__p)
{
volatile struct { char x[64]; } *__v = __p;
asm volatile(".byte 0x66, 0x0f, 0xae, 0x30"
: "+m" (*__v));
}
SFENCE를 실행하여 모든 쓰기가 메모리에 도달했음을 보장해야 합니다. PMEM 시나리오에서 이를 빠뜨리면 정전 시 데이터 손실이 발생합니다.
12. 커널 캐시 API
캐시 플러시 API
아키텍처 독립적인 캐시 관리 API:
/* include/asm-generic/cacheflush.h */
void flush_cache_all(void); /* 전체 캐시 플러시 */
void flush_cache_range(struct vm_area_struct *vma,
unsigned long start, unsigned long end);
void flush_cache_page(struct vm_area_struct *vma,
unsigned long addr, unsigned long pfn);
void flush_icache_range(unsigned long start, unsigned long end);
메모리 타입 변경 (PAT)
/* arch/x86/mm/pat/set_memory.c */
int set_memory_uc(unsigned long addr, int numpages); /* Uncacheable */
int set_memory_wc(unsigned long addr, int numpages); /* Write-Combining */
int set_memory_wb(unsigned long addr, int numpages); /* Write-Back (기본) */
int set_memory_wt(unsigned long addr, int numpages); /* Write-Through */
| API | PAT 엔트리 | 용도 |
|---|---|---|
set_memory_uc() | UC | 디바이스 레지스터 (MMIO) |
set_memory_wc() | WC | 프레임버퍼, GPU 메모리 |
set_memory_wt() | WT | 특수 코히런시 요구 |
set_memory_wb() | WB | 일반 메모리 (기본) |
ioremap_cache() | WB | 캐시 가능 I/O 영역 |
ioremap_wc() | WC | Write-Combining I/O 영역 |
kmap과 캐시 일관성
VIPT(Virtually-Indexed Physically-Tagged) 캐시를 사용하는 아키텍처(일부 ARM)에서는 같은 물리 페이지가 다른 가상 주소로 매핑될 때 캐시 앨리어싱(aliasing) 문제가 발생할 수 있습니다. kmap()/kunmap()은 이를 고려하여 일관된 매핑을 제공합니다.
DMA 캐시 동기화
DMA 전송 전후에 CPU 캐시와 디바이스 간 일관성을 보장해야 합니다:
/* DMA 방향별 캐시 동기화 */
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);
/* 디바이스→CPU 전송 후: 캐시를 무효화하여 새 데이터 읽기 */
dma_sync_single_for_device(dev, dma_handle, size, DMA_TO_DEVICE);
/* CPU→디바이스 전송 전: 캐시를 플러시하여 메모리에 기록 */
dma_alloc_coherent()로 할당된 메모리는 하드웨어 코히런시를 보장하므로 별도 동기화가 불필요합니다. 단, uncacheable로 매핑되어 CPU 접근이 느립니다. 빈번한 CPU 접근이 필요하면 streaming DMA(dma_map_single())와 명시적 동기화를 사용하세요.
13. 실전 진단
perf stat 캐시 이벤트
# L1/LLC 캐시 미스율 측정
perf stat -e cache-references,cache-misses,\
L1-dcache-loads,L1-dcache-load-misses,\
LLC-loads,LLC-load-misses,\
dTLB-loads,dTLB-load-misses \
-- ./workload
# 출력 예시:
# 1,234,567 cache-references
# 123,456 cache-misses # 10.00% of all cache refs
# 5,678,901 L1-dcache-loads
# 567,890 L1-dcache-load-misses # 10.00%
# 234,567 LLC-loads
# 23,456 LLC-load-misses # 10.00%
# 4,567,890 dTLB-loads
# 4,567 dTLB-load-misses # 0.10%
perf c2c
Cache-to-Cache 전송과 false sharing을 분석하는 전문 도구:
# 시스템 전체 C2C 프로파일링 (10초)
perf c2c record -a -- sleep 10
# 보고서 생성
perf c2c report --stdio
# 주요 확인 항목:
# 1) Shared Data Cache Line Table → HITM 비율이 높은 캐시 라인
# 2) 해당 캐시 라인을 접근하는 소스 코드 위치
# 3) Load/Store 비율과 접근 CPU 분포
Valgrind Cachegrind
# 캐시 시뮬레이션 기반 프로파일링 (유저 공간 프로그램)
valgrind --tool=cachegrind ./program
cg_annotate cachegrind.out.<pid>
# 함수별/라인별 캐시 미스 수를 상세히 보여줌
# I1/D1/LL(Last-Level) 미스를 각각 표시
sysfs 캐시 인터페이스
# CPU0의 캐시 정보 확인
for i in /sys/devices/system/cpu/cpu0/cache/index*/; do
echo "=== $(cat $i/level) $(cat $i/type) ==="
echo " size: $(cat $i/size)"
echo " ways: $(cat $i/ways_of_associativity)"
echo " sets: $(cat $i/number_of_sets)"
echo " line_size: $(cat $i/coherency_line_size)"
echo " shared_cpus: $(cat $i/shared_cpu_list)"
done
# 출력 예시:
# === 1 Data ===
# size: 48K
# ways: 12
# sets: 64
# line_size: 64
# shared_cpus: 0,8
# === 1 Instruction ===
# size: 32K
# === 2 Unified ===
# size: 1280K
# === 3 Unified ===
# size: 18432K
# shared_cpus: 0-7
hwloc 패키지의 lstopo 명령은 캐시 계층을 포함한 전체 CPU 토폴로지를 그래픽으로 시각화합니다. lstopo --of png > topology.png로 이미지를 생성하거나 lstopo-no-graphics로 텍스트 출력을 확인할 수 있습니다.