← Architecture 비교

Linux 6.18.37 LTS · Architecture comparison 06/21

Cache coherency와 memory barrier: DMB/DSB, x86 ordering, FENCE

compiler barrier, SMP barrier, DMA barrier와 instruction-cache synchronization을 서로 다른 계약으로 나눕니다.

비교 대상
arm64 / x86-64 / RISC-V
실제 원본
파일 3개 · 원문 238줄
기준 tag
Linux v6.18.37
분석 축
state · ordering · lifetime · latency

01 · QUESTION

무엇을 확인할 것인가

한 CPU의 store가 다른 CPU나 DMA device에 보인다는 말은 어느 observer와 어느 순서를 기준으로 하는가?

Linux memory model은 acquire/release와 full barrier API를 제공하지만 ISA의 기본 ordering, shareability domain, device memory와 instruction fetch coherence가 구현을 결정한다.

producer store, barrier, publication flag와 consumer acquire load를 한 litmus test로 읽는다. cache clean/invalidate는 ordering barrier와 같은 연산이 아니다.

지연 시간 관점barrier 비용은 outstanding memory transaction, domain과 microarchitecture에 따라 달라진다. DSB처럼 completion까지 기다리는 명령과 ordering만 강제하는 DMB를 분리한다.

02 · CONTRACT

공통 계약과 architecture 구현

architecture핵심 mechanism실패 형태확인할 상태
arm64DMB/DSB/ISB와 shareability·load/store optiondmb ish만으로 device MMIO completion이나 instruction-cache 갱신까지 됐다고 오해한다.barrier 종류, domain, cache line, memory attribute와 observer CPU/device를 기록한다.
x86-64TSO 기본 ordering과 LFENCE/SFENCE/MFENCE·locked opx86은 strong ordering이라는 이유로 DMA/MMIO와 speculation barrier를 모두 빈 연산으로 처리한다.memory type, locked op, fence, store buffer와 device posted-write completion을 확인한다.
RISC-VFENCE predecessor/successor set과 aq/rl bitdata fence 뒤 새 code가 실행될 것으로 가정하거나 I/O bit가 빠진 fence로 MMIO를 publish한다.FENCE pred/succ, aq/rl, I/O vs memory, hart mask와 icache sync를 기록한다.

03 · DIAGRAMS

세 그림으로 먼저 읽기

그림 1. 같은 목적, 서로 다른 mechanism각 ISA에서 실제로 추적할 state와 checkpoint를 한 줄에 맞췄습니다.

arm64

mechanism
DMB/DSB/ISB와 shareability·load/store option
state
weakly ordered ISA라 dmb ish, ishld, ishst가 SMP order를 표현한다. device/DMA에는 outer-shareable domain이 필요하고 code patch 뒤에는 D-cache clean, I-cache invalidate와 ISB가 이어진다.
checkpoint
barrier 종류, domain, cache line, memory attribute와 observer CPU/device를 기록한다.

x86-64

mechanism
TSO 기본 ordering과 LFENCE/SFENCE/MFENCE·locked op
state
WB memory의 store-store/load-load는 강하지만 store buffer, non-temporal store, WC memory와 speculation 때문에 barrier가 완전히 사라지지는 않는다. SMP full barrier는 locked instruction으로 구현될 수 있다.
checkpoint
memory type, locked op, fence, store buffer와 device posted-write completion을 확인한다.

RISC-V

mechanism
FENCE predecessor/successor set과 aq/rl bit
state
RVWMO는 fence rw,rw, r,r, w,w로 observer order를 지정한다. AMO의 aq/rl은 해당 atomic 전후 ordering을 포함하고 instruction fetch에는 별도 fence.i가 필요하다.
checkpoint
FENCE pred/succ, aq/rl, I/O vs memory, hart mask와 icache sync를 기록한다.
그림 2. 공통 kernel과 architecture hook의 소유권공통 정책이 hardware state를 직접 소유하지 않는 경계를 표시합니다.
Linux common contractLinux memory model은 acquire/release와 full barrier API를 제공하지만 ISA의 기본 ordering, shareability domain, device memory와 instruction fetch coherence가 구현을 결정한다.
arm64DMB/DSB/ISB와 shareability·load/store optionrelease/acquire instruction과 barrier scope가 observer domain을 포함해야 한다.
x86-64TSO 기본 ordering과 LFENCE/SFENCE/MFENCE·locked opcompiler reorder와 CPU reorder를 구분하고 MMIO/WC에는 별도 API를 사용한다.
RISC-VFENCE predecessor/successor set과 aq/rl bitremote hart의 instruction cache는 local fence.i만으로 갱신되지 않을 수 있어 IPI/SBI 기반 sync가 필요하다.
lifetime boundarylock-free object는 publication 전 완성되어야 하고 consumer가 reference를 놓기 전 free되면 안 된다. DMA buffer는 CPU/device ownership 전환과 cache maintenance가 맞아야 한다.
그림 3. publication과 관찰 순서state를 준비한 뒤 architecture ordering을 거쳐 관찰 가능한 checkpoint가 됩니다.
arm64state 준비release/acquire instruction과 barrier scope가 observer domain을 포함해야 한다.관찰: barrier 종류, domain, cache line, memory attribute와 observer CPU/device를 기록한다.
x86-64state 준비compiler reorder와 CPU reorder를 구분하고 MMIO/WC에는 별도 API를 사용한다.관찰: memory type, locked op, fence, store buffer와 device posted-write completion을 확인한다.
RISC-Vstate 준비remote hart의 instruction cache는 local fence.i만으로 갱신되지 않을 수 있어 IPI/SBI 기반 sync가 필요하다.관찰: FENCE pred/succ, aq/rl, I/O vs memory, hart mask와 icache sync를 기록한다.

04 · SOURCE

Linux 6.18.37 원본 코드와 주요 구문 해설

소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 원문 전체 발췌를 보존하고, 개별 해설은 근거가 있는 구문에만 붙였습니다.

arm64 · Linux 6.18.37

DMB/DSB/ISB와 shareability·load/store option

weakly ordered ISA라 dmb ish, ishld, ishst가 SMP order를 표현한다. device/DMA에는 outer-shareable domain이 필요하고 code patch 뒤에는 D-cache clean, I-cache invalidate와 ISB가 이어진다.

원본 코드: arch/arm64/include/asm/barrier.h:45-141

45						 ARM64_HAS_SB))
46 
47#define gsb_ack()	asm volatile(GSB_ACK_BARRIER_INSN : : : "memory")
48#define gsb_sys()	asm volatile(GSB_SYS_BARRIER_INSN : : : "memory")
49 
50#ifdef CONFIG_ARM64_PSEUDO_NMI
51#define pmr_sync()						\
52	do {							\
53		asm volatile(					\
54		ALTERNATIVE_CB("dsb sy",			\
55			       ARM64_HAS_GIC_PRIO_RELAXED_SYNC,	\
56			       alt_cb_patch_nops)		\
57		);						\
58	} while(0)
59#else
60#define pmr_sync()	do {} while (0)
61#endif
62 
63#define __mb()		dsb(sy)
64#define __rmb()		dsb(ld)
65#define __wmb()		dsb(st)
66 
67#define __dma_mb()	dmb(osh)
68#define __dma_rmb()	dmb(oshld)
69#define __dma_wmb()	dmb(oshst)
70 
71#define io_stop_wc()	dgh()
72 
73#define tsb_csync()								\
74	do {									\
75		/*								\
76		 * CPUs affected by Arm Erratum 2054223 or 2067961 needs	\
77		 * another TSB to ensure the trace is flushed. The barriers	\
78		 * don't have to be strictly back to back, as long as the	\
79		 * CPU is in trace prohibited state.				\
80		 */								\
81		if (cpus_have_final_cap(ARM64_WORKAROUND_TSB_FLUSH_FAILURE))	\
82			__tsb_csync();						\
83		__tsb_csync();							\
84	} while (0)
85 
86/*
87 * Generate a mask for array_index__nospec() that is ~0UL when 0 <= idx < sz
88 * and 0 otherwise.
89 */
90#define array_index_mask_nospec array_index_mask_nospec
91static inline unsigned long array_index_mask_nospec(unsigned long idx,
92						    unsigned long sz)
93{
94	unsigned long mask;
95 
96	asm volatile(
97	"	cmp	%1, %2\n"
98	"	sbc	%0, xzr, xzr\n"
99	: "=r" (mask)
100	: "r" (idx), "Ir" (sz)
101	: "cc");
102 
103	csdb();
104	return mask;
105}
106 
107/*
108 * Ensure that reads of the counter are treated the same as memory reads
109 * for the purposes of ordering by subsequent memory barriers.
110 *
111 * This insanity brought to you by speculative system register reads,
112 * out-of-order memory accesses, sequence locks and Thomas Gleixner.
113 *
114 * https://lore.kernel.org/r/[email protected]/
115 */
116#define arch_counter_enforce_ordering(val) do {				\
117	u64 tmp, _val = (val);						\
118									\
119	asm volatile(							\
120	"	eor	%0, %1, %1\n"					\
121	"	add	%0, sp, %0\n"					\
122	"	ldr	xzr, [%0]"					\
123	: "=r" (tmp) : "r" (_val));					\
124} while (0)
125 
126#define __smp_mb()	dmb(ish)
127#define __smp_rmb()	dmb(ishld)
128#define __smp_wmb()	dmb(ishst)
129 
130#define __smp_store_release(p, v)					\
131do {									\
132	typeof(p) __p = (p);						\
133	union { __unqual_scalar_typeof(*p) __val; char __c[1]; } __u =	\
134		{ .__val = (__force __unqual_scalar_typeof(*p)) (v) };	\
135	compiletime_assert_atomic_type(*p);				\
136	kasan_check_write(__p, sizeof(*p));				\
137	switch (sizeof(*p)) {						\
138	case 1:								\
139		asm volatile ("stlrb %w1, %0"				\
140				: "=Q" (*__p)				\
141				: "rZ" (*(__u8 *)__u.__c)		\

주요 구문 해설

원본 97줄은 그대로 표시하며, 검토한 64개 구문에 설명을 붙였습니다.

L45 ARM64_HAS_SB))

대체 명령을 선택할 기능 번호 ARM64_HAS_SB를 지정합니다. SB 지원 CPU에서는 전용 추측 실행 장벽을 사용하고 미지원 CPU에서는 앞의 DSB·ISB 조합을 유지합니다.

L47#define gsb_ack() asm volatile(GSB_ACK_BARRIER_INSN : : : "memory")

GICv5의 GSB ACK 명령을 내보냅니다. 인터럽트 확인·응답에 사용하는 GICR 명령의 효과가 이후 동작보다 앞서도록 동기화하는 용도입니다. 일반 RAM의 읽기·쓰기를 정렬하는 DMB와 목적이 다르며 memory clobber는 컴파일러의 메모리 접근 재배치도 막습니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L48#define gsb_sys() asm volatile(GSB_SYS_BARRIER_INSN : : : "memory")

GICv5의 GSB SYS 명령을 내보냅니다. ACK보다 넓게 앞선 GIC 시스템 명령의 인터럽트 관련 효과를 이후 동작과 동기화합니다. 명령 이름을 모르는 어셈블러에서도 지정한 인코딩을 낼 수 있도록 GSB_SYS_BARRIER_INSN을 사용합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L50#ifdef CONFIG_ARM64_PSEUDO_NMI

pseudo-NMI는 GIC의 우선순위 마스크를 이용해 일반 IRQ와 더 높은 우선순위 요청을 구분합니다. 켜지면 PMR 변경의 동기화를 위해 DSB를 포함하되 CPU가 완화된 동기화를 지원하면 대체 패치로 제거합니다. 꺼지면 pmr_sync는 빈 매크로입니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L51#define pmr_sync() \

pseudo-NMI를 사용하는 구성에서 GIC 우선순위 마스크 PMR 변경을 동기화하는 매크로입니다. 기본적으로 DSB SY를 실행하지만 완화된 PMR 동기화를 지원하는 CPU에서는 alternatives가 이를 NOP으로 바꾸어 불필요한 대기를 줄입니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L52 do { \

PMR 동기화의 여러 명령을 하나의 C 문장처럼 쓸 수 있도록 do 블록을 시작합니다. 끝의 while(0)과 짝이므로 반복 실행을 위한 루프가 아닙니다.

L53 asm volatile( \

PMR 우선순위 마스크 변경의 동기화 명령을 인라인 어셈블리로 넣기 시작합니다. CPU 기능에 따라 기본 DSB를 생략할 수 있도록 다음 alternatives를 사용합니다.

L54 ALTERNATIVE_CB("dsb sy", \

기본 명령을 DSB SY로 두고 기능별 패치 콜백을 등록합니다. PMR 변경이 인터럽트 인식에 반영되는 데 강한 동기화가 필요한 CPU의 경로입니다.

L55 ARM64_HAS_GIC_PRIO_RELAXED_SYNC, \

GIC 우선순위 변경에 완화된 동기화가 가능한 CPU 기능 비트를 패치 조건으로 지정합니다. 이 기능이 참일 때 아래 콜백으로 기본 DSB를 제거할 수 있습니다.

L56 alt_cb_patch_nops) \

기능 조건이 맞으면 기본 명령 구간을 NOP으로 바꿀 패치 콜백을 지정합니다. 불필요한 DSB 비용을 없애되 해당 하드웨어에서만 적용합니다.

L57 ); \

PMR 동기화의 volatile 어셈블리 문장을 마칩니다. 이 줄 자체에는 새 동기화 명령이 없으며 앞에서 지정한 대체 명령이 삽입됩니다.

L58 } while(0)

pmr_sync 매크로의 do 문장을 닫습니다. 조건이 0이므로 반복하지 않으며, 앞의 PMR 변경 동기화 명령들을 호출 지점에서 하나의 C 문장처럼 사용할 수 있게 합니다.

L59#else

pseudo-NMI를 빌드하지 않으므로 GIC PMR 변경을 위한 pmr_sync를 빈 매크로로 둡니다. 일반 메모리 배리어 mb/rmb/wmb까지 없애는 것은 아닙니다.

L60#define pmr_sync() do {} while (0)

CONFIG_ARM64_PSEUDO_NMI가 꺼진 구성의 빈 구현입니다. 이 PMR 기반 인터럽트 마스킹 경로를 사용하지 않으므로 전용 동기화 명령도 필요하지 않습니다. do-while(0)은 호출 위치에서 문장 하나로 사용할 형태만 유지합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L61#endif

50행에서 시작한 선택 구간을 마칩니다. pseudo-NMI는 GIC의 우선순위 마스크를 이용해 일반 IRQ와 더 높은 우선순위 요청을 구분합니다. 켜지면 PMR 변경의 동기화를 위해 DSB를 포함하되 CPU가 완화된 동기화를 지원하면 대체 패치로 제거합니다. 꺼지면 pmr_sync는 빈 매크로입니다.

L63#define __mb() dsb(sy)

DSB SY로 앞선 메모리 접근의 완료와 뒤의 진행을 시스템 범위에서 동기화하는 전체 장벽을 정의합니다. 읽기와 쓰기를 모두 다루며, 단순히 컴파일러에게 순서만 지시하는 빈 장벽보다 강한 하드웨어 동작입니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L64#define __rmb() dsb(ld)

DSB LD를 사용하는 읽기 장벽입니다. 앞선 읽기가 완료된 뒤 이후 메모리 접근이 진행하도록 동기화하므로, 읽은 상태를 바탕으로 다음 접근을 수행할 때 사용합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L65#define __wmb() dsb(st)

DSB ST를 사용하는 쓰기 장벽입니다. 앞선 쓰기의 완료와 이후 쓰기의 순서를 맞추며, 읽기까지 모두 묶는 전체 장벽과는 보장 범위가 다릅니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L67#define __dma_mb() dmb(osh)

DMA용 전체 장벽을 DMB OSH로 구현합니다. outer-shareable 영역의 관찰자와 공유하는 메모리에서 앞뒤 읽기·쓰기 순서를 맞추는 용도이며, CPU 캐시를 비우거나 DMA 작업 완료를 기다리는 함수는 아닙니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L68#define __dma_rmb() dmb(oshld)

DMA가 사용하는 공유 메모리의 읽기 순서를 DMB OSHLD로 맞춥니다. 장치의 완료 상태를 관찰한 뒤 관련 자료를 읽는 등의 순서에 쓰며, 별도의 캐시 유지보수가 필요한 메모리라면 그 작업까지 대신하지는 않습니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L69#define __dma_wmb() dmb(oshst)

outer-shareable device observer에 대한 DMA store ordering을 나타낸다.

L71#define io_stop_wc() dgh()

write-combining 구간의 접근을 앞뒤 구간과 합치지 않게 하는 DGH 힌트를 사용합니다. Normal-NC 또는 Device-GRE 접근의 병합 경계를 만드는 용도이며 일반적인 전체 메모리 장벽과 구분합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L73#define tsb_csync() \

추적 정보를 내보내는 Trace Synchronization Barrier를 실행합니다. Arm erratum 2054223·2067961 대상 CPU에는 TSB를 한 번 더 실행하고, 나머지는 한 번 실행하여 추적 데이터가 남는 문제를 피합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L74 do { \

trace 동기화 명령과 CPU 오류 우회를 한 문장으로 묶는 do 블록입니다. 끝의 while(0) 때문에 호출당 본문을 한 번 수행합니다.

L81 if (cpus_have_final_cap(ARM64_WORKAROUND_TSB_FLUSH_FAILURE)) \

trace flush 실패 오류가 있는 CPU인지 최종 기능 비트로 검사합니다. 해당 CPU이면 보통 한 번인 TSB를 한 번 더 실행해야 합니다.

L82 __tsb_csync(); \

Arm 오류 2054223 또는 2067961의 우회를 위해 TSB CSYNC를 추가 실행합니다. 이어지는 공통 TSB와 합쳐 trace가 비워지는 것을 보장합니다.

L83 __tsb_csync(); \

모든 경로에서 필요한 기본 TSB CSYNC를 실행합니다. 오류 우회 CPU는 위에서 이미 한 번 더 실행한 상태입니다.

L84 } while (0)

tsb_csync 매크로의 한 문장 범위를 닫습니다. CPU 오류 우회에 필요한 추가 trace 동기화와 기본 동기화를 묶는 문법이며 이 줄에서 별도 함수를 호출하지 않습니다.

L90#define array_index_mask_nospec array_index_mask_nospec

arm64 전용 array_index_mask_nospec 함수가 있음을 공통 nospec 헤더에 알립니다. 실제 함수는 인덱스가 범위 안이면 모든 비트가 1인 마스크, 밖이면 0을 만들고 CSDB를 사용하므로 추측 실행에서도 범위 밖 인덱스를 그대로 쓰지 않게 합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L91static inline unsigned long array_index_mask_nospec(unsigned long idx,

배열 인덱스 idx가 유효하면 모든 비트가 1, 아니면 0인 마스크를 만드는 함수입니다. 분기 예측이 틀려도 마스크를 통해 범위 밖 접근을 제한하는 데 씁니다.

L92 unsigned long sz)

배열 원소 개수 sz를 두 번째 인자로 받습니다. 주소 바이트 크기 대신 인덱스와 비교할 같은 단위의 상한입니다.

L94 unsigned long mask;

비교 결과를 0 또는 모든 비트 1로 표현할 unsigned long 변수를 준비합니다. 아래 SBC의 출력이 이 값에 들어갑니다.

L96 asm volatile(

CMP와 SBC를 연속 배치하는 어셈블리를 시작합니다. 조건 코드로부터 마스크를 직접 만들어 조건 분기를 사용하지 않습니다.

L97 " cmp %1, %2\n"

idx와 sz를 unsigned 비교할 수 있도록 뺄셈 조건 코드를 만듭니다. idx가 sz보다 작으면 borrow 관계가 다음 SBC의 마스크 계산에 반영됩니다.

L98 " sbc %0, xzr, xzr\n"

0에서 0과 carry의 반전을 빼 마스크를 만듭니다. idx<sz이면 -1 즉 모든 비트 1, 그 외에는 0이 됩니다.

L99 : "=r" (mask)

계산한 마스크를 일반 레지스터 출력으로 받아 C 변수 mask에 연결합니다. =는 이전 값이 입력으로 필요 없는 출력이라는 뜻입니다.

L100 : "r" (idx), "Ir" (sz)

idx는 레지스터로, sz는 허용되는 즉시값 또는 레지스터로 전달합니다. 입력 피연산자 번호 %1과 %2에 해당합니다.

L101 : "cc");

CMP·SBC가 조건 코드 플래그를 바꾼다는 사실을 cc clobber로 알립니다. 컴파일러가 이 구간 이전 플래그를 이후에도 그대로 사용할 수 없게 합니다.

L103 csdb();

비교 결과로 만든 배열 인덱스 마스크가 이후 접근에 실제로 반영되도록 추측 실행을 제한합니다. 범위 밖 인덱스가 추측 실행 중 먼저 사용되는 것을 막는 array_index_mask_nospec의 마지막 단계입니다.

L104 return mask;

범위 검사 결과 마스크를 호출자에게 반환합니다. 유효 인덱스는 비트 AND 후 유지되고 유효하지 않으면 0으로 제한하는 데 쓰입니다.

L116#define arch_counter_enforce_ordering(val) do { \

타이머 카운터를 읽은 값 val에 의존하는 가짜 스택 읽기를 만듭니다. EOR 결과는 0이지만 의존 관계를 남겨 카운터 읽기를 뒤의 메모리 장벽이 정렬할 수 있게 합니다. xzr로 읽기 결과를 버리므로 스택 자료를 변경하지 않습니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L117 u64 tmp, _val = (val); \

카운터 값을 _val에 한 번만 평가하여 보관하고 의존성 계산용 tmp를 준비합니다. 매크로 인자에 부수 효과가 있어도 중복 평가하지 않습니다.

L118 \

역슬래시만 있는 매크로 연속 줄입니다. 카운터 값 준비와 의존성 어셈블리를 시각적으로 나누며 실행 명령을 추가하지 않습니다.

L119 asm volatile( \

카운터 읽기를 뒤의 메모리 장벽과 연결할 데이터 의존성 어셈블리를 시작합니다. 시스템 레지스터 읽기를 메모리 읽기처럼 순서 제약에 참여시키기 위한 구성입니다.

L120 " eor %0, %1, %1\n" \

카운터 값을 자기 자신과 XOR하여 결과 0을 만듭니다. 수치상 0이어도 뒤의 주소 계산이 앞서 읽은 카운터 값에 의존하도록 명령 연결을 만듭니다.

L121 " add %0, sp, %0\n" \

스택 포인터에 방금 만든 0을 더해 스택 주소를 구합니다. 실제 주소는 SP이지만 카운터 입력으로 이어지는 데이터 의존성을 갖게 합니다.

L122 " ldr xzr, [%0]" \

의존성이 있는 스택 주소에서 값을 읽고 xzr로 버립니다. 카운터 값을 얻은 뒤에야 완료될 수 있는 메모리 읽기를 만들어 이후 장벽이 순서를 잡도록 합니다.

L123 : "=r" (tmp) : "r" (_val)); \

주소 계산 결과를 tmp 출력, 카운터 값을 _val 입력으로 지정하고 어셈블리를 마칩니다. 읽은 스택 데이터 자체는 결과로 돌려주지 않습니다.

L124} while (0)

카운터 값에 의존하는 스택 읽기를 묶은 매크로의 끝입니다. 앞에서 만든 데이터 의존성으로 카운터 읽기와 뒤의 메모리 장벽 사이에 순서를 부여하며, while (0)은 실행을 반복하지 않습니다.

L126#define __smp_mb() dmb(ish)

inner-shareable CPU observer 사이의 read/write ordering을 full DMB로 강제한다.

L127#define __smp_rmb() dmb(ishld)

CPU들이 공유하는 inner-shareable 메모리에 DMB ISHLD를 적용합니다. 앞선 읽기와 이후 읽기·쓰기의 순서를 맞추는 SMP 읽기 장벽이며, 장치까지 포함하는 DMA 장벽과 범위가 다릅니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L128#define __smp_wmb() dmb(ishst)

DMB ISHST로 CPU 간에 공유하는 메모리의 앞선 쓰기가 뒤의 쓰기보다 먼저 관찰되도록 합니다. 읽기 순서까지 전부 보장하는 전체 장벽으로 해석하면 안 됩니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L130#define __smp_store_release(p, v) \

p가 가리키는 값을 release 의미로 저장합니다. 앞에서 준비한 데이터 접근이 이 공개용 저장 뒤로 넘어가지 않도록 하고, 값의 1·2·4·8바이트 폭에 맞춰 STLRB·STLRH·STLR을 고릅니다. 읽는 쪽의 acquire와 함께 자료 공개 순서를 만들 때 사용합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L131do { \

release 저장 매크로의 준비·타입 검사·저장을 하나의 C 문장으로 묶습니다. 끝의 while(0)과 짝을 이뤄 조건문 안에서도 안전하게 사용합니다.

L132 typeof(p) __p = (p); \

저장 대상 포인터 p를 원래 타입 그대로 __p에 한 번 평가해 보관합니다. 이후 검사와 저장에서 매크로 인자를 다시 평가하지 않도록 합니다.

L133 union { __unqual_scalar_typeof(*p) __val; char __c[1]; } __u = \

저장값을 원래 스칼라형과 바이트 배열 두 형태로 볼 수 있는 union을 선언합니다. 크기별 저장 명령에 맞는 폭으로 같은 값을 꺼내기 위한 준비입니다.

L134 { .__val = (__force __unqual_scalar_typeof(*p)) (v) }; \

입력 v를 대상 스칼라형으로 변환하여 union의 값 필드를 초기화합니다. __force는 sparse 타입 검사에 의도한 변환임을 표시합니다.

L135 compiletime_assert_atomic_type(*p); \

대상 타입이 원자적으로 접근할 수 있는 스칼라 크기인지 빌드 시 검사합니다. 지원되지 않는 복합 타입을 release 저장에 넣는 사용을 막습니다.

L136 kasan_check_write(__p, sizeof(*p)); \

KASAN이 켜졌으면 __p부터 대상 크기만큼 쓰기가 유효한지 검사합니다. 아래 인라인 어셈블리 저장은 일반 C 쓰기처럼 자동 계측되지 않으므로 명시적으로 호출합니다.

L137 switch (sizeof(*p)) { \

대상 크기에 따라 1·2·4·8바이트 release 저장 명령을 선택합니다. 데이터 폭을 맞춰 이웃 메모리를 덮어쓰지 않도록 합니다.

L138 case 1: \

1바이트 대상의 처리 분기입니다. 이어 STLRB 명령으로 바이트 release 저장을 수행합니다.

L139 asm volatile ("stlrb %w1, %0" \

STLRB로 값의 하위 한 바이트를 대상에 release 저장합니다. 앞선 메모리 접근들이 이 저장 뒤로 넘어가지 않도록 하드웨어 순서를 부여합니다.

L140 : "=Q" (*__p) \

저장 대상을 단일 주소 레지스터로 참조하는 메모리 출력 =Q로 지정합니다. STL 계열 명령이 요구하는 메모리 피연산자 형태를 컴파일러에 알립니다.

L141 : "rZ" (*(__u8 *)__u.__c) \

union에서 꺼낸 8비트 값을 레지스터 또는 0 상수로 넘깁니다. rZ 제약으로 0이면 zero register를 사용할 수 있게 합니다.

x86-64 · Linux 6.18.37

TSO 기본 ordering과 LFENCE/SFENCE/MFENCE·locked op

WB memory의 store-store/load-load는 강하지만 store buffer, non-temporal store, WC memory와 speculation 때문에 barrier가 완전히 사라지지는 않는다. SMP full barrier는 locked instruction으로 구현될 수 있다.

원본 코드: arch/x86/include/asm/barrier.h:9-84

9 * Force strict CPU ordering.
10 * And yes, this might be required on UP too when we're talking
11 * to devices.
12 */
13 
14#ifdef CONFIG_X86_32
15#define mb() asm volatile(ALTERNATIVE("lock addl $0,-4(%%esp)", "mfence", \
16				      X86_FEATURE_XMM2) ::: "memory", "cc")
17#define rmb() asm volatile(ALTERNATIVE("lock addl $0,-4(%%esp)", "lfence", \
18				       X86_FEATURE_XMM2) ::: "memory", "cc")
19#define wmb() asm volatile(ALTERNATIVE("lock addl $0,-4(%%esp)", "sfence", \
20				       X86_FEATURE_XMM2) ::: "memory", "cc")
21#else
22#define __mb()	asm volatile("mfence":::"memory")
23#define __rmb()	asm volatile("lfence":::"memory")
24#define __wmb()	asm volatile("sfence" ::: "memory")
25#endif
26 
27/**
28 * array_index_mask_nospec() - generate a mask that is ~0UL when the
29 * 	bounds check succeeds and 0 otherwise
30 * @index: array element index
31 * @size: number of elements in array
32 *
33 * Returns:
34 *     0 - (index < size)
35 */
36#define array_index_mask_nospec(idx,sz) ({	\
37	typeof((idx)+(sz)) __idx = (idx);	\
38	typeof(__idx) __sz = (sz);		\
39	unsigned long __mask;			\
40	asm volatile ("cmp %1,%2; sbb %0,%0"	\
41			:"=r" (__mask)		\
42			:ASM_INPUT_G (__sz),	\
43			 "r" (__idx)		\
44			:"cc");			\
45	__mask; })
46 
47/* Prevent speculative execution past this barrier. */
48#define barrier_nospec() alternative("", "lfence", X86_FEATURE_LFENCE_RDTSC)
49 
50#define __dma_rmb()	barrier()
51#define __dma_wmb()	barrier()
52 
53#define __smp_mb()	asm volatile("lock addl $0,-4(%%" _ASM_SP ")" ::: "memory", "cc")
54 
55#define __smp_rmb()	dma_rmb()
56#define __smp_wmb()	barrier()
57#define __smp_store_mb(var, value) do { (void)xchg(&var, value); } while (0)
58 
59#define __smp_store_release(p, v)					\
60do {									\
61	compiletime_assert_atomic_type(*p);				\
62	barrier();							\
63	WRITE_ONCE(*p, v);						\
64} while (0)
65 
66#define __smp_load_acquire(p)						\
67({									\
68	typeof(*p) ___p1 = READ_ONCE(*p);				\
69	compiletime_assert_atomic_type(*p);				\
70	barrier();							\
71	___p1;								\
72})
73 
74/* Atomic operations are already serializing on x86 */
75#define __smp_mb__before_atomic()	do { } while (0)
76#define __smp_mb__after_atomic()	do { } while (0)
77 
78/* Writing to CR3 provides a full memory barrier in switch_mm(). */
79#define smp_mb__after_switch_mm()	do { } while (0)
80 
81#include <asm-generic/barrier.h>
82 
83#endif /* _ASM_X86_BARRIER_H */
84 

주요 구문 해설

원본 76줄은 그대로 표시하며, 검토한 47개 구문에 설명을 붙였습니다.

L14#ifdef CONFIG_X86_32

32비트 x86에서는 SSE2가 없는 CPU도 지원할 수 있으므로 LOCK 연산을 기본 배리어로 두고 SSE2 지원 시 MFENCE·LFENCE·SFENCE로 교체합니다. 64비트 빌드는 아래에서 해당 fence 명령을 직접 사용합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L15#define mb() asm volatile(ALTERNATIVE("lock addl $0,-4(%%esp)", "mfence", \

32비트 x86의 전체 메모리 장벽입니다. SSE2 기능이 있으면 MFENCE로 패치하고, 없으면 스택 위치에 0을 원자적으로 더하는 LOCK 연산의 순서 보장을 사용합니다. 더하는 값이 0이라 그 위치의 데이터 값은 바뀌지 않습니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L16 X86_FEATURE_XMM2) ::: "memory", "cc")

32비트 x86의 완전 메모리 장벽에서 SSE2(XMM2) 지원을 대체 조건으로 지정합니다. 지원하면 MFENCE, 없으면 스택에 대한 locked 연산을 쓰며 memory·cc 변경도 컴파일러에 알립니다.

L17#define rmb() asm volatile(ALTERNATIVE("lock addl $0,-4(%%esp)", "lfence", \

32비트 x86의 읽기 장벽입니다. SSE2가 있으면 LFENCE를 사용하고 없으면 LOCK ADD의 더 강한 순서 보장으로 대신합니다. ALTERNATIVE는 매 호출마다 C 조건문으로 CPU 종류를 검사하는 방식이 아닙니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L18 X86_FEATURE_XMM2) ::: "memory", "cc")

32비트 읽기 장벽의 기능 조건과 clobber를 지정합니다. SSE2가 있으면 LFENCE를 쓰고 없으면 더 강한 locked 연산으로 읽기 순서를 보장합니다.

L19#define wmb() asm volatile(ALTERNATIVE("lock addl $0,-4(%%esp)", "sfence", \

32비트 x86의 쓰기 장벽입니다. SSE2 지원 CPU에서는 SFENCE로 패치하며, 지원하지 않으면 LOCK ADD를 사용해 앞뒤 저장의 순서를 맞춥니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L20 X86_FEATURE_XMM2) ::: "memory", "cc")

32비트 쓰기 장벽에서 SSE2 여부에 따라 SFENCE 또는 locked 연산을 선택합니다. 컴파일러에도 메모리·조건 코드 영향이 있다고 선언합니다.

L21#else

64비트 x86에서는 MFENCE·LFENCE·SFENCE를 각각 전체·읽기·쓰기 배리어로 직접 정의합니다. 32비트 구형 CPU를 위한 LOCK 연산 대안이 필요 없는 구성입니다.

L22#define __mb() asm volatile("mfence":::"memory")

64비트 x86의 전체 장벽을 MFENCE로 정의합니다. 앞선 읽기·쓰기와 뒤의 읽기·쓰기 순서를 하드웨어에서 맞추며 memory clobber로 컴파일러의 재배치도 막습니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L23#define __rmb() asm volatile("lfence":::"memory")

64비트 x86의 읽기 장벽을 LFENCE로 구현합니다. 앞선 읽기와 이후 실행 사이에 필요한 순서를 만들며 memory clobber로 컴파일러에게도 접근 경계를 알립니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L24#define __wmb() asm volatile("sfence" ::: "memory")

64비트 x86의 쓰기 장벽을 SFENCE로 구현합니다. 앞선 저장과 뒤의 저장 순서를 맞추며, 모든 읽기까지 묶는 MFENCE와는 보장 범위가 다릅니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L25#endif

14행에서 시작한 선택 구간을 마칩니다. 32비트 x86에서는 SSE2가 없는 CPU도 지원할 수 있으므로 LOCK 연산을 기본 배리어로 두고 SSE2 지원 시 MFENCE·LFENCE·SFENCE로 교체합니다. 64비트 빌드는 아래에서 해당 fence 명령을 직접 사용합니다.

L36#define array_index_mask_nospec(idx,sz) ({ \

CMP와 SBB로 인덱스 범위를 검사한 결과를 비트 마스크로 만듭니다. unsigned 비교에서 idx가 sz보다 작으면 모든 비트가 1, 아니면 0이므로 이 마스크를 인덱스에 AND하여 범위 밖 값을 0으로 제한할 수 있습니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L37 typeof((idx)+(sz)) __idx = (idx); \

idx와 sz를 더했을 때의 공통 정수형으로 인덱스를 한 번 평가합니다. 두 피연산자의 폭을 맞춰 뒤 비교를 수행할 준비입니다.

L38 typeof(__idx) __sz = (sz); \

인덱스와 같은 타입으로 배열 상한 sz를 보관합니다. 비교의 피연산자 크기를 통일하며 매크로 인자를 한 번만 평가합니다.

L39 unsigned long __mask; \

유효 인덱스에 대해 모든 비트 1, 그 외에 0이 될 마스크 변수를 선언합니다. 뒤의 SBB 결과를 받습니다.

L40 asm volatile ("cmp %1,%2; sbb %0,%0" \

CMP로 idx와 sz를 비교한 뒤 SBB로 carry를 0 또는 -1 마스크로 바꿉니다. idx<sz인 unsigned 비교 결과를 조건 분기 없이 값으로 만듭니다.

L41 :"=r" (__mask) \

마스크 계산 결과를 레지스터 출력으로 받아 __mask에 저장합니다. 기존 __mask 값은 입력으로 사용하지 않습니다.

L42 :ASM_INPUT_G (__sz), \

상한 __sz를 아키텍처가 허용하는 일반 입력 제약으로 전달합니다. CMP의 첫 번째 입력 피연산자 자리입니다.

L43 "r" (__idx) \

인덱스 __idx를 레지스터 입력으로 넘깁니다. AT&T 문법의 CMP에서 앞의 크기와 비교하여 unsigned 범위 판정용 carry를 만듭니다.

L44 :"cc"); \

CMP와 SBB가 조건 플래그를 바꾼다고 컴파일러에 알립니다. 이후 코드가 이전 플래그를 재사용하지 않게 합니다.

L45 __mask; })

GNU statement expression의 최종 값으로 __mask를 내놓습니다. 매크로 전체가 인덱스 제한에 사용할 마스크 표현식으로 평가됩니다.

L48#define barrier_nospec() alternative("", "lfence", X86_FEATURE_LFENCE_RDTSC)

일반 data ordering이 아니라 speculative execution을 제한하는 alternative다.

L50#define __dma_rmb() barrier()

x86의 DMA 공유 메모리 읽기 장벽은 컴파일러 장벽으로 구현합니다. 이 메모리 접근에 필요한 CPU 쪽 순서는 아키텍처가 제공하므로 추가 fence 명령을 넣지 않지만, 컴파일러가 앞뒤 읽기를 재배치하지는 못하게 합니다. 캐시 유지보수나 MMIO 읽기 완료를 대신하지 않습니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L51#define __dma_wmb() barrier()

x86의 DMA 공유 메모리 쓰기 순서는 CPU가 제공하므로 컴파일러의 저장 재배치만 barrier로 막습니다. 추가 CPU 명령이 없다는 뜻이지 호출 위치의 순서 요구가 사라진다는 뜻은 아닙니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L53#define __smp_mb() asm volatile("lock addl $0,-4(%%" _ASM_SP ")" ::: "memory", "cc")

locked RMW가 full memory ordering point를 제공한다.

L55#define __smp_rmb() dma_rmb()

SMP 읽기 장벽을 dma_rmb에 연결합니다. x86에서는 필요한 읽기 순서가 하드웨어에 이미 있고 dma_rmb가 컴파일러 재배치를 막으므로 별도 LFENCE를 추가하지 않는 경로입니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L56#define __smp_wmb() barrier()

CPU 간 공유 메모리의 쓰기 순서를 지키기 위해 컴파일러 장벽을 둡니다. x86의 일반 메모리 저장 순서 보장을 이용하므로 이 매크로 자체는 별도 CPU fence를 실행하지 않습니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L57#define __smp_store_mb(var, value) do { (void)xchg(&var, value); } while (0)

var에 value를 저장하면서 전체 장벽 효과를 얻도록 메모리 XCHG를 사용합니다. XCHG가 돌려주는 이전 값은 void로 버리고, 새 값 저장과 원자적 교환의 순서 보장만 사용합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L59#define __smp_store_release(p, v) \

저장 전에 컴파일러 장벽을 두고 WRITE_ONCE로 값을 한 번 기록하여 release 저장을 구현합니다. x86의 메모리 순서 보장을 이용하므로 STLR 같은 별도 명령이 필요하지 않으며, 공유 자료를 공개하는 저장 뒤로 앞선 접근이 이동하지 않게 합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L60do { \

release 저장의 타입 검사·컴파일러 장벽·실제 쓰기를 한 문장으로 묶습니다. do/while(0) 관용구의 시작이며 재시도 루프가 아닙니다.

L61 compiletime_assert_atomic_type(*p); \

대상 타입이 원자적으로 접근 가능한 스칼라 타입인지 컴파일 시 검사합니다. 잘못된 크기나 복합 자료형을 단일 release 저장으로 오인하지 않게 합니다.

L62 barrier(); \

컴파일러가 앞선 메모리 접근을 아래의 저장 뒤로 옮기지 못하게 합니다. x86의 일반 메모리 순서와 조합하여 release 저장을 구현하므로 이 줄은 별도의 CPU fence를 발행하지 않습니다.

L63 WRITE_ONCE(*p, v); \

대상 *p에 v를 한 번의 C 관측 가능한 쓰기로 기록합니다. 컴파일러의 쓰기 병합·제거를 제한하고 앞 장벽과 함께 release 공개 지점이 됩니다.

L64} while (0)

release 저장을 한 문장으로 묶는 매크로의 끝입니다. 앞의 컴파일러 장벽과 WRITE_ONCE가 이전 접근을 이 저장 뒤로 옮기지 못하게 하며, while (0)은 반복문으로 동작하지 않습니다.

L66#define __smp_load_acquire(p) \

READ_ONCE로 값을 한 번 읽은 뒤 컴파일러 장벽을 두어 acquire 읽기를 구현합니다. 이후 접근이 이 읽기 앞으로 옮겨지지 않도록 하고 읽은 값을 반환하며, 하드웨어 순서는 x86의 보장을 이용합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L67({ \

값을 반환하는 GNU statement expression으로 acquire 읽기를 시작합니다. 내부에 선언과 장벽을 넣으면서 매크로 전체를 하나의 값으로 사용할 수 있게 합니다.

L68 typeof(*p) ___p1 = READ_ONCE(*p); \

*p를 READ_ONCE로 읽어 같은 타입의 임시값에 보관합니다. 뒤의 장벽 이후에도 원래 읽은 값 하나를 반환하도록 합니다.

L69 compiletime_assert_atomic_type(*p); \

읽기 대상 타입이 원자적으로 접근 가능한 크기인지 빌드 시 검사합니다. 분할된 여러 읽기를 단일 acquire 읽기로 취급하지 않게 합니다.

L70 barrier(); \

이후 메모리 접근을 앞의 READ_ONCE보다 앞으로 옮기지 못하게 하는 컴파일러 장벽입니다. x86 하드웨어의 일반 읽기 순서와 함께 acquire 의미를 제공합니다.

L71 ___p1; \

앞서 읽은 임시값을 statement expression의 결과로 선택합니다. 장벽 뒤에 대상 메모리를 다시 읽지 않습니다.

L72})

acquire 읽기의 statement expression을 마칩니다. 매크로를 사용한 식에는 앞줄의 ___p1 값이 반환됩니다.

L75#define __smp_mb__before_atomic() do { } while (0)

원자적 read-modify-write 앞에 추가 장벽을 넣지 않는 x86 구현입니다. 이 장벽과 짝지어 쓰는 원자 연산이 이미 필요한 순서를 보장하기 때문입니다. 일반 atomic_read나 atomic_set까지 언제나 전체 장벽이라는 뜻은 아닙니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L76#define __smp_mb__after_atomic() do { } while (0)

원자적 read-modify-write 뒤에도 별도 장벽을 추가하지 않습니다. 해당 x86 원자 연산의 기존 순서 보장을 재사용하는 빈 구현이며, 소스의 호출을 삭제해도 다른 아키텍처에서 같은 보장이 유지된다는 뜻은 아닙니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L79#define smp_mb__after_switch_mm() do { } while (0)

switch_mm의 주소 공간 전환 뒤에 추가 장벽을 넣지 않습니다. x86의 해당 전환 경로에서 CR3를 쓰는 동작이 필요한 전체 메모리 장벽을 제공하므로 같은 목적의 fence를 중복 실행하지 않습니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L81#include <asm-generic/barrier.h>

x86가 직접 정의하지 않은 공통 배리어 조합을 가져옵니다. 위에서 정한 MFENCE·LFENCE·SFENCE와 컴파일러 순서 제어를 바탕으로 공통 커널 코드가 같은 barrier API를 사용할 수 있게 합니다. 전처리 단계에서 헤더 내용을 포함하며, CPU가 실행 중 헤더를 호출하는 동작은 없습니다.

L83#endif /* _ASM_X86_BARRIER_H */

2행에서 시작한 선택 구간을 마칩니다. 이 헤더의 배리어 정의가 한 번만 처리되도록 하는 포함 보호 조건입니다. CPU 기능이나 실행 중의 분기를 선택하는 Kconfig 설정과는 다릅니다.

RISC-V · Linux 6.18.37

FENCE predecessor/successor set과 aq/rl bit

RVWMO는 fence rw,rw, r,r, w,w로 observer order를 지정한다. AMO의 aq/rl은 해당 atomic 전후 ordering을 포함하고 instruction fetch에는 별도 fence.i가 필요하다.

원본 코드: arch/riscv/include/asm/barrier.h:12-76

12 
13#ifndef __ASSEMBLER__
14#include <asm/cmpxchg.h>
15#include <asm/fence.h>
16 
17/* These barriers need to enforce ordering on both devices or memory. */
18#define __mb()		RISCV_FENCE(iorw, iorw)
19#define __rmb()		RISCV_FENCE(ir, ir)
20#define __wmb()		RISCV_FENCE(ow, ow)
21 
22/* These barriers do not need to enforce ordering on devices, just memory. */
23#define __smp_mb()	RISCV_FENCE(rw, rw)
24#define __smp_rmb()	RISCV_FENCE(r, r)
25#define __smp_wmb()	RISCV_FENCE(w, w)
26 
27/*
28 * This is a very specific barrier: it's currently only used in two places in
29 * the kernel, both in the scheduler.  See include/linux/spinlock.h for the two
30 * orderings it guarantees, but the "critical section is RCsc" guarantee
31 * mandates a barrier on RISC-V.  The sequence looks like:
32 *
33 *    lr.aq lock
34 *    sc    lock <= LOCKED
35 *    smp_mb__after_spinlock()
36 *    // critical section
37 *    lr    lock
38 *    sc.rl lock <= UNLOCKED
39 *
40 * The AQ/RL pair provides a RCpc critical section, but there's not really any
41 * way we can take advantage of that here because the ordering is only enforced
42 * on that one lock.  Thus, we're just doing a full fence.
43 *
44 * Since we allow writeX to be called from preemptive regions we need at least
45 * an "o" in the predecessor set to ensure device writes are visible before the
46 * task is marked as available for scheduling on a new hart.  While I don't see
47 * any concrete reason we need a full IO fence, it seems safer to just upgrade
48 * this in order to avoid any IO crossing a scheduling boundary.  In both
49 * instances the scheduler pairs this with an mb(), so nothing is necessary on
50 * the new hart.
51 */
52#define smp_mb__after_spinlock()	RISCV_FENCE(iorw, iorw)
53 
54#define __smp_store_release(p, v)					\
55do {									\
56	compiletime_assert_atomic_type(*p);				\
57	RISCV_FENCE(rw, w);						\
58	WRITE_ONCE(*p, v);						\
59} while (0)
60 
61#define __smp_load_acquire(p)						\
62({									\
63	typeof(*p) ___p1 = READ_ONCE(*p);				\
64	compiletime_assert_atomic_type(*p);				\
65	RISCV_FENCE(r, rw);						\
66	___p1;								\
67})
68 
69#ifdef CONFIG_RISCV_ISA_ZAWRS
70#define smp_cond_load_relaxed(ptr, cond_expr) ({			\
71	typeof(ptr) __PTR = (ptr);					\
72	__unqual_scalar_typeof(*ptr) VAL;				\
73	for (;;) {							\
74		VAL = READ_ONCE(*__PTR);				\
75		if (cond_expr)						\
76			break;						\

주요 구문 해설

원본 65줄은 그대로 표시하며, 검토한 31개 구문에 설명을 붙였습니다.

L13#ifndef __ASSEMBLER__

C 컴파일에서만 사용할 inline 함수와 매크로 정의를 여는 조건입니다. 어셈블리에서 이 헤더를 포함할 때 C 형식 선언이 assembler 입력에 섞이지 않게 합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L14#include <asm/cmpxchg.h>

RISC-V의 compare-exchange 및 값 변화 대기 helper를 가져옵니다. 아래 조건부 메모리 대기가 __cmpwait_relaxed로 예약 상태의 변화를 기다릴 때 필요한 정의입니다. 전처리 단계에서 헤더 내용을 포함하며, CPU가 실행 중 헤더를 호출하는 동작은 없습니다.

L15#include <asm/fence.h>

RISC-V FENCE의 선행·후행 접근 종류를 지정하는 매크로를 가져옵니다. 아래 iorw·ir·ow 조합으로 장치 I/O와 일반 메모리의 읽기·쓰기 순서를 구분해 제어합니다. 전처리 단계에서 헤더 내용을 포함하며, CPU가 실행 중 헤더를 호출하는 동작은 없습니다.

L18#define __mb() RISCV_FENCE(iorw, iorw)

FENCE IORW,IORW로 장치 입력·출력과 일반 메모리 읽기·쓰기를 모두 정렬합니다. i/o는 장치, r/w는 메모리 접근 집합이며 앞선 집합의 효과가 뒤의 집합보다 먼저 관찰되도록 하는 전체 장벽입니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L19#define __rmb() RISCV_FENCE(ir, ir)

FENCE IR,IR로 장치 입력과 일반 메모리 읽기를 앞뒤로 정렬합니다. RISC-V에서는 장치 접근과 메모리 접근의 집합을 따로 표시하므로 읽기 장벽에 i와 r을 함께 넣습니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L20#define __wmb() RISCV_FENCE(ow, ow)

FENCE OW,OW로 장치 출력과 일반 메모리 쓰기의 앞뒤 순서를 맞춥니다. 장치 레지스터 쓰기까지 포함하려고 o를 넣으며 읽기는 이 장벽의 대상 집합에 포함하지 않습니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L23#define __smp_mb() RISCV_FENCE(rw, rw)

memory read/write 사이의 full RVWMO ordering을 만든다.

L24#define __smp_rmb() RISCV_FENCE(r, r)

SMP의 읽기 장벽은 장치 입력을 제외한 FENCE R,R입니다. CPU들이 공유하는 일반 메모리의 읽기 순서를 맞추는 데 필요한 범위만 선택합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L25#define __smp_wmb() RISCV_FENCE(w, w)

SMP의 쓰기 장벽은 FENCE W,W입니다. CPU 간 공유 메모리의 앞선 저장이 뒤의 저장보다 먼저 관찰되게 하며 장치 출력 순서까지 포함하는 OW,OW와 구분합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L52#define smp_mb__after_spinlock() RISCV_FENCE(iorw, iorw)

Linux lock ordering requirement를 맞추기 위해 I/O까지 포함한 강한 fence를 사용한다.

L54#define __smp_store_release(p, v) \

FENCE RW,W 뒤에 WRITE_ONCE로 저장하여 release 의미를 만듭니다. 앞선 읽기·쓰기가 이 저장 뒤로 넘어가지 않게 하므로, 준비한 자료를 다른 CPU에 공개하는 포인터나 상태 값의 저장에 사용합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L55do { \

release 저장의 타입 검사·FENCE·쓰기를 한 문장으로 묶는 블록입니다. 끝의 while(0)으로 한 번만 실행합니다.

L56 compiletime_assert_atomic_type(*p); \

저장할 타입이 원자 접근 가능한 스칼라 크기인지 컴파일 시 검사합니다. 큰 구조체 저장을 단일 release 연산으로 취급하지 않도록 합니다.

L57 RISCV_FENCE(rw, w); \

FENCE rw,w로 앞선 읽기·쓰기가 뒤의 저장보다 먼저 관찰되게 합니다. 이어지는 WRITE_ONCE가 자료 공개 지점이 되도록 하는 release 장벽입니다.

L58 WRITE_ONCE(*p, v); \

p가 가리키는 값에 v를 한 번 기록합니다. 앞의 FENCE가 준비한 자료보다 이 공개 저장이 먼저 관찰되는 것을 막습니다.

L59} while (0)

release 저장 매크로의 끝입니다. 앞의 FENCE rw,w가 이전 읽기·쓰기를 이번 저장보다 앞서게 하고 WRITE_ONCE가 값을 기록합니다. do/while (0)은 이 둘을 하나의 C 문장으로 묶습니다.

L61#define __smp_load_acquire(p) \

READ_ONCE로 읽고 FENCE R,RW를 실행하여 acquire 의미를 만듭니다. 이후 읽기·쓰기가 방금 읽은 값보다 먼저 진행한 것으로 관찰되지 않게 하고, 그 값을 반환합니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L62({ \

값을 반환하는 statement expression으로 acquire 읽기를 시작합니다. 읽기와 장벽을 묶은 뒤 원래 읽은 값을 반환합니다.

L63 typeof(*p) ___p1 = READ_ONCE(*p); \

*p를 READ_ONCE로 한 번 읽어 같은 타입의 ___p1에 보관합니다. 이 읽기에서 얻은 공개 상태를 기준으로 이후 데이터에 접근합니다.

L64 compiletime_assert_atomic_type(*p); \

대상이 원자적으로 읽을 수 있는 스칼라 타입인지 빌드 시 검사합니다. acquire를 한 번의 읽기와 연결하기 위한 타입 제약입니다.

L65 RISCV_FENCE(r, rw); \

FENCE r,rw로 앞의 읽기 이후에 뒤의 읽기·쓰기가 오도록 정렬합니다. 공개된 값을 확인하기 전에 후속 자료를 먼저 읽는 일을 막습니다.

L66 ___p1; \

acquire 매크로의 결과로 원래 READ_ONCE에서 얻은 값을 내놓습니다. 대상 메모리를 재조회하지 않습니다.

L67})

읽기·장벽·결과값을 묶은 statement expression을 닫습니다. 호출 식에는 앞의 임시값이 전달됩니다.

L69#ifdef CONFIG_RISCV_ISA_ZAWRS

Zawrs는 load-reserved로 설정한 예약의 변화를 기다리는 명령 확장입니다. 지원 빌드에서는 조건부 메모리 대기에 __cmpwait_relaxed를 사용하여 값이 바뀔 때까지 불필요하게 계속 읽는 부담을 줄입니다. 이 조건 자체가 모든 CPU의 실제 지원을 보장하는 것은 아닙니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L70#define smp_cond_load_relaxed(ptr, cond_expr) ({ \

ZAWRS 지원 구성이 사용할 조건부 대기 읽기입니다. *ptr을 VAL에 읽어 cond_expr을 평가하고, 아직 거짓이면 __cmpwait_relaxed로 값이 바뀌기를 효율적으로 기다립니다. 조건을 만족한 값을 반환하지만 relaxed형이라 별도의 acquire 순서는 추가하지 않습니다. 이 이름과 인자는 전처리 단계에 정의된 내용으로 치환됩니다. 매크로 정의 자체가 CPU 명령 실행은 아닙니다.

L71 typeof(ptr) __PTR = (ptr); \

조건 대기 대상 ptr을 같은 포인터 타입의 __PTR에 보관합니다. 반복 중 값을 읽을 기준 주소를 정하는 단계입니다.

L72 __unqual_scalar_typeof(*ptr) VAL; \

대상 스칼라형에서 const·volatile 같은 한정자를 제거한 VAL을 선언합니다. 반복해서 읽은 값을 담고 호출자가 전달한 cond_expr에서도 이 이름으로 참조합니다.

L73 for (;;) { \

조건이 만족될 때까지 값을 다시 읽는 루프를 시작합니다. 조건이 거짓이면 아래 __cmpwait_relaxed가 값 변화 대기에 들어갑니다.

L74 VAL = READ_ONCE(*__PTR); \

대기 대상 메모리를 READ_ONCE로 읽어 VAL에 넣습니다. 컴파일러가 한 번 읽은 값을 계속 재사용하지 못하게 하여 다른 CPU의 변경을 관측합니다.

L75 if (cond_expr) \

현재 VAL로 호출자가 지정한 종료 조건을 검사합니다. 참이면 대기를 끝내고, 거짓이면 해당 값이 바뀔 때까지 기다린 뒤 다시 읽습니다.

L76 break; \

조건이 만족되었으므로 반복을 종료합니다. 매크로는 루프 뒤에서 마지막 관측값 VAL을 반환하며 이 relaxed 경로는 별도 acquire 순서를 추가하지 않습니다.

05 · WORKED EXAMPLE

숫자로 검산하기

01

message-passing litmus를 architecture instruction으로 내린다

CPU0이 data=0x55를 쓰고 ready=1을 publish하며 CPU1이 ready를 본 뒤 data를 읽는 상황이다.

  1. CPU0data store 뒤 release store 또는 write barrier를 두고 ready를 쓴다.
  2. CPU1ready acquire load가 1이면 이후 data load가 0x55보다 먼저 관찰될 수 없어야 한다.
  3. ISAarm64는 STLR/LDAR 또는 DMB, x86은 TSO와 compiler constraint, RISC-V는 aq/rl 또는 FENCE로 계약을 만든다.
  4. DMAconsumer가 device라면 SMP barrier 대신 dma_wmb와 cache ownership API가 필요한지 확인한다.

결론'barrier를 넣었다'가 아니라 어떤 observer의 어떤 두 access 순서를 만들었는지 문장으로 증명한다.

06 · DEEP DIVE

경계별 상세 분석

01

공통 kernel core와 architecture hook의 경계

Linux memory model은 acquire/release와 full barrier API를 제공하지만 ISA의 기본 ordering, shareability domain, device memory와 instruction fetch coherence가 구현을 결정한다.

producer store, barrier, publication flag와 consumer acquire load를 한 litmus test로 읽는다. cache clean/invalidate는 ordering barrier와 같은 연산이 아니다.

02

arm64: DMB/DSB/ISB와 shareability·load/store option

weakly ordered ISA라 dmb ish, ishld, ishst가 SMP order를 표현한다. device/DMA에는 outer-shareable domain이 필요하고 code patch 뒤에는 D-cache clean, I-cache invalidate와 ISB가 이어진다.

release/acquire instruction과 barrier scope가 observer domain을 포함해야 한다. 디버깅할 때는 barrier 종류, domain, cache line, memory attribute와 observer CPU/device를 기록한다.

03

x86-64: TSO 기본 ordering과 LFENCE/SFENCE/MFENCE·locked op

WB memory의 store-store/load-load는 강하지만 store buffer, non-temporal store, WC memory와 speculation 때문에 barrier가 완전히 사라지지는 않는다. SMP full barrier는 locked instruction으로 구현될 수 있다.

compiler reorder와 CPU reorder를 구분하고 MMIO/WC에는 별도 API를 사용한다. 디버깅할 때는 memory type, locked op, fence, store buffer와 device posted-write completion을 확인한다.

04

RISC-V: FENCE predecessor/successor set과 aq/rl bit

RVWMO는 fence rw,rw, r,r, w,w로 observer order를 지정한다. AMO의 aq/rl은 해당 atomic 전후 ordering을 포함하고 instruction fetch에는 별도 fence.i가 필요하다.

remote hart의 instruction cache는 local fence.i만으로 갱신되지 않을 수 있어 IPI/SBI 기반 sync가 필요하다. 디버깅할 때는 FENCE pred/succ, aq/rl, I/O vs memory, hart mask와 icache sync를 기록한다.

05

객체 수명과 소유권을 먼저 고정한다

lock-free object는 publication 전 완성되어야 하고 consumer가 reference를 놓기 전 free되면 안 된다. DMA buffer는 CPU/device ownership 전환과 cache maintenance가 맞아야 한다.

주소나 register 값이 맞는지만 확인하면 stale state를 놓친다. producer, publication, consumer와 폐기 지점을 같은 표에 기록한다.

06

latency upper bound는 hardware instruction 하나가 아니다

barrier 비용은 outstanding memory transaction, domain과 microarchitecture에 따라 달라진다. DSB처럼 completion까지 기다리는 명령과 ordering만 강제하는 DMB를 분리한다.

평균값 외에 interrupt-off 구간, remote CPU 응답, firmware 호출과 retry 횟수를 분리해야 최악 지연의 원인을 찾을 수 있다.

07 · FAILURE

실패를 어떤 증거로 나눌 것인가

분류관찰되는 결과첫 확인값
arm64dmb ish만으로 device MMIO completion이나 instruction-cache 갱신까지 됐다고 오해한다.barrier 종류, domain, cache line, memory attribute와 observer CPU/device를 기록한다.
x86-64x86은 strong ordering이라는 이유로 DMA/MMIO와 speculation barrier를 모두 빈 연산으로 처리한다.memory type, locked op, fence, store buffer와 device posted-write completion을 확인한다.
RISC-Vdata fence 뒤 새 code가 실행될 것으로 가정하거나 I/O bit가 빠진 fence로 MMIO를 publish한다.FENCE pred/succ, aq/rl, I/O vs memory, hart mask와 icache sync를 기록한다.

08 · LAB

재현과 계측 절차

  1. herd7 litmus와 실제 stress test를 함께 사용해 허용 outcome을 비교한다.
  2. WB/WC/MMIO memory type을 바꿔 x86에서도 barrier 결과가 달라지는 지점을 확인한다.
  3. 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
  4. 소스만 읽고 끝내지 않고 최종 vmlinuxobjdump -dr, readelf -SW 결과로 선택된 alternative와 section 배치를 확인한다.

09 · REFERENCES

원문 좌표

Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.