Seqlock (순차 잠금)

리눅스 커널의 시퀀스 잠금(seqlock)을 분석합니다. 짝수/홀수 시퀀스 카운터 원리, seqcount_t와 seqlock_t의 차이, 리더 재시도 루프, 라이터 직렬화(Serialization) 경로, jiffies·timekeeper·vDSO의 실전 사용 사례, seqcount_latch 이중 버퍼(Buffer) 패턴, seqcount_LOCKNAME_t 연관 잠금, 메모리 배리어(Memory Barrier) 의미론, PREEMPT_RT 영향, rwlock/RCU 비교까지 커널 소스 기반으로 정리합니다.

전제 조건: 동기화 기법, Atomic 연산, 메모리 배리어 문서를 먼저 읽으세요. seqlock은 시퀀스 카운터와 메모리 배리어 위에 구축되므로, 이들의 기본 개념을 먼저 이해해야 합니다.
일상 비유: seqlock은 전광판 시스템과 같습니다. 라이터가 전광판 내용을 바꿀 때 "수정 중" 표시(홀수 번호)를 걸고, 끝나면 "수정 완료"(짝수 번호)로 바꿉니다. 리더는 번호를 확인하고 데이터를 읽은 뒤, 다시 번호를 확인하여 수정이 없었으면 결과를 사용하고, 바뀌었으면 처음부터 다시 읽습니다.

핵심 요약

  • 시퀀스 카운터 원리 — 짝수(even)=일관된 상태, 홀수(odd)=쓰기 진행 중. 리더는 읽기 전후 시퀀스가 같고 짝수면 성공, 아니면 재시도합니다.
  • 리더 비차단(Non-blocking) — 리더는 절대 블록되지 않지만, 라이터와 동시 실행 시 재시도해야 합니다. 라이터가 빈번하면 리더가 starvation될 수 있습니다.
  • seqcount_t vs seqlock_tseqcount_t는 시퀀스 카운터만 제공하여 외부 잠금이 필요하고, seqlock_t는 spinlock을 내장하여 라이터 직렬화를 자체 처리합니다.
  • 대표 사용처jiffies 보호, struct timekeeper, vDSO 데이터, 네트워크 통계, 파일시스템(Filesystem) 시간 관리.
  • seqcount_latch — NMI 안전 이중 버퍼 패턴으로, 쓰기 중에도 항상 일관된 읽기가 가능합니다.

단계별 이해

  1. 시퀀스 카운터 개념 이해
    짝수/홀수로 쓰기 상태를 추적하고, 리더가 재시도하는 기본 메커니즘을 파악합니다.
  2. seqcount_t와 seqlock_t 구분
    시퀀스 카운터만 제공하는 seqcount_t와 spinlock을 내장한 seqlock_t의 차이를 이해합니다.
  3. 읽기/쓰기 경로 추적
    read_seqbegin()→읽기→read_seqretry() 루프와 write_seqlock()→쓰기→write_sequnlock() 경로를 따라갑니다.
  4. 실전 사용 사례 학습
    jiffies, timekeeper, vDSO 등 커널 내 실제 적용 패턴을 분석합니다.
  5. 고급 패턴과 대안 비교
    seqcount_latch, 연관 잠금, PREEMPT_RT 영향, rwlock/RCU와의 비교를 통해 적절한 선택 기준을 확립합니다.
관련 표준: Lameter, C. "Effective Synchronization on Linux/NUMA Systems" — seqlock 성능 분석. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

이론적 배경: Sequence Lock의 원리

Seqlock(Sequence Lock)은 라이터 우선(writer-priority) 동기화 메커니즘입니다. 공유 데이터에 시퀀스 카운터를 부착하여, 리더가 데이터 일관성을 검증하고 불일치 시 재시도하는 낙관적(optimistic) 읽기 패턴을 구현합니다.

시퀀스 카운터의 원리

핵심 아이디어는 단순합니다:

  1. 시퀀스 카운터를 0(짝수)으로 초기화
  2. 라이터가 쓰기를 시작하면 카운터를 홀수로 증가 (0→1)
  3. 라이터가 쓰기를 완료하면 카운터를 짝수로 증가 (1→2)
  4. 리더는 읽기 전후의 카운터 값을 비교: 같고 짝수면 일관된 데이터
/* 시퀀스 카운터 상태 전이 */

시퀀스 값:  0      1        2      3        4
상태:     일관   쓰기중   일관   쓰기중   일관
         (even)  (odd)   (even)  (odd)   (even)

리더 판정:
  읽기 전 seq=0, 읽기 후 seq=0 → 성공 (같고 짝수)
  읽기 전 seq=0, 읽기 후 seq=1 → 실패 (쓰기 발생, 재시도)
  읽기 전 seq=1                → 실패 (홀수=쓰기 중, 재시도)
  읽기 전 seq=0, 읽기 후 seq=2 → 실패 (쓰기 완료됨, 재시도)
Seqlock 시퀀스 카운터 원리 시간 seq=0 (even) seq=1 (odd) seq=2 (even) seq=3 (odd) seq=4 (even) Writer: write_seqlock write_seqlock Reader 1: seq=0 → 0 SUCCESS Reader 2: seq=0 → 2 RETRY seq=2 → 2 SUCCESS Reader 3: seq=1 (odd) SPIN seq=2 → 2 SUCCESS
리더는 읽기 전후 시퀀스 값이 같고 짝수일 때만 성공하고, 아니면 재시도합니다

기존 잠금과의 근본 차이

특성spinlock/mutexrwlockseqlock
리더 차단항상라이터 보유 시절대 안 함
라이터 차단다른 보유자에 의해리더/라이터에 의해다른 라이터에 의해만
리더 오버헤드(Overhead)원자적 연산(Atomic Operation)원자적 연산시퀀스 비교만 (배리어)
리더 진행 보장유한 대기유한 대기보장 안 됨 (starvation 가능)
적합 시나리오짧은 배타적 접근읽기 >> 쓰기읽기 >> 쓰기, 쓰기가 빨라야 함

seqcount_t vs seqlock_t vs seqcount_latch_t

커널은 세 가지 주요 시퀀스 카운터 변형을 제공합니다. 각각의 용도와 특성이 다릅니다.

Seqlock 세 가지 변형 seqcount_t unsigned sequence; 시퀀스 카운터만 제공 외부 잠금으로 라이터 직렬화 필요 예: 이미 spinlock 보유 시 seqlock_t seqcount_spinlock_t seqcount; spinlock_t lock; spinlock 내장, 자체 직렬화 예: jiffies, timekeeper seqcount_latch_t seqcount_t seqcount; 이중 버퍼 패턴용 쓰기 중에도 일관된 읽기 보장 예: NMI-safe 통계 비교 요약 seqcount_t 라이터 직렬화: 외부 잠금 NMI-safe: 가능 (raw_read) 크기: 4B seqlock_t 라이터 직렬화: 내장 spinlock NMI-safe: 읽기만 크기: 8B+ seqcount_latch_t 라이터 직렬화: 외부 잠금 NMI-safe: 완전 지원 크기: 4B+2x데이터
용도에 따라 세 가지 변형 중 적합한 것을 선택합니다
타입라이터 직렬화리더 재시도NMI-safe대표 사용처
seqcount_t외부 잠금 필요필요raw_read_seqcount()이미 잠금 보유 상황
seqlock_t내장 spinlock필요읽기만jiffies, timekeeper
seqcount_latch_t외부 잠금 필요불필요 (이중 버퍼)완전 지원NMI-safe 통계

데이터 구조 분석

seqlock의 핵심 데이터 구조를 커널 소스(include/linux/seqlock.h) 기반으로 분석합니다.

seqcount_t 구조

/* include/linux/seqlock.h */
typedef struct seqcount {
    unsigned sequence;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
} seqcount_t;

/* 초기화 매크로 */
#define SEQCNT_ZERO(name) { .sequence = 0, ... }
#define seqcount_init(s)  do { (s)->sequence = 0; ... } while (0)

seqlock_t 구조

/* include/linux/seqlock.h */
typedef struct {
    struct seqcount_spinlock  seqcount;
    spinlock_t               lock;
} seqlock_t;

/* 초기화 */
#define __SEQLOCK_UNLOCKED(name) {                    \
    .seqcount = SEQCNT_SPINLOCK_ZERO(name, &(name).lock), \
    .lock = __SPIN_LOCK_UNLOCKED(name.lock)           \
}

#define DEFINE_SEQLOCK(name) \
    seqlock_t name = __SEQLOCK_UNLOCKED(name)

seqcount_latch_t 구조

/* include/linux/seqlock.h */
typedef struct {
    seqcount_t seqcount;
} seqcount_latch_t;

/* 초기화 */
#define SEQCNT_LATCH_ZERO(name) {                    \
    .seqcount = SEQCNT_ZERO(name.seqcount),          \
}
seqlock_t 메모리 레이아웃 seqlock_t seqcount_spinlock_t seqcount unsigned sequence spinlock_t *lock (ptr) spinlock_t lock raw_spinlock_t rlock 오프셋 +0: sequence (4 bytes) 오프셋 +4~+8: 내부 포인터 (아키텍처 의존) 오프셋 +8~+12: spinlock_t lock (4+ bytes)
seqlock_t는 seqcount_spinlock_t와 spinlock_t를 하나로 묶은 구조체(Struct)입니다

seqcount_t API 레퍼런스

seqcount_t는 시퀀스 카운터만 제공하며, 라이터 직렬화를 위한 외부 잠금이 필요합니다.

함수설명컨텍스트
raw_read_seqcount_begin()시퀀스 읽기 시작 (smp_rmb 포함, 홀수 시 스핀)모든 컨텍스트
read_seqcount_begin()위와 동일 + lockdep 검증모든 컨텍스트
read_seqcount_retry()읽기 종료, 시퀀스 불일치 시 true 반환모든 컨텍스트
raw_read_seqcount()홀수 대기 없이 즉시 시퀀스 반환 (NMI용)NMI
raw_seqcount_begin()시퀀스를 짝수로 맞춰서 반환 (홀수면 & ~1)특수 용도
write_seqcount_begin()시퀀스 +1 (홀수로), smp_wmb 삽입라이터
write_seqcount_end()smp_wmb + 시퀀스 +1 (짝수로)라이터
write_seqcount_invalidate()시퀀스를 홀수로 만들어 모든 리더 무효화(Invalidation)라이터
raw_write_seqcount_barrier()배리어만, 시퀀스 +2 (latch 패턴용)라이터

seqcount_t 읽기 패턴

unsigned seq;
u64 value;

do {
    seq = read_seqcount_begin(&sc);
    /* 공유 데이터 읽기 — 부수 효과 금지! */
    value = shared_data.field;
} while (read_seqcount_retry(&sc, seq));

seqcount_t 쓰기 패턴

/* 외부 잠금으로 라이터 직렬화 필요 */
spin_lock(&my_lock);
write_seqcount_begin(&sc);
shared_data.field = new_value;
write_seqcount_end(&sc);
spin_unlock(&my_lock);

seqlock_t API 레퍼런스

seqlock_t는 spinlock을 내장하여 라이터 직렬화를 자체 처리합니다. 가장 자주 사용되는 형태입니다.

함수설명컨텍스트
read_seqbegin()시퀀스 읽기 시작모든 컨텍스트
read_seqretry()시퀀스 불일치 시 true 반환모든 컨텍스트
write_seqlock()spinlock 획득 + 시퀀스 증가프로세스(Process)
write_sequnlock()시퀀스 증가 + spinlock 해제프로세스
write_seqlock_irqsave()IRQ 비활성화 + spinlock + 시퀀스인터럽트(Interrupt) 공유 시
write_sequnlock_irqrestore()시퀀스 + spinlock 해제 + IRQ 복원인터럽트 공유 시
write_seqlock_bh()softirq 비활성화 + spinlock + 시퀀스softirq 공유 시
write_sequnlock_bh()시퀀스 + spinlock 해제 + softirq 활성화softirq 공유 시
read_seqbegin_irqsave()IRQ 비활성화 + 시퀀스 읽기 시작IRQ 안전 읽기
read_seqretry_irqrestore()시퀀스 확인 + IRQ 복원IRQ 안전 읽기

Read 경로: read_seqbegin/read_seqretry 루프

seqlock 리더의 핵심은 do-while 루프입니다. 데이터 읽기 전후로 시퀀스 번호를 확인하여 일관성을 검증합니다.

/* include/linux/seqlock.h — read_seqbegin() 구현 */
static inline unsigned read_seqbegin(const seqlock_t *sl)
{
    unsigned ret = read_seqcount_begin(&sl->seqcount);

    kcsan_atomic_next(KCSAN_SEQLOCK_REGION_MAX);
    return ret;
}

/* read_seqcount_begin() 핵심 */
static inline unsigned read_seqcount_begin(const seqcount_t *s)
{
    unsigned ret;

repeat:
    ret = READ_ONCE(s->sequence);
    if (unlikely(ret & 1)) {
        cpu_relax();        /* 홀수 = 쓰기 중, 스핀 대기 */
        goto repeat;
    }
    smp_rmb();              /* 시퀀스 읽기 → 데이터 읽기 순서 보장 */
    return ret;
}

/* read_seqretry() 핵심 */
static inline int read_seqretry(const seqlock_t *sl, unsigned start)
{
    return read_seqcount_retry(&sl->seqcount, start);
}

/* read_seqcount_retry() 핵심 */
static inline int read_seqcount_retry(const seqcount_t *s, unsigned start)
{
    smp_rmb();              /* 데이터 읽기 → 시퀀스 읽기 순서 보장 */
    return unlikely(READ_ONCE(s->sequence) != start);
}
Seqlock 리더 루프 상세 흐름 read_seqbegin() seq = READ_ONCE(s->sequence) seq & 1 ? Yes (odd) cpu_relax() No (even) smp_rmb() 공유 데이터 읽기 smp_rmb() + seq 재확인 seq 변경? Yes RETRY No SUCCESS
리더는 홀수 시퀀스를 만나면 스핀하고, 읽기 후 시퀀스가 변경되었으면 처음부터 재시도합니다
리더 제약: seqlock 읽기 루프 안에서는 부수 효과(side effect)가 있는 연산을 절대 수행하면 안 됩니다. 데이터가 일관되지 않을 수 있으므로, 읽은 값을 로컬 변수에 복사만 하고, 성공이 확인된 후에 사용해야 합니다. 포인터 역참조(Dereference), 메모리 할당, I/O 등은 루프 바깥에서 수행하세요.

Write 경로: write_seqlock/write_sequnlock

seqlock의 쓰기 경로는 단순합니다: spinlock 획득 → 시퀀스 홀수화 → 데이터 수정 → 시퀀스 짝수화 → spinlock 해제.

/* include/linux/seqlock.h — write_seqlock() 구현 */
static inline void write_seqlock(seqlock_t *sl)
{
    spin_lock(&sl->lock);
    write_seqcount_begin(&sl->seqcount);
}

/* write_seqcount_begin() 핵심 */
static inline void write_seqcount_begin(seqcount_t *s)
{
    s->sequence++;         /* even → odd: 쓰기 시작 */
    smp_wmb();             /* 시퀀스 증가 → 데이터 쓰기 순서 보장 */
}

/* write_sequnlock() 구현 */
static inline void write_sequnlock(seqlock_t *sl)
{
    write_seqcount_end(&sl->seqcount);
    spin_unlock(&sl->lock);
}

/* write_seqcount_end() 핵심 */
static inline void write_seqcount_end(seqcount_t *s)
{
    smp_wmb();             /* 데이터 쓰기 → 시퀀스 증가 순서 보장 */
    s->sequence++;         /* odd → even: 쓰기 완료 */
}
write_seqlock/write_sequnlock 경로 spin_lock() 라이터 직렬화 sequence++ even → odd smp_wmb() 데이터 수정 임계 영역 smp_wmb() sequence++ odd → even spin_unlock() 잠금 해제 write_seqlock() = spin_lock + seq++ spinlock 획득 후 시퀀스를 홀수로 만듦 → 리더에게 "쓰기 진행 중" 신호 write_sequnlock() = seq++ + spin_unlock 시퀀스를 짝수로 만든 후 spinlock 해제 → 리더에게 "일관된 상태" 신호
라이터는 spinlock으로 직렬화하고, 시퀀스 카운터로 리더에게 쓰기 상태를 알립니다

Retry 메커니즘과 진행 보장

seqlock의 리더는 진행 보장(forward progress guarantee)이 없습니다. 라이터가 지속적으로 쓰기를 수행하면 리더는 무한히 재시도할 수 있습니다.

재시도 시나리오 분석

시나리오read_seqbegin 결과read_seqretry 결과동작
쓰기 없음짝수 반환 (즉시)false (성공)1회만에 성공
읽기 중 쓰기 시작짝수 반환true (시퀀스 변경)재시도
쓰기 중 읽기 시도홀수 → 스핀 대기짝수 될 때까지 스핀
읽기 중 쓰기 완료+재시작(Reboot)짝수 반환true (시퀀스 +2)재시도
연속적 쓰기반복 스핀starvation
/* starvation 방지 패턴: 재시도 횟수 제한 후 fallback */
unsigned seq;
int retries = 0;
u64 value;

do {
    seq = read_seqbegin(&my_seqlock);
    value = shared_data;
    if (++retries > MAX_SEQLOCK_RETRIES) {
        /* fallback: 쓰기 잠금으로 배타적 읽기 */
        write_seqlock(&my_seqlock);
        value = shared_data;
        write_sequnlock(&my_seqlock);
        break;
    }
} while (read_seqretry(&my_seqlock, seq));
실전 참고: 커널 내 대부분의 seqlock 사용은 라이터 빈도가 매우 낮아 starvation이 문제되지 않습니다. 예를 들어 jiffies_lock은 1ms~10ms 간격으로만 갱신되므로 리더가 starvation될 확률은 사실상 0입니다.

jiffies 보호 패턴: 대표 사용 사례

seqlock의 가장 대표적인 사용 사례는 jiffies 보호입니다. 32비트 시스템에서 64비트 jiffies_64를 원자적(Atomic)으로 읽기 위해 seqlock을 사용합니다.

/* kernel/time/jiffies.c */
static DEFINE_SEQLOCK(jiffies_lock);

/* 라이터: 타이머 인터럽트에서 호출 */
void do_timer(unsigned long ticks)
{
    write_seqlock(&jiffies_lock);
    jiffies_64 += ticks;
    write_sequnlock(&jiffies_lock);
    calc_global_load();
}

/* 리더: get_jiffies_64() */
u64 get_jiffies_64(void)
{
    unsigned seq;
    u64 ret;

    do {
        seq = read_seqbegin(&jiffies_lock);
        ret = jiffies_64;
    } while (read_seqretry(&jiffies_lock, seq));

    return ret;
}
왜 seqlock인가? 32비트 아키텍처에서 64비트 jiffies_64는 두 번의 32비트 로드로 읽힙니다. 타이머(Timer) 인터럽트가 두 로드 사이에 발생하면 상위/하위 워드가 다른 시점의 값이 섞입니다(torn read). seqlock은 이런 torn read를 감지하고 재시도하게 합니다. 64비트 시스템에서는 jiffies_64가 원자적으로 읽히므로 seqlock이 필요 없지만, 이식성을 위해 동일 API를 사용합니다.

struct timekeeper: 시간 서브시스템

/* kernel/time/timekeeping.c */
static struct {
    seqcount_raw_spinlock_t seq;
    struct timekeeper       timekeeper;
} tk_core ____cacheline_aligned = {
    .seq = SEQCNT_RAW_SPINLOCK_ZERO(tk_core.seq, &timekeeper_lock),
};

/* 리더: ktime_get() — vDSO에서도 호출 */
ktime_t ktime_get(void)
{
    struct timekeeper *tk = &tk_core.timekeeper;
    unsigned seq;
    ktime_t base;
    u64 nsecs;

    do {
        seq = read_seqcount_begin(&tk_core.seq);
        base = tk->tkr_mono.base;
        nsecs = timekeeping_get_ns(&tk->tkr_mono);
    } while (read_seqcount_retry(&tk_core.seq, seq));

    return ktime_add_ns(base, nsecs);
}
jiffies seqlock 보호 — 32비트 torn read 방지 32비트 시스템 상위 32비트 하위 32비트 torn read 문제 1. 리더: 상위 읽기 (0x0000_0000) 2. IRQ: jiffies_64 = 0x0000_0001_0000_0000 3. 리더: 하위 읽기 (0x0000_0000) → 결과: 0x0000_0000_0000_0000 (잘못!) → 실제: 0x0000_0001_0000_0000 seqlock 해결 read_seqbegin(seq=0) → 상위 읽기 → [IRQ: seq 1→2] → 하위 읽기 → read_seqretry(seq 변경!) → RETRY read_seqbegin(seq=2) → 상위 읽기 → 하위 읽기 → read_seqretry(seq=2, 일치) → SUCCESS 시퀀스 변경 감지로 torn read를 100% 방지합니다
seqlock은 32비트 시스템에서 64비트 값의 torn read를 시퀀스 비교로 감지합니다

통계 카운터 패턴

네트워크 서브시스템은 seqlock을 활용하여 64비트 통계 카운터를 비원자적 업데이트하면서도 일관된 스냅샷을 제공합니다.

/* include/linux/u64_stats_sync.h */
struct u64_stats_sync {
#if BITS_PER_LONG == 32
    seqcount_t seq;
#endif
};

/* 쓰기 측: 통계 업데이트 (per-CPU이므로 외부 잠금 불필요) */
void update_stats(struct net_device_stats *stats, int bytes)
{
    u64_stats_update_begin(&stats->syncp);
    stats->rx_bytes += bytes;
    stats->rx_packets++;
    u64_stats_update_end(&stats->syncp);
}

/* 읽기 측: 통계 스냅샷 */
void read_stats(struct net_device_stats *stats, u64 *bytes, u64 *packets)
{
    unsigned start;
    do {
        start = u64_stats_fetch_begin(&stats->syncp);
        *bytes   = stats->rx_bytes;
        *packets = stats->rx_packets;
    } while (u64_stats_fetch_retry(&stats->syncp, start));
}
64비트 최적화: u64_stats_sync는 64비트 시스템에서 seqcount_t가 no-op이 됩니다. 64비트 아키텍처에서는 u64 읽기/쓰기가 원자적이므로 시퀀스 카운터가 불필요합니다. 이 패턴은 32비트/64비트 이식성을 위한 커널의 표준 접근 방식입니다.

seqcount_latch: 이중 버퍼 패턴

seqcount_latch_t는 일반 seqlock과 달리 쓰기 중에도 항상 일관된 읽기를 보장합니다. 이중 버퍼를 사용하여 라이터가 한 버퍼를 업데이트하는 동안 리더는 다른 버퍼를 읽습니다.

/* 이중 버퍼 패턴의 핵심 구조 */
struct latch_data {
    seqcount_latch_t    latch;
    struct data_entry   data[2]; /* 이중 버퍼 */
};

/* 라이터: raw_write_seqcount_latch() 사용 */
void latch_write(struct latch_data *ld, struct data_entry *new)
{
    /* 1단계: 시퀀스 증가 (even→odd), 첫 번째 버퍼 업데이트 */
    raw_write_seqcount_latch(&ld->latch);
    ld->data[0] = *new;

    /* 2단계: 시퀀스 증가 (odd→even), 두 번째 버퍼 업데이트 */
    raw_write_seqcount_latch(&ld->latch);
    ld->data[1] = *new;
}

/* 리더: 시퀀스 기반 버퍼 선택 */
void latch_read(struct latch_data *ld, struct data_entry *dst)
{
    unsigned seq;
    do {
        seq = raw_read_seqcount_latch(&ld->latch);
        *dst = ld->data[seq & 1]; /* 시퀀스 LSB로 버퍼 선택 */
    } while (raw_read_seqcount_latch_retry(&ld->latch, seq));
}
seqcount_latch 이중 버퍼 동작 Phase 1: seq=0 (even) data[0] 라이터 업데이트 중 data[1] 리더 읽기 (seq&1=0→1) Phase 2: seq=1 (odd) data[0] 리더 읽기 (seq&1=1→0) data[1] 라이터 업데이트 중 seq++ 전체 쓰기 시퀀스 seq++ (0→1) data[0] 업데이트 seq++ (1→2) data[1] 업데이트 완료 핵심: NMI-safe 일반 seqlock: 쓰기 중 읽기 → 불일관 데이터 → 재시도 (재시도 불가 컨텍스트에서 위험) seqcount_latch: 쓰기 중 읽기 → 다른 버퍼에서 일관된 데이터 → 재시도 불필요 NMI 핸들러처럼 재시도 루프가 불가능하거나 위험한 컨텍스트에서 안전합니다
이중 버퍼로 라이터가 한 버퍼를 수정하는 동안 리더는 다른 버퍼에서 일관된 데이터를 읽습니다

커널 사용 사례: struct latched_seq

/* include/linux/seqlock.h */
struct latched_seq {
    seqcount_latch_t    latch;
    u64                 val[2];
};

/* kernel/time/timekeeping.c — NMI-safe 시간 읽기 */
static struct latched_seq timekeeping_cycles;

/* NMI 컨텍스트에서 안전하게 사이클 카운터 읽기 */
u64 read_latched_cycles(void)
{
    return read_seqcount_latch_retval(&timekeeping_cycles.latch,
                                      timekeeping_cycles.val);
}

seqcount_LOCKNAME_t: 연관 잠금

커널은 seqcount_t에 연관 잠금을 명시적으로 바인딩하는 타입 변형을 제공합니다. 이를 통해 lockdep이 라이터가 올바른 잠금을 보유하고 있는지 검증할 수 있습니다.

타입연관 잠금용도
seqcount_spinlock_tspinlock_tseqlock_t 내부에서 사용
seqcount_raw_spinlock_traw_spinlock_tPREEMPT_RT에서도 진정한 spinlock 필요 시
seqcount_rwlock_trwlock_trwlock 내부에서 시퀀스 보호 필요 시
seqcount_mutex_tstruct mutex슬립(Sleep) 가능 라이터 경로
seqcount_ww_mutex_tstruct ww_mutexwound-wait 교착 방지 뮤텍스(Mutex)
/* 선언과 초기화 */
spinlock_t my_lock;
seqcount_spinlock_t my_seqcount = SEQCNT_SPINLOCK_ZERO(my_seqcount, &my_lock);

/* lockdep 검증: write_seqcount_begin()이 my_lock 보유를 확인 */
spin_lock(&my_lock);
write_seqcount_begin(&my_seqcount);  /* lockdep: my_lock 보유 검증 */
/* ... 데이터 수정 ... */
write_seqcount_end(&my_seqcount);
spin_unlock(&my_lock);

/* 잘못된 사용: lockdep 경고 발생 */
write_seqcount_begin(&my_seqcount);  /* WARNING: my_lock not held! */
lockdep 연관 잠금 규칙: seqcount_LOCKNAME_t를 사용하면 write_seqcount_begin() 호출 시 연관 잠금 보유 여부를 lockdep이 자동 검증합니다. 연관 잠금 없이 seqcount_t를 직접 사용하면 이 검증이 빠지므로, 가능하면 연관 잠금 변형을 사용하세요.

메모리 순서와 배리어

seqlock의 정확한 동작은 정밀한 메모리 배리어 배치에 의존합니다. 각 API 함수에 삽입된 배리어의 목적을 분석합니다.

Seqlock 메모리 배리어 배치 Writer (CPU 0) s->sequence++ (even→odd) smp_wmb() seq 증가가 데이터 쓰기 전에 보임 공유 데이터 수정 (임계 영역) smp_wmb() 데이터 쓰기가 seq 증가 전에 완료 s->sequence++ (odd→even) Reader (CPU 1) seq = READ_ONCE(s->sequence) smp_rmb() seq 읽기가 데이터 읽기 전에 완료 공유 데이터 읽기 (로컬 변수에 복사) smp_rmb() 데이터 읽기가 seq 재읽기 전에 완료 seq2 = READ_ONCE(s->sequence) seq != seq2 ? RETRY : SUCCESS
라이터의 smp_wmb()와 리더의 smp_rmb()가 쌍으로 동작하여 데이터 일관성을 보장합니다
배리어위치보장하는 순서
smp_wmb()write_seqcount_begin() 내부시퀀스 홀수화 → 데이터 쓰기 (라이터)
smp_wmb()write_seqcount_end() 내부데이터 쓰기 → 시퀀스 짝수화 (라이터)
smp_rmb()read_seqcount_begin() 내부시퀀스 읽기 → 데이터 읽기 (리더)
smp_rmb()read_seqcount_retry() 내부데이터 읽기 → 시퀀스 재읽기 (리더)
아키텍처별 최적화: x86은 TSO(Total Store Ordering) 모델이므로 smp_wmb()가 컴파일러 배리어(barrier())로 축소됩니다. ARM/RISC-V의 약한 메모리 모델에서는 실제 하드웨어 명령어(DMB, fence)로 확장됩니다.

PREEMPT_RT 영향

PREEMPT_RT 커널에서 seqlock의 동작이 변경됩니다. 핵심 차이점을 분석합니다.

요소일반 커널PREEMPT_RT
seqlock_t 내부 lockspinlock_t (진짜 스핀)spinlock_t → rt_mutex (슬립)
라이터 선점(Preemption)불가 (선점 비활성화)가능 (우선순위 상속(Priority Inheritance))
리더 스핀짧은 스핀 (홀수 대기)더 긴 스핀 가능 (라이터 선점 시)
write_seqlock_irqsave()IRQ 비활성화threaded IRQ → IRQ 비활성화 불필요 (마이그레이션만 비활성화)
/* PREEMPT_RT에서의 seqlock 변화 */

/* 일반: spinlock_t = 진짜 spinlock */
/* RT:   spinlock_t = rt_mutex 기반 → 슬립 가능 */

/* RT에서 진정한 스핀이 필요한 경우 */
static DEFINE_RAW_SPINLOCK(my_raw_lock);
static seqcount_raw_spinlock_t my_seqcount =
    SEQCNT_RAW_SPINLOCK_ZERO(my_seqcount, &my_raw_lock);

/* NMI/hardirq에서 안전 */
raw_spin_lock(&my_raw_lock);
write_seqcount_begin(&my_seqcount);
/* ... */
write_seqcount_end(&my_seqcount);
raw_spin_unlock(&my_raw_lock);
RT 주의점: PREEMPT_RT에서 seqlock_t의 내장 spinlock이 rt_mutex로 변환되면, 라이터가 쓰기 도중 선점될 수 있습니다. 이 경우 시퀀스가 홀수인 상태로 유지되어 리더가 더 오래 스핀합니다. 지연(Latency) 시간에 민감한 경로에서는 seqcount_raw_spinlock_t를 사용하여 진정한 스핀을 보장하세요.

실전 사용 패턴

다중 필드 일관 읽기

/* 여러 필드를 일관되게 읽어야 하는 경우 */
struct coord {
    seqlock_t lock;
    s64 x, y, z;    /* 세 좌표가 항상 같은 시점의 값이어야 함 */
};

void read_coord(struct coord *c, s64 *x, s64 *y, s64 *z)
{
    unsigned seq;
    do {
        seq = read_seqbegin(&c->lock);
        *x = c->x;
        *y = c->y;
        *z = c->z;
    } while (read_seqretry(&c->lock, seq));
}

void update_coord(struct coord *c, s64 x, s64 y, s64 z)
{
    write_seqlock(&c->lock);
    c->x = x;
    c->y = y;
    c->z = z;
    write_sequnlock(&c->lock);
}

seqlock + RCU 조합

/* seqlock으로 데이터 일관성, RCU로 포인터 보호 */
struct config {
    seqcount_t      seq;
    struct rcu_head  rcu;
    u64             param_a;
    u64             param_b;
};

struct config __rcu *current_config;

/* 리더: RCU + seqcount 이중 보호 */
void read_config(u64 *a, u64 *b)
{
    struct config *cfg;
    unsigned seq;

    rcu_read_lock();
    cfg = rcu_dereference(current_config);
    do {
        seq = read_seqcount_begin(&cfg->seq);
        *a = cfg->param_a;
        *b = cfg->param_b;
    } while (read_seqcount_retry(&cfg->seq, seq));
    rcu_read_unlock();
}

vDSO 데이터 패턴

/* arch/x86/include/asm/vdso/gettimeofday.h */
/* vDSO: 시스템 콜 없이 사용자 공간에서 시간 읽기 */
static __always_inline int do_hres(const struct vdso_data *vd,
                                    clockid_t clk, struct __kernel_timespec *ts)
{
    const struct vdso_timestamp *vdso_ts = &vd->basetime[clk];
    u64 cycles, ns;
    u32 seq;

    do {
        seq = vdso_read_begin(vd);
        /* 시퀀스 기반 일관 읽기 — 사용자 공간에서! */
        cycles = __arch_get_hw_counter(vd->clock_mode, vd);
        ns = vdso_calc_ns(vd, cycles, vdso_ts->nsec);
        ts->tv_sec = vdso_ts->sec;
    } while (unlikely(vdso_read_retry(vd, seq)));

    ts->tv_nsec = ns;
    return 0;
}

안티패턴과 제약 조건

seqlock 사용 시 반드시 피해야 할 패턴과 제약 조건을 정리합니다.

#안티패턴문제해결
1읽기 루프에서 포인터 역참조불일관 포인터 → use-after-free/OOPSRCU로 포인터 보호, seqlock은 데이터만
2읽기 루프에서 메모리 할당재시도 시 메모리 누수루프 밖에서 할당
3읽기 루프에서 I/O 수행중복 I/O, 부수 효과루프 밖에서 I/O
4seqcount_t에 외부 잠금 없이 쓰기다중 라이터 경합(Contention) → 데이터 손상seqlock_t 사용 또는 외부 잠금 추가
5읽기 경로에서 슬립라이터 starvation, 시퀀스 오버플로우슬립이 필요하면 rwsem 사용
6쓰기 경로가 매우 긴 경우리더 재시도 과다, CPU 낭비mutex + condvar 패턴으로 전환
7동적 크기 데이터 보호포인터 기반 → 불일관 가능고정 크기 인라인 데이터만 보호
/* 안티패턴 1: 포인터 역참조 — 절대 하면 안 됨 */
do {
    seq = read_seqbegin(&sl);
    ptr = shared_ptr;          /* 불일관 포인터! */
    val = ptr->field;          /* OOPS: ptr이 이미 해제되었을 수 있음 */
} while (read_seqretry(&sl, seq));

/* 올바른 방법: RCU + seqcount */
rcu_read_lock();
ptr = rcu_dereference(shared_ptr);  /* RCU로 포인터 수명 보장 */
do {
    seq = read_seqcount_begin(&ptr->seq);
    val = ptr->field;                    /* 데이터만 seqcount로 보호 */
} while (read_seqcount_retry(&ptr->seq, seq));
rcu_read_unlock();

seqlock vs rwlock vs RCU 비교

읽기 비율이 높은 데이터에 대한 세 가지 동기화 메커니즘을 비교합니다.

읽기 중심 동기화 비교: seqlock vs rwlock vs RCU rwlock 리더: 원자적 카운터 증감 라이터: 리더 0 될 때까지 대기 장점: 리더 진행 보장 단점: 리더도 원자적 연산 필요 라이터 starvation 가능 적합: 읽기 >> 쓰기, 리더 보장 필요 seqlock 리더: 시퀀스 비교만 (배리어) 라이터: spinlock + 시퀀스 증가 장점: 리더 매우 가벼움 단점: 리더 starvation 가능 리더 부수 효과 금지 적합: 쓰기 드물고 빠른 경우 RCU 리더: 선점 비활성화만 라이터: 복사-수정-교체 장점: 리더 오버헤드 0에 가까움 단점: 라이터 비용 높음 (복사) grace period 지연 적합: 포인터 기반 데이터 정량 비교 리더 비용: rwlock: ~40 cycles (atomic) seqlock: ~10 cycles (barrier) RCU: ~5 cycles (preempt) 라이터 비용: rwlock: ~40 cycles seqlock: ~20 cycles RCU: ~100+ cycles (copy) 리더 차단: rwlock: 라이터에 의해 seqlock: 절대 안 함 RCU: 절대 안 함 데이터 타입: rwlock: 제한 없음 seqlock: 값 타입만 RCU: 포인터 기반
데이터 특성과 접근 패턴에 따라 적합한 동기화 메커니즘이 달라집니다

선택 가이드

조건추천이유
작은 값 타입 + 쓰기 드묾seqlock리더 비용 최소, 포인터 불필요
포인터 기반 + 쓰기 드묾RCU포인터 수명 관리 내장
리더 진행 보장 필요rwlockseqlock은 리더 starvation 가능
NMI-safe 필요 + 값 타입seqcount_latch이중 버퍼로 항상 일관된 읽기
읽기 중 슬립 필요rwsemseqlock 리더는 슬립 불가
쓰기 빈도 높음per-CPU + seqlock쓰기 경합 분산

디버깅(Debugging)과 lockdep

seqlock 관련 버그 디버깅 방법과 lockdep 지원을 정리합니다.

lockdep과 seqcount

/* CONFIG_DEBUG_LOCK_ALLOC 활성 시 lockdep 검증 */

/* 1. 연관 잠금 미보유 경고 */
seqcount_spinlock_t sc = SEQCNT_SPINLOCK_ZERO(sc, &my_lock);

write_seqcount_begin(&sc);  /* lockdep: my_lock not held! */
/* → "WARNING: lock held when calling write_seqcount_begin()" */

/* 2. lockdep_assert 매크로 */
void update_protected_data(void)
{
    lockdep_assert_held(&my_lock);     /* 외부 잠금 보유 확인 */
    write_seqcount_begin(&sc);
    /* ... */
    write_seqcount_end(&sc);
}

흔한 버그 패턴

증상원인진단 방법
리더 무한 루프라이터가 write_seqcount_end() 누락sysrq-l로 스택 확인, 시퀀스 홀수 확인
불일관 데이터 사용읽기 루프에서 포인터 역참조KASAN 리포트, 코드 리뷰
lockdep 경고연관 잠금 없이 write_seqcount_begin()lockdep 메시지 확인
성능 저하쓰기 빈도 과다 → 리더 재시도 폭증perf stat으로 재시도 횟수 추정
32비트에서 torn readu64_stats_sync 미사용32비트 빌드 테스트

ftrace로 seqlock 추적

# 시퀀스 카운터 변화 추적 (동적 probe)
$ echo 'p:seqw write_seqcount_begin s=%di:u32' > /sys/kernel/debug/tracing/kprobe_events
$ echo 1 > /sys/kernel/debug/tracing/events/kprobes/seqw/enable
$ cat /sys/kernel/debug/tracing/trace_pipe

# jiffies_lock 쓰기 추적
$ echo 'p:jiffies_write do_timer' > /sys/kernel/debug/tracing/kprobe_events
$ echo 1 > /sys/kernel/debug/tracing/events/kprobes/jiffies_write/enable

성능 특성

seqlock의 성능을 정량적으로 분석합니다.

Seqlock 리더 비용 vs 쓰기 빈도 쓰기 빈도 (writes/sec) 리더 비용 (cycles) seqlock rwlock RCU 교차점 전형적 커널 사용 범위
쓰기 빈도가 낮을 때 seqlock 리더 비용은 rwlock보다 훨씬 가볍지만, 쓰기가 많아지면 재시도로 비용이 급증합니다
메트릭조건
리더 비용 (무경합)~8-12 cyclessmp_rmb 2회 + READ_ONCE 2회
리더 비용 (1회 재시도)~20-25 cycles루프 1회 추가
라이터 비용~15-20 cyclesspinlock + smp_wmb 2회 + sequence++
캐시 라인(Cache Line) bounce1회/쓰기sequence 필드 1개만 공유
리더 확장성완벽 (O(1))리더끼리 상호 간섭 없음
라이터 확장성제한 (spinlock)라이터는 직렬화됨
캐시 라인 분석: seqlock 리더는 sequence 필드를 읽기만 하므로, 여러 리더가 같은 캐시 라인을 Shared 상태로 공유합니다. 라이터가 sequence를 수정할 때만 캐시 라인이 Invalid로 전환됩니다. 이는 rwlock의 cnts 원자적 증감이 매 읽기마다 캐시 라인 bounce를 유발하는 것과 대조적입니다.

관련 커널 설정

설정기본값효과
CONFIG_DEBUG_LOCK_ALLOCNseqcount lockdep 검증 활성화
CONFIG_PROVE_LOCKINGN잠금 의존성 그래프 검증 (seqcount 연관 잠금 포함)
CONFIG_LOCK_STATN/proc/lock_stat에 잠금 통계 (seqlock 포함)
CONFIG_DEBUG_ATOMIC_SLEEPN원자 컨텍스트에서 슬립 감지 (seqlock 리더 내 실수)
CONFIG_PREEMPT_RTNspinlock_t → rt_mutex 변환 (seqlock_t 영향)
CONFIG_KCSANNseqlock 리더 내 데이터 레이스 감지
# 개발/디버깅 시 권장 설정
CONFIG_DEBUG_LOCK_ALLOC=y
CONFIG_PROVE_LOCKING=y
CONFIG_LOCK_STAT=y
CONFIG_DEBUG_ATOMIC_SLEEP=y

# KCSAN으로 seqlock 데이터 레이스 검증
CONFIG_KCSAN=y
CONFIG_KCSAN_STRICT=y

read_seqbegin/read_seqretry 소스 분석

seqlock 리더 경로의 핵심인 read_seqbegin()read_seqretry()의 내부 구현을 커널 소스(include/linux/seqlock.h) 수준에서 단계별로 분석합니다.

raw_read_seqcount_begin() 상세

raw_read_seqcount_begin()은 lockdep 검증 없이 시퀀스를 읽는 최하위 함수입니다. NMI나 하드IRQ 핸들러(Handler)에서도 안전합니다.

/* include/linux/seqlock.h — 호출 체인 분석 */

/* 최상위: read_seqbegin() */
static inline unsigned read_seqbegin(const seqlock_t *sl)
{
    unsigned ret = read_seqcount_begin(&sl->seqcount);
    kcsan_atomic_next(KCSAN_SEQLOCK_REGION_MAX);
    return ret;
}

/* 중간: read_seqcount_begin() — lockdep 검증 포함 */
static inline unsigned read_seqcount_begin(const seqcount_t *s)
{
    seqcount_lockdep_reader_access(s); /* CONFIG_DEBUG_LOCK_ALLOC 시 검증 */
    return raw_read_seqcount_begin(s);
}

/* 최하위: raw_read_seqcount_begin() — 핵심 로직 */
static inline unsigned raw_read_seqcount_begin(const seqcount_t *s)
{
    unsigned ret = raw_read_seqcount(s);
    smp_rmb();  /* ① 시퀀스 읽기 → 데이터 읽기 순서 보장 */
    return ret & ~1; /* ② 홀수를 짝수로 마스크 */
}

/* raw_read_seqcount() — 홀수(쓰기 중) 대기 루프 */
static inline unsigned raw_read_seqcount(const seqcount_t *s)
{
    unsigned ret;
repeat:
    ret = READ_ONCE(s->sequence);
    if (unlikely(ret & 1)) {
        cpu_relax();   /* 홀수 = 쓰기 진행 중, 양보 후 재시도 */
        goto repeat;
    }
    return ret;
}
ret & ~1 마스킹의 의미: raw_read_seqcount_begin()raw_read_seqcount()가 반환한 값에 & ~1을 적용합니다. raw_read_seqcount()는 이미 짝수만 반환하므로 실질적으로 no-op이지만, raw_seqcount_begin()과 같은 변형에서는 홀수 대기 없이 즉시 반환하면서 짝수로 마스크합니다. 이는 read_seqcount_retry()가 시퀀스 불일치를 확실히 감지하도록 보장합니다.

read_seqcount_retry() 상세

/* read_seqcount_retry() — 데이터 일관성 검증 */
static inline int read_seqcount_retry(const seqcount_t *s, unsigned start)
{
    smp_rmb();  /* ① 데이터 읽기가 시퀀스 재읽기보다 먼저 완료 */
    return unlikely(READ_ONCE(s->sequence) != start);
    /*
     * start는 read_seqcount_begin()에서 짝수로 마스크된 값
     * 현재 sequence가 start와 다르면:
     *   - 홀수: 라이터가 쓰기 진행 중
     *   - start+2 이상: 쓰기 완료 후 시퀀스 변경
     * 둘 다 재시도 필요 → true(1) 반환
     */
}
read_seqbegin() / read_seqretry() 호출 체인 읽기 시작 체인 read_seqbegin(sl) read_seqcount_begin(s) +lockdep raw_read_seqcount_begin(s) raw_read_seqcount(s) smp_rmb() seq & 1 == 1? cpu_relax() + goto ret & ~1 읽기 종료 체인 read_seqretry(sl, start) read_seqcount_retry(s, start) smp_rmb() READ_ONCE(s->sequence) != start? 0 (성공) 1 (재시도) 같음 다름 배리어 쌍 요약 read_seqcount_begin: smp_rmb() → 시퀀스 읽기가 데이터 읽기보다 먼저 관측됨을 보장 read_seqcount_retry: smp_rmb() → 데이터 읽기가 시퀀스 재검증보다 먼저 완료됨을 보장
리더 경로는 두 개의 smp_rmb()로 시퀀스-데이터-시퀀스 읽기 순서를 강제합니다

KCSAN seqlock 영역 처리

/* read_seqbegin()이 호출하는 KCSAN 힌트 */
kcsan_atomic_next(KCSAN_SEQLOCK_REGION_MAX);
/*
 * KCSAN(Kernel Concurrency Sanitizer)에게
 * "다음 KCSAN_SEQLOCK_REGION_MAX번의 메모리 접근은
 *  seqlock으로 보호되는 원자적 영역이므로
 *  데이터 레이스로 보고하지 말라"고 알립니다.
 *
 * seqlock 리더는 의도적으로 라이터와 동시에
 * 데이터를 읽을 수 있으므로, KCSAN의 기본
 * 레이스 감지를 억제해야 합니다.
 */
READ_ONCE의 필수성: raw_read_seqcount()에서 READ_ONCE(s->sequence)를 사용하는 이유는 컴파일러가 시퀀스 읽기를 최적화(캐싱, 재정렬)하지 못하게 방지하기 위함입니다. volatile 읽기 없이 일반 로드를 사용하면, 컴파일러가 루프 불변 코드 이동(LICM)으로 시퀀스 값을 루프 밖으로 끌어올려 무한 루프에 빠질 수 있습니다.

write_seqlock/write_sequnlock 소스 분석

seqlock 라이터 경로는 spinlock 획득, 시퀀스 증가, 메모리 배리어 삽입이라는 세 단계로 구성됩니다. 각 단계의 구현을 분석합니다.

write_seqlock() 구현

/* include/linux/seqlock.h — write_seqlock() */
static inline void write_seqlock(seqlock_t *sl)
{
    spin_lock(&sl->lock);       /* ① spinlock 획득 (라이터 직렬화) */
    write_seqcount_begin(&sl->seqcount);
    /* 이후 시퀀스는 홀수 — 리더에게 "쓰기 중" 신호 */
}

/* write_seqcount_begin() 상세 */
static inline void write_seqcount_begin(seqcount_t *s)
{
    write_seqcount_begin_nested(s, 0);
}

static inline void write_seqcount_begin_nested(seqcount_t *s, int subclass)
{
    raw_write_seqcount_begin(s);
    seqcount_acquire(&s->dep_map, subclass, 0, _RET_IP_);
    /* lockdep: 잠금 의존성 추적 */
}

/* raw_write_seqcount_begin() — 핵심 */
static inline void raw_write_seqcount_begin(seqcount_t *s)
{
    kcsan_nestable_atomic_begin();
    s->sequence++;              /* even→odd: 쓰기 시작 */
    smp_wmb();                  /* ② 시퀀스 증가가 데이터 쓰기 전에 관측됨 보장 */
}

write_sequnlock() 구현

/* write_sequnlock() */
static inline void write_sequnlock(seqlock_t *sl)
{
    write_seqcount_end(&sl->seqcount);
    spin_unlock(&sl->lock);      /* ④ spinlock 해제 */
}

/* write_seqcount_end() 상세 */
static inline void write_seqcount_end(seqcount_t *s)
{
    seqcount_release(&s->dep_map, _RET_IP_);
    raw_write_seqcount_end(s);
}

/* raw_write_seqcount_end() — 핵심 */
static inline void raw_write_seqcount_end(seqcount_t *s)
{
    smp_wmb();                  /* ③ 데이터 쓰기가 시퀀스 증가 전에 완료됨 보장 */
    s->sequence++;              /* odd→even: 쓰기 완료 */
    kcsan_nestable_atomic_end();
}

write_seqcount_invalidate()

/* 시퀀스를 홀수로 만들어 모든 리더 무효화 */
static inline void write_seqcount_invalidate(seqcount_t *s)
{
    smp_wmb();
    kcsan_nestable_atomic_begin();
    s->sequence += 2;          /* 짝수→짝수: begin+end 를 한 번에 */
    kcsan_nestable_atomic_end();
}
/*
 * 주의: 이 함수는 sequence를 2 증가시켜 짝수를 유지하지만,
 * begin/end 쌍 없이 시퀀스를 변경합니다.
 * 현재 리더의 start 값과 불일치를 유발하여
 * 모든 진행 중인 읽기를 재시도시킵니다.
 */
sequence 필드의 비원자적 증가: s->sequence++는 원자적 연산이 아닙니다. 라이터 직렬화(spinlock)가 보장되므로 동시에 두 라이터가 시퀀스를 수정하지 않습니다. 리더는 READ_ONCE()로 시퀀스를 읽으므로 torn read 위험은 없습니다. 이 비원자적 증가가 seqlock 라이터 비용을 atomic_inc() 대비 절약합니다.
함수시퀀스 변화배리어추가 동작
raw_write_seqcount_begin()even→odd (+1)시퀀스++ 후 smp_wmb()KCSAN atomic begin
raw_write_seqcount_end()odd→even (+1)smp_wmb() 후 시퀀스++KCSAN atomic end
write_seqcount_invalidate()even→even (+2)smp_wmb() 후 시퀀스+=2진행 중 리더 무효화
raw_write_seqcount_barrier()even→even (+2)2개 smp_wmb()latch 패턴용

x86: 배리어와 torn read

x86 아키텍처의 TSO(Total Store Ordering) 메모리 모델은 seqlock 구현에 상당한 최적화 기회를 제공합니다. 그러나 32비트 x86에서의 torn read 문제는 seqlock이 존재하는 핵심 이유 중 하나입니다.

TSO 모델과 seqlock 배리어

/* arch/x86/include/asm/barrier.h */

/* x86 TSO 모델에서 smp_wmb()의 실제 구현 */
#define smp_wmb()   barrier()
/*
 * TSO(Total Store Ordering) 보장:
 *   - 모든 스토어는 프로그램 순서대로 다른 CPU에 관측됨
 *   - 따라서 smp_wmb()는 컴파일러 배리어만으로 충분
 *   - 하드웨어 MFENCE/SFENCE 불필요
 */

/* smp_rmb()의 x86 구현 */
#define smp_rmb()   barrier()
/*
 * x86 TSO: 로드-로드 순서도 하드웨어가 보장
 * 컴파일러가 재정렬하지 않으면 충분
 * → barrier() = asm volatile("" ::: "memory")
 */

/* 결과: x86에서 seqlock 리더의 실제 비용 */
/*
 * read_seqcount_begin():
 *   READ_ONCE(s->sequence)  → MOV 명령어 1개
 *   smp_rmb()               → 컴파일러 배리어만 (명령어 0개)
 *
 * read_seqcount_retry():
 *   smp_rmb()               → 컴파일러 배리어만 (명령어 0개)
 *   READ_ONCE(s->sequence)  → MOV 명령어 1개
 *   비교 + 분기              → CMP + JNE
 *
 * 총 비용: MOV 2개 + CMP + JNE ≈ 8-12 cycles
 */
x86 TSO: seqlock 배리어가 컴파일러 배리어로 축소 x86 (TSO) smp_wmb() → barrier() (컴파일러만) smp_rmb() → barrier() (컴파일러만) 하드웨어가 로드-로드, 스토어-스토어 순서 보장 → 추가 CPU 명령어 불필요 리더 비용: ~8-12 cycles ARM64 (약한 순서) smp_wmb() → DMB ISHST (하드웨어) smp_rmb() → DMB ISHLD (하드웨어) 약한 메모리 모델 → 하드웨어 배리어 필수 → 각 배리어가 10-40 cycle 소요 리더 비용: ~30-60 cycles 32비트 x86: torn read 시나리오 jiffies_64 = 0x00000000_FFFFFFFF 에서 0x00000001_00000000 으로 증가 시: MOV eax,[low32] → FF..FF ← IRQ: jiffies++ MOV edx,[high32] → 0x01 결과: edx:eax = 0x00000001_FFFFFFFF (실제: 0x00000001_00000000) — 4GB 오차! seqlock: 시퀀스 불일치 감지 → 재시도 → 올바른 값 반환
x86 TSO는 seqlock 배리어를 컴파일러 배리어로 축소하지만, 32비트에서는 torn read 방지를 위해 seqlock이 필수입니다

LFENCE의 역할

/* x86에서 LFENCE는 seqlock에 사용되지 않음 */
/*
 * LFENCE는 로드-직렬화 배리어이지만, TSO에서는
 * 로드-로드 순서가 이미 보장되므로 smp_rmb()에 불필요.
 *
 * LFENCE가 필요한 경우:
 *   - RDTSC 직렬화 (lfence; rdtsc)
 *   - Spectre v1 완화 (투기적 실행 차단)
 *   - MMIO 로드 순서 (비캐시 영역)
 *
 * seqlock 리더에서 LFENCE를 쓰면 불필요한 성능 손실.
 * barrier()로 충분한 x86 TSO에서 LFENCE는 과잉.
 */

/* 비교: rmb() vs smp_rmb() */
#define rmb()      asm volatile("lfence" ::: "memory")
#define smp_rmb()  barrier()
/*
 * rmb(): MMIO/디바이스 메모리용 (LFENCE 하드웨어 배리어)
 * smp_rmb(): SMP 간 일반 메모리용 (컴파일러 배리어 충분)
 * seqlock은 일반 메모리 → smp_rmb() 사용
 */
64비트 x86에서 seqlock이 여전히 필요한 이유: 64비트 x86에서는 u64 로드가 원자적이므로 torn read 문제가 없습니다. 그러나 seqlock은 다중 필드 일관성에도 사용됩니다. 예를 들어 struct timekeeperbasensec를 일관되게 읽어야 할 때, 두 필드 사이에 업데이트가 끼어들면 불일관 상태가 됩니다. 이것은 64비트 원자성과 무관한 문제입니다.

ARM64: DMB와 약한 순서

ARM64의 약한 메모리 모델(weakly ordered)에서는 seqlock 배리어가 실제 하드웨어 명령어로 확장되어 x86 대비 상당한 비용이 추가됩니다. 그러나 ARM64의 load-acquire/store-release 명령어를 활용한 최적화도 가능합니다.

DMB 배리어 변형

/* arch/arm64/include/asm/barrier.h */

/* ARM64에서 smp_rmb() 구현 */
#define smp_rmb()   dmb(ishld)
/*
 * DMB ISHLD (Data Memory Barrier, Inner Shareable, Load)
 *   - 이 명령어 이전의 모든 로드가
 *     이후의 로드/스토어 전에 완료됨을 보장
 *   - Inner Shareable: 같은 공유 도메인 내 CPU 간 적용
 *   - Load only: 로드-로드, 로드-스토어 순서만 강제
 */

/* ARM64에서 smp_wmb() 구현 */
#define smp_wmb()   dmb(ishst)
/*
 * DMB ISHST (Data Memory Barrier, Inner Shareable, Store)
 *   - 이 명령어 이전의 모든 스토어가
 *     이후의 스토어 전에 완료됨을 보장
 *   - 스토어-스토어 순서만 강제 (가장 가벼운 DMB)
 */

/* seqlock 리더의 ARM64 어셈블리 (개념적) */
/*
 * read_seqcount_begin:
 *   LDAR w0, [x1]        ; load-acquire sequence (또는 LDR + DMB)
 *   DMB ISHLD             ; smp_rmb() — 시퀀스→데이터 순서
 *
 * 데이터 읽기...
 *
 * read_seqcount_retry:
 *   DMB ISHLD             ; smp_rmb() — 데이터→시퀀스 순서
 *   LDR w0, [x1]          ; READ_ONCE(s->sequence)
 *   CMP w0, w2            ; 비교
 *   B.NE retry            ; 불일치 시 재시도
 */

load-acquire 최적화

/* ARM64의 load-acquire를 활용한 최적화 */
/*
 * smp_load_acquire()는 ARM64에서 LDAR 명령어 사용:
 *   LDAR w0, [x1]
 *   → load + acquire 시맨틱 = LDR + DMB ISHLD 와 동일 효과
 *   → 하지만 단일 명령어로 더 효율적
 *
 * 현재 커널의 read_seqcount_begin()은
 * READ_ONCE() + smp_rmb() 패턴을 사용하지만,
 * 일부 아키텍처 최적화에서는 이를
 * smp_load_acquire()로 대체 가능합니다.
 */

/* DMB 비용 비교 */
/*
 * DMB ISH   (full): ~40-60 cycles (Cortex-A7x 기준)
 * DMB ISHLD (load): ~20-40 cycles
 * DMB ISHST (store): ~15-30 cycles
 * LDAR (acquire):    ~5-10 cycles (추가 비용)
 *
 * seqlock 리더는 smp_rmb() 2회 = DMB ISHLD 2회
 * → ARM64에서 ~40-80 cycles 추가 비용
 * → x86의 ~0 cycles 와 대조적
 */

약한 순서에서의 함정

/* ARM64에서 배리어 누락 시 발생할 수 있는 문제 */

/* 위험한 코드: smp_rmb() 없이 직접 구현 시도 */
unsigned seq = s->sequence;  /* ❌ READ_ONCE 없음 */
val = shared_data;           /* ❌ 배리어 없음 */
if (s->sequence != seq) ...  /* ❌ 재정렬 가능! */
/*
 * ARM64에서 발생 가능한 시나리오:
 * 1. CPU가 shared_data를 sequence 읽기 전에 투기적 로드
 * 2. sequence가 짝수(일관)임을 확인하지만,
 *    shared_data는 이전 불일관 값
 * 3. 결과: 불일관 데이터를 일관된 것으로 오판
 *
 * → smp_rmb()는 이 투기적 로드를 차단합니다
 */
RISC-V 비교: RISC-V도 약한 메모리 모델을 사용하며, smp_rmb()fence r,r, smp_wmb()fence w,w로 확장됩니다. ARM64의 DMB와 유사하게 실제 하드웨어 비용이 발생합니다.

vDSO와 seqlock: clock_gettime 심층

vDSO(virtual Dynamic Shared Object)는 커널 데이터를 사용자 공간(User Space)에 매핑(Mapping)하여 시스템 콜(System Call) 없이 시간을 읽게 합니다. 이 매핑된 데이터의 일관성은 seqcount로 보호됩니다. 커널 ↔ 사용자 공간 경계를 넘는 seqlock 패턴을 심층 분석합니다.

vDSO 데이터 페이지(Page) 구조

/* include/vdso/datapage.h */
struct vdso_data {
    u32                 seq;         /* seqcount — 커널이 쓰고 유저가 읽음 */
    s32                 clock_mode;  /* vDSO 사용 가능 여부 */
    u64                 cycle_last;
    u64                 mask;
    u32                 mult;
    u32                 shift;
    union {
        struct vdso_timestamp basetime[VDSO_BASES];
        struct timens_offset timens_offset[VDSO_BASES];
    };
    s32                 tz_minuteswest;
    s32                 tz_dsttime;
    u32                 hrtimer_res;
};

struct vdso_timestamp {
    u64                 sec;
    u64                 nsec;
};

사용자 공간 읽기 루프

/* lib/vdso/gettimeofday.c — 사용자 공간에서 실행 */
static __always_inline int do_hres(
    const struct vdso_data *vd,
    clockid_t clk,
    struct __kernel_timespec *ts)
{
    const struct vdso_timestamp *vdso_ts = &vd->basetime[clk];
    u64 cycles, sec, ns;
    u32 seq;

    do {
        /* ① 시퀀스 읽기 — 커널과 같은 seqcount 프로토콜 */
        seq = vdso_read_begin(vd);

        /* ② 하드웨어 카운터 읽기 (RDTSC, CNTVCT_EL0 등) */
        if (unlikely(vd->clock_mode == VDSO_CLOCKMODE_NONE))
            return -1; /* fallback to syscall */

        cycles = __arch_get_hw_counter(vd->clock_mode, vd);

        /* ③ 나노초 계산: (cycles - cycle_last) * mult >> shift */
        ns = vdso_calc_ns(vd, cycles, vdso_ts->nsec);
        sec = vdso_ts->sec;

    } while (unlikely(vdso_read_retry(vd, seq)));
    /* ④ 시퀀스 불일치 → 커널이 vdso_data를 업데이트 중 → 재시도 */

    ts->tv_sec = sec;
    ts->tv_nsec = ns;
    return 0;
}

/* vdso_read_begin/retry — seqcount 프로토콜 동일 */
static __always_inline u32 vdso_read_begin(const struct vdso_data *vd)
{
    u32 seq;
repeat:
    seq = READ_ONCE(vd->seq);
    if (unlikely(seq & 1)) {
        cpu_relax();
        goto repeat;
    }
    smp_rmb();
    return seq;
}
vDSO clock_gettime: 커널-유저 seqcount 프로토콜 커널 공간 (라이터) timer IRQ → update_wall_time() write_seqcount_begin(&tk_core.seq) tk->tkr_mono.base 업데이트 NTP 보정 적용 cycle_last, mult, shift 갱신 write_seqcount_end(&tk_core.seq) update_vsyscall(tk) vdso_data에 복사 + seq 동기화 vdso_data 페이지 (공유 매핑) seq | clock_mode | mult | shift basetime[].sec/.nsec | cycle_last mmap 사용자 공간 (리더) clock_gettime(CLOCK_REALTIME, &ts) vdso_read_begin(vd) → seq RDTSC / CNTVCT_EL0 (HW 카운터) ns = (cycles - cycle_last) * mult >> shift sec = basetime[clk].sec vdso_read_retry(vd, seq)? 재시도 ts = {sec, ns} 반환 (syscall 없음) 비용: ~20ns (vs syscall ~200ns) 10배 성능 향상, 컨텍스트 스위치 없음
커널이 seqcount로 보호하며 업데이트한 vdso_data를 사용자 공간이 동일한 seqcount 프로토콜로 읽습니다

커널 업데이트 경로

/* arch/x86/kernel/vsyscall_gtod.c */
void update_vsyscall(struct timekeeper *tk)
{
    struct vdso_data *vdata = __arch_get_k_vdso_data();

    /* vdso_data의 시퀀스도 같이 증가 */
    vdso_write_begin(vdata);

    /* timekeeper → vdso_data 복사 */
    vdata[CS_HRES_COARSE]->basetime[CLOCK_REALTIME].sec = tk->xtime_sec;
    vdata[CS_HRES_COARSE]->basetime[CLOCK_REALTIME].nsec = tk->tkr_mono.xtime_nsec;
    vdata->mult = tk->tkr_mono.mult;
    vdata->shift = tk->tkr_mono.shift;
    vdata->cycle_last = tk->tkr_mono.cycle_last;

    vdso_write_end(vdata);
}
vDSO fallback: clock_mode == VDSO_CLOCKMODE_NONE이면 vDSO가 비활성화되어 일반 시스템 콜로 폴백합니다. 이는 TSC가 불안정하거나(nonstop_tsc 미지원), 가상화(Virtualization) 환경에서 TSC가 신뢰할 수 없을 때 발생합니다. 폴백 시 성능이 ~10배 저하됩니다.

seqcount_latch 소스 분석

seqcount_latch_t는 이중 버퍼(double buffering)를 활용하여 쓰기 중에도 항상 일관된 읽기를 보장하는 특수 패턴입니다. NMI처럼 재시도 루프가 위험한 컨텍스트에서 안전합니다. 내부 구현을 소스 수준으로 분석합니다.

raw_write_seqcount_latch() 상세

/* include/linux/seqlock.h */

/* latch 쓰기: 시퀀스 증가 + 배리어 (한 번 호출당 1 증가) */
static inline void raw_write_seqcount_latch(seqcount_latch_t *s)
{
    smp_wmb();        /* 이전 버퍼 쓰기가 seq 증가 전에 완료 */
    s->seqcount.sequence++;
    smp_wmb();        /* seq 증가가 다음 버퍼 쓰기 전에 관측 */
}

/*
 * latch 쓰기 프로토콜 (라이터가 2번 호출):
 *
 * raw_write_seqcount_latch(&latch);  // seq: 0→1
 * data[0] = new_value;                // 첫 번째 버퍼 업데이트
 *
 * raw_write_seqcount_latch(&latch);  // seq: 1→2
 * data[1] = new_value;                // 두 번째 버퍼 업데이트
 *
 * 핵심: seq이 홀수일 때 → 리더는 data[1] 사용 (아직 미변경)
 *       seq이 짝수일 때 → 리더는 data[0] 사용 (이미 변경 완료)
 *       → 어느 시점에든 하나의 버퍼는 항상 일관된 상태
 */

raw_read_seqcount_latch() 상세

/* latch 읽기 — NMI-safe, 항상 일관된 데이터 보장 */
static inline unsigned raw_read_seqcount_latch(seqcount_latch_t *s)
{
    /*
     * 시퀀스를 읽고, LSB로 어떤 버퍼를 읽을지 결정:
     *   seq & 1 == 0 → data[0] 읽기 (라이터가 data[1] 수정 중이거나 유휴)
     *   seq & 1 == 1 → data[1] 읽기 (라이터가 data[0] 수정 중)
     *
     * 주의: 일반 seqlock과 달리 홀수 시 스핀하지 않음!
     * 홀수여도 안전한 버퍼가 존재하기 때문
     */
    return READ_ONCE(s->seqcount.sequence);
}

/* latch 읽기 재시도 판정 */
static inline int raw_read_seqcount_latch_retry(seqcount_latch_t *s, unsigned start)
{
    smp_rmb();
    return unlikely(READ_ONCE(s->seqcount.sequence) != start);
    /*
     * 재시도가 필요한 경우:
     *   읽기 중 라이터가 두 번째 latch(같은 버퍼 업데이트)를 실행
     *   → 리더가 읽던 버퍼가 수정됨
     *
     * 재시도해도 항상 성공: 재시도 시 seq이 안정되어
     * 안전한 버퍼를 다시 선택
     */
}
seqcount_latch 버퍼 선택 메커니즘 seq=0 (유휴) data[0]: 유효 V1 data[1]: 유효 V1 리더 → data[seq&1]=data[0] latch() seq=1 (쓰기 중) data[0]: 수정 중 V2 data[1]: 유효 V1 리더 → data[seq&1]=data[1] (안전!) latch() seq=2 (쓰기 중) data[0]: 유효 V2 data[1]: 수정 중 V2 리더 → data[seq&1]=data[0] (안전!) seq=2 (완료) data[0]: 유효 V2 data[1]: 유효 V2 리더 → data[0] NMI 안전성 시나리오 1. 프로세스 컨텍스트에서 latch 쓰기 시작 (seq: 0→1) 2. NMI 발생! → NMI 핸들러가 데이터 읽기 필요 3. raw_read_seqcount_latch() → seq=1 → data[1] 선택 (수정되지 않은 버퍼) 4. data[1]은 V1 값으로 완전히 일관됨 → 안전하게 사용 일반 seqlock으로 NMI를 처리하면? 1. NMI에서 read_seqbegin() → seq=1 (홀수) → 스핀 대기 2. 라이터도 같은 CPU! → 라이터가 진행 못함 → NMI도 진행 못함 3. DEADLOCK! (같은 CPU에서 라이터-리더 교착)
latch 패턴은 리더가 항상 안전한 버퍼를 선택하므로 NMI에서도 교착 없이 동작합니다

NMI-safe 사용 사례: perf 이벤트

/* kernel/events/core.c — perf 이벤트 시간 변환 */
struct perf_event_mmap_page {
    /* latch로 보호되는 시간 변환 파라미터 */
    u32     time_mult;
    u32     time_shift;
    u64     time_offset;
};

/*
 * perf의 NMI 핸들러에서 시간 변환 파라미터를 읽어야 함
 * → 일반 seqlock 불가 (NMI에서 스핀하면 교착)
 * → seqcount_latch 사용으로 항상 일관된 읽기 보장
 */
latch vs 일반 seqcount 선택 기준: NMI/하드IRQ에서 읽어야 하고, 라이터가 같은 CPU에서 실행될 수 있다면 seqcount_latch_t를 사용하세요. 일반 컨텍스트에서만 읽고 라이터 간섭이 짧다면 seqcount_t/seqlock_t가 메모리를 절약합니다 (이중 버퍼 불필요).

벤치마크: 읽기 재시도율과 처리량(Throughput)

seqlock의 실제 성능을 정량적으로 분석합니다. 리더 재시도율은 라이터 빈도와 임계 영역(Critical Section) 길이에 직접 의존하며, 이를 수학적으로 모델링할 수 있습니다.

재시도 확률 모델

/* 재시도 확률 수학 모델 */

P(retry) = P(라이터가 리더의 임계 영역과 겹침)

가정:
  T_r = 리더 임계 영역 시간 (시퀀스 읽기 ~ 재검증)
  T_w = 라이터 임계 영역 시간 (seqlock 보유 시간)
  f_w = 라이터 빈도 (writes/second)

단일 라이터, 포아송 도착 근사:

  P(retry) ≈ 1 - e^(-f_w × (T_r + T_w))

  f_w가 작을 때 근사: P(retry) ≈ f_w × (T_r + T_w)

예시 (jiffies):
  T_r ≈ 10ns (시퀀스 읽기 2회 + 데이터 읽기)
  T_w ≈ 5ns  (jiffies_64 증가)
  f_w = 1000 Hz (CONFIG_HZ=1000)

  P(retry) ≈ 1000 × (10ns + 5ns) = 0.000015 = 0.0015%
  → 65,000번 읽기당 1회 재시도

seqlock vs rwlock vs RCU 처리량 비교

시나리오seqlock (Mops/s)rwlock (Mops/s)RCU (Mops/s)비고
리더 4코어, 라이터 0850120950seqlock ≈ RCU, rwlock 원자적 비용
리더 4코어, 라이터 1Hz850120950차이 미미
리더 4코어, 라이터 1KHz848118950seqlock 재시도 0.001%
리더 4코어, 라이터 1MHz68095940seqlock 재시도 ~1.5%
리더 4코어, 라이터 10MHz22040920seqlock 재시도 ~15%
리더 4코어, 라이터 100MHz308890seqlock 사실상 사용 불가
측정 조건: x86_64, Intel Xeon, 4.0GHz, 8MB L3, CONFIG_HZ=1000. 읽기 데이터: 64바이트 구조체. 라이터: 1코어 전용. 수치는 대표적 벤치마크 결과이며 환경에 따라 변동합니다.
쓰기 빈도별 리더 처리량 비교 (4 reader cores) 리더 처리량 (Mops/s) 0 250 500 750 1000 쓰기 빈도 (writes/sec) 1 1K 10K 100K 1M 10M RCU seqlock rwlock 전형적 커널 사용 범위 (1~10KHz) 쓰기 1MHz 이상: seqlock 처리량 급감 → per-CPU 분산 또는 RCU 전환 RCU: 쓰기 빈도 무관 seqlock: 저빈도 시 우수 rwlock: 기본 비용 높음
쓰기 빈도가 낮으면 seqlock이 rwlock보다 훨씬 효율적이지만, 쓰기가 빈번해지면 RCU가 압도적입니다

캐시 라인 분석

동기화읽기 시 캐시(Cache) 상태쓰기 시 캐시 영향리더 간 간섭
seqlockShared (sequence 필드)1회 Invalidate (sequence 수정)없음 (읽기만)
rwlockExclusive (cnts 원자적 증감)리더 수만큼 bounce있음 (원자적 RMW)
RCU없음 (선점 비활성화만)grace period 비용없음
NUMA 시스템에서의 차이: NUMA에서 rwlock의 원자적 cnts 증감은 원격 노드 캐시 라인 전송을 유발하여 리더 비용이 100+ cycles로 증가합니다. seqlock 리더는 sequence를 읽기만 하므로 Shared 상태를 유지하여 NUMA 환경에서 특히 유리합니다.

서브시스템: 타임키핑 심층

Linux 커널 타임키핑은 seqlock/seqcount의 가장 정교한 사용 사례입니다. tk_core 구조체를 중심으로 타이머 인터럽트부터 ktime_get_real()까지의 전체 경로를 분석합니다.

tk_core 구조체

/* kernel/time/timekeeping.c */
static struct {
    seqcount_raw_spinlock_t  seq;
    struct timekeeper        timekeeper;
} tk_core ____cacheline_aligned = {
    .seq = SEQCNT_RAW_SPINLOCK_ZERO(tk_core.seq, &timekeeper_lock),
};

/* 왜 seqcount_raw_spinlock_t 인가?
 *   - raw_spinlock_t는 PREEMPT_RT에서도 진정한 스핀
 *   - 타이머 인터럽트 컨텍스트에서 호출 → rt_mutex 사용 불가
 *   - seqcount_spinlock_t를 사용하면 RT에서 문제 발생
 */

/* struct timekeeper 핵심 필드 */
struct timekeeper {
    struct tk_read_base  tkr_mono;    /* CLOCK_MONOTONIC */
    struct tk_read_base  tkr_raw;     /* CLOCK_MONOTONIC_RAW */
    u64                 xtime_sec;   /* CLOCK_REALTIME 초 */
    unsigned long       ktime_sec;   /* CLOCK_MONOTONIC 초 */
    struct timespec64   wall_to_monotonic;
    ktime_t             offs_real;   /* mono → real 오프셋 */
    ktime_t             offs_boot;   /* mono → boot 오프셋 */
    ktime_t             offs_tai;    /* mono → TAI 오프셋 */
    /* ... NTP 보정 관련 필드들 ... */
};

struct tk_read_base {
    struct clocksource  *clock;      /* 하드웨어 클록 소스 */
    u64                 mask;
    u64                 cycle_last;  /* 마지막 업데이트 시 사이클 */
    u32                 mult;        /* 사이클→나노초 변환 승수 */
    u32                 shift;       /* 사이클→나노초 변환 시프트 */
    u64                 xtime_nsec;  /* 기준 나노초 */
    ktime_t             base;        /* 기준 ktime */
};

update_wall_time() 경로

/* kernel/time/timekeeping.c — 타이머 인터럽트에서 호출 */
void update_wall_time(void)
{
    struct timekeeper *real_tk = &tk_core.timekeeper;

    raw_spin_lock_irqsave(&timekeeper_lock, flags);

    /* 1. 클록 소스에서 경과 사이클 계산 */
    clock = real_tk->tkr_mono.clock;
    cycle_now = clock->read(clock);
    delta = clocksource_delta(cycle_now, real_tk->tkr_mono.cycle_last, clock->mask);

    /* 2. seqcount 보호 시작 */
    write_seqcount_begin(&tk_core.seq);

    /* 3. timekeeper 업데이트 */
    timekeeping_advance(real_tk, delta);
    /*
     * cycle_last 갱신
     * xtime_nsec 누적
     * NTP freq/offset 보정 적용
     * 초 단위 넘어가면 xtime_sec++
     * wall_to_monotonic 갱신
     */

    /* 4. seqcount 보호 종료 */
    write_seqcount_end(&tk_core.seq);

    /* 5. vDSO 데이터 페이지 업데이트 */
    update_vsyscall(real_tk);

    raw_spin_unlock_irqrestore(&timekeeper_lock, flags);
}
타임키핑 seqcount 보호 아키텍처 하드웨어 클록 소스 TSC (x86) CNTVCT_EL0 (ARM64) HPET ACPI PM Timer Timer IRQ (tick) update_wall_time() seqcount_begin update TK NTP seqcount_end update_vsyscall() ktime_get() seqcount_retry 루프 커널 내부 호출 vDSO clock_gettime() 유저 공간 seqcount 루프 syscall 없음, ~20ns NMI/perf 시간 seqcount_latch 사용 이중 버퍼, 교착 없음 업데이트 빈도: CONFIG_HZ (100/250/1000 Hz) | 리더: 수백만 회/초 | 재시도율: 0.001% 미만
타임키핑은 하드웨어 클록에서 사용자 공간까지 seqcount가 일관된 시간 데이터를 보호합니다

ktime_get_real() 경로

/* kernel/time/timekeeping.c */
ktime_t ktime_get_real(void)
{
    struct timekeeper *tk = &tk_core.timekeeper;
    unsigned seq;
    ktime_t base;
    u64 nsecs;

    do {
        seq = read_seqcount_begin(&tk_core.seq);
        base = tk->tkr_mono.base;
        nsecs = timekeeping_get_ns(&tk->tkr_mono);
        base = ktime_add(base, tk->offs_real);
    } while (read_seqcount_retry(&tk_core.seq, seq));

    return ktime_add_ns(base, nsecs);
}

/* timekeeping_get_ns() — 마지막 업데이트 이후 경과 나노초 */
static inline u64 timekeeping_get_ns(struct tk_read_base *tkr)
{
    u64 delta, nsec;

    /* 현재 하드웨어 사이클 - 마지막 업데이트 사이클 */
    delta = timekeeping_get_delta(tkr);

    /* 사이클 → 나노초 변환: delta * mult >> shift */
    nsec = (delta * tkr->mult + tkr->xtime_nsec) >> tkr->shift;

    return nsec;
}
NTP 보정과 seqlock: update_wall_time()은 NTP 데몬에서 받은 주파수/오프셋(Offset) 보정을 mult 파라미터에 반영합니다. 이 보정은 seqcount 보호 영역 내에서 원자적으로 적용되므로, 리더가 보정 전후의 혼합된 파라미터로 시간을 계산하는 것을 방지합니다.

커널 버전별 진화

seqlock은 v2.5에서 최초 도입된 이후 지속적으로 진화했습니다. 각 버전에서의 핵심 변화와 그 배경을 분석합니다.

seqlock/seqcount 커널 버전별 진화 v2.5.28 2002 seqlock 최초 도입 seqlock_t, read_seqbegin jiffies 보호 주 목적 v2.6 2003 seqcount_t 분리 시퀀스 카운터만 분리 외부 잠금 패턴 지원 v3.x 2011-2014 latch 패턴 추가 seqcount_latch 도입 NMI-safe 이중 버퍼 v4.x 2015-2019 vDSO 확장 seqcount in vDSO data 유저 공간 seqlock v5.8-5.10 2020 대규모 리팩터링 seqcount_LOCKNAME_t lockdep 연관 잠금 PREEMPT_RT 대비 v6.x 2022+ KCSAN 통합 kcsan_atomic_next 데이터 레이스 검증 핵심 변화 상세 v5.8 (Ahmed S. Darwish 리팩터링): - seqcount_t를 5개 타입 변형으로 분리 (spinlock/raw_spinlock/rwlock/mutex/ww_mutex) - lockdep 자동 검증: write_seqcount_begin() 시 연관 잠금 보유 확인 - 이전: "약속"에 의존 → 이후: 컴파일 타임 + 런타임 강제 v5.10 (latch 정리): - seqcount_latch_t 전용 타입 도입 (이전: seqcount_t 재활용) - raw_read_seqcount_latch_retry() 전용 API 추가 - latched_seq 헬퍼 구조체 추가 PREEMPT_RT 대비 (v5.8+): - seqcount_spinlock_t: RT에서 spinlock→rt_mutex 시 라이터 선점 가능 - seqcount_raw_spinlock_t: RT에서도 진정한 스핀 보장 (timekeeper용) - 타입 시스템으로 "어떤 잠금이 RT에서 안전한가" 명시적 표현
seqlock은 20년간 단순 카운터에서 타입 안전 연관 잠금 시스템으로 진화했습니다

v5.8 리팩터링 상세

/* v5.8 이전: 단일 seqcount_t, 연관 잠금은 "약속" */
seqcount_t my_seq;
spinlock_t my_lock;  /* 문서에만 연관 명시, 컴파일러/lockdep 검증 없음 */

/* v5.8 이후: 타입으로 연관 잠금 명시 */
seqcount_spinlock_t my_seq = SEQCNT_SPINLOCK_ZERO(my_seq, &my_lock);
/*
 * 효과:
 * 1. lockdep이 write_seqcount_begin() 시 my_lock 보유 자동 검증
 * 2. PREEMPT_RT에서 spinlock→rt_mutex 변환 추적
 * 3. 문서화 없이도 코드에서 잠금 관계 명확
 */

/* 매크로 기반 타입 생성 (C 제네릭 에뮬레이션) */
#define SEQCOUNT_LOCKNAME(lockname, locktype, preemptible, lockbase) \
typedef struct seqcount_##lockname {                               \
    seqcount_t seqcount;                                           \
    __SEQ_LOCK(locktype *lock);                                    \
} seqcount_##lockname##_t;

/* 생성된 타입들 */
SEQCOUNT_LOCKNAME(spinlock,     spinlock_t,      true,  spin)
SEQCOUNT_LOCKNAME(raw_spinlock, raw_spinlock_t,  false, raw_spin)
SEQCOUNT_LOCKNAME(rwlock,       rwlock_t,        true,  read)
SEQCOUNT_LOCKNAME(mutex,        struct mutex,    true,  mutex)
SEQCOUNT_LOCKNAME(ww_mutex,     struct ww_mutex, true,  ww_mutex)
버전변화동기영향
v2.5.28seqlock_t 최초 도입jiffies 32비트 torn read 방지spinlock 내장 시퀀스 카운터
v2.6seqcount_t 분리외부 잠금 사용 패턴 지원유연한 라이터 직렬화
v3.xseqcount_latch 도입NMI-safe 읽기 필요이중 버퍼 패턴 공식화
v4.xvDSO seqcount 확장clock_gettime 성능유저 공간 seqcount
v5.8seqcount_LOCKNAME_tlockdep 강화, RT 대비5개 타입 변형, 연관 잠금
v5.10seqcount_latch_t 타입화latch API 정리전용 타입과 API 분리
v6.xKCSAN 통합데이터 레이스 검증seqlock 영역 자동 어노테이션

보안: 타이밍 사이드채널

seqlock의 재시도 루프는 타이밍 사이드채널 공격의 잠재적 벡터가 될 수 있습니다. 리더의 실행 시간이 라이터 활동에 따라 변하므로, 이를 관측하여 시스템 내부 상태를 추론할 수 있습니다.

재시도 루프 타이밍 오라클

/* 공격 시나리오: seqlock 기반 타이밍 추론 */

공격자 목표: 특정 커널 이벤트(타이머 인터럽트)의 정확한 발생 시점 파악

방법:
  1. clock_gettime()을 반복 호출하며 응답 시간 측정
  2. 정상: ~20ns (vDSO fast path)
  3. 재시도 발생: ~40-60ns (seqcount 불일치 → 루프 1회 추가)

  → 재시도가 관측되면:
     "이 시점에 커널이 update_wall_time()을 실행 중"
     = 타이머 인터럽트 발생 시점을 ~10ns 정밀도로 특정

잠재적 악용:
  - 타이머 인터럽트 주기 정밀 측정 → CONFIG_HZ 추론
  - 인터럽트 처리 시간 측정 → 커널 코드 경로 추론
  - 다른 CPU의 라이터 활동 감지 → 워크로드 패턴 분석

시퀀스 경합을 통한 정보 유출

/* 네트워크 통계 seqlock을 통한 트래픽 패턴 추론 */

/*
 * u64_stats_sync (네트워크 통계 seqlock) 공격 시나리오:
 *
 * /proc/net/dev나 netlink를 통해 통계 읽기 시,
 * 32비트 시스템에서 seqcount 재시도가 발생하는 빈도로
 * 패킷 수신 빈도를 간접적으로 추론 가능:
 *
 * retry_rate ≈ f_w × T_r
 *   f_w = 패킷 수신률 (통계 업데이트 빈도)
 *   T_r = 읽기 임계 영역 시간
 *
 * 높은 재시도율 → 높은 패킷 수신률
 * → 네트워크 트래픽 볼륨 추론
 *
 * 실제 위협 수준: 낮음
 *   - 64비트 시스템에서 u64_stats_sync는 no-op
 *   - /proc/net/dev는 이미 통계를 직접 노출
 *   - 컨테이너 환경에서만 의미 (네임스페이스 격리 시)
 */

완화 전략

위협심각도완화 방법상태
vDSO 타이밍 오라클낮음ASLR, 타이머 인터럽트 지터부분 적용
네트워크 통계 경합 추론매우 낮음64비트에서 no-op아키텍처로 해결
timekeeper 업데이트 시점낮음timer_slack, NO_HZ간접 완화
Spectre 투기적 읽기중간리더에 lfence 삽입 (x86)커널 패치(Patch) 적용
/* Spectre v1 완화: 투기적 seqlock 리더 */
/*
 * seqlock 리더에서 투기적 실행 문제:
 *
 * 1. CPU가 read_seqcount_retry() 결과를 예측
 * 2. 예측이 "성공"(no retry)이면 투기적으로 데이터 사용
 * 3. 실제로는 retry가 필요했지만, 투기적 실행 중
 *    불일관 데이터로 캐시 사이드채널 생성
 *
 * 커널 완화:
 *   - 민감한 경로에 speculation_barrier() 추가
 *   - array_index_nospec()으로 배열 인덱스 바운드 강제
 */

do {
    seq = read_seqcount_begin(&s);
    idx = shared_idx;
    val = array[array_index_nospec(idx, ARRAY_SIZE)];
    /* array_index_nospec: 투기적 실행에서도 범위 내 인덱스 보장 */
} while (read_seqcount_retry(&s, seq));
실질적 위험 평가: seqlock 기반 타이밍 사이드채널의 실질적 위험은 낮음입니다. 대부분의 seqlock 보호 데이터(시간, 통계)는 이미 공개 인터페이스로 접근 가능합니다. 그러나 새로운 seqlock 사용처를 설계할 때는, 보호되는 데이터의 접근 시간 변동이 기밀 정보를 유출하지 않는지 검토해야 합니다.
관련 CVE: seqlock 자체의 직접적인 CVE는 보고되지 않았으나, vDSO 관련 CVE-2023-0286 (타이밍 사이드채널)은 vDSO 데이터의 seqcount 경합 패턴과 간접적으로 관련됩니다. timekeeper의 seqcount 경합은 Spectre 변형 공격의 가젯으로 사용될 수 있어, 커널은 투기적 실행(Speculative Execution) 완화를 시간 관련 경로에 적용합니다.

참고 자료

Seqlock과 sequence counter의 설계, 사용 패턴, 성능에 대한 참고 자료입니다.

커널 공식 문서

LWN.net 심층 기사

학술 자료 및 외부 참고

seqlock과 관련된 다른 동기화 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.