Futex (Fast Userspace Mutex)

Futex(Fast Userspace Mutex)는 리눅스 커널이 제공하는 사용자 공간(User Space) 동기화 프리미티브의 핵심 빌딩 블록입니다. 경합(Contention)이 없는 경우(uncontended path) 커널 진입 없이 원자적 연산(Atomic Operation)만으로 잠금(Lock)을 획득/해제하고, 경합이 발생하면(contended path) futex(2) 시스템 콜(System Call)을 통해 커널의 대기 큐(Wait Queue)에서 효율적으로 슬립(Sleep)/웨이크업합니다. 이 문서에서는 kernel/futex/ 디렉터리의 내부 구현, 해시 테이블(Hash Table)과 futex_q 대기 큐, PI(Priority Inheritance) Futex, Robust Futex, FUTEX_WAIT_BITSET, FUTEX_CMP_REQUEUE, NUMA 최적화, futex2/futex_waitv(Linux 5.16+), glibc pthread 매핑(Mapping), Lock Elision, 성능 특성, 디버깅(Debugging), 보안 고려사항, 아키텍처별 차이, PREEMPT_RT 상호작용까지 종합적으로 다룹니다.

전제 조건: 동기화 프리미티브 문서와 원자적 연산 문서를 먼저 읽으세요. Futex는 사용자 공간의 원자적(Atomic) cmpxchg 연산과 커널의 대기 큐를 결합한 메커니즘이므로, 메모리 순서(memory ordering)와 CAS(Compare-And-Swap) 개념 이해가 필수입니다. 메모리 배리어(Memory Barrier) 지식이 있으면 futex 내부의 동기화 프로토콜을 더 깊이 이해할 수 있습니다.
일상 비유: Futex는 화장실 문의 잠금장치에 비유할 수 있습니다. 화장실이 비어 있으면(경합 없음) 문의 손잡이를 돌려 바로 들어갑니다 — 관리자(커널)를 부를 필요가 없습니다. 하지만 이미 누군가 사용 중이면(경합 발생) 대기 줄에 서서 관리자가 “비었습니다”라고 알려줄 때까지 기다립니다. 이 “비어 있으면 즉시 진입, 점유 중이면 대기” 패턴이 바로 futex의 fast path와 slow path입니다.

핵심 요약

Futex(Fast Userspace Mutex)는 Linux 2.5.7(2002년)에 도입된 사용자 공간 동기화 메커니즘입니다. 사용자 공간의 32비트 정수(futex word)와 커널의 대기 큐를 결합하여, 경합이 없는 경우 시스템 콜 오버헤드(Overhead) 없이 동기화를 달성합니다. glibc의 pthread_mutex_lock(), pthread_cond_wait(), sem_wait() 등 대부분의 POSIX 동기화 함수가 내부적으로 futex를 사용합니다. 커널 소스에서는 kernel/futex/ 디렉터리에 구현되어 있습니다.
  • futex word — 사용자 공간의 자연 정렬된 32비트 정수. 프로세스(Process) 간 공유 메모리 또는 스레드(Thread) 간 공유 변수로 사용됩니다.
  • futex_q — 커널 내부의 대기 큐 노드. 슬립 중인 태스크(Task), futex 키, 타임아웃, PI 상태 등을 보유합니다.
  • futex_hash_bucket — futex word의 주소를 해시(Hash)하여 매핑되는 해시 버킷. 각 버킷은 spinlock과 plist로 보호됩니다.
  • futex_key — futex word의 고유 식별자. 프라이빗(per-mm) 또는 공유(inode 기반) futex를 구분합니다.
  • FUTEX_WAIT / FUTEX_WAKE — 가장 기본적인 futex 연산. 각각 슬립과 웨이크업을 수행합니다.
  • PI Futex — 우선순위 역전(Priority Inversion)을 방지하기 위해 rt_mutex를 활용하는 확장입니다.
  • Robust Futex — 잠금 보유자가 비정상 종료해도 교착 상태(Deadlock)를 방지하는 메커니즘입니다.
  • futex_waitv (futex2) — Linux 5.16에 추가된 벡터 대기 인터페이스. 여러 futex word를 동시에 대기할 수 있습니다.
Futex 연산(operation) 전체 목록
연산도입 버전설명
FUTEX_WAIT02.5.7futex word 값이 기대값과 같으면 슬립
FUTEX_WAKE12.5.7대기 중인 태스크를 최대 N개 깨움
FUTEX_FD22.5.7futex에 파일 디스크립터(File Descriptor) 연결 (제거됨, Linux 2.6.26)
FUTEX_REQUEUE32.5.70대기자를 다른 futex로 이동 (안전하지 않음)
FUTEX_CMP_REQUEUE42.6.7값 비교 후 대기자 이동 (안전 버전)
FUTEX_WAKE_OP52.6.14원자적 연산 수행 후 두 futex 깨움
FUTEX_LOCK_PI62.6.18PI 프로토콜 잠금 획득
FUTEX_UNLOCK_PI72.6.18PI 프로토콜 잠금 해제
FUTEX_TRYLOCK_PI82.6.18PI 비차단(Non-blocking) 잠금 시도
FUTEX_WAIT_BITSET92.6.25비트 마스크 기반 선택적 대기
FUTEX_WAKE_BITSET102.6.25비트 마스크 기반 선택적 깨움
FUTEX_WAIT_REQUEUE_PI112.6.31PI futex로의 조건부 requeue 대기
FUTEX_CMP_REQUEUE_PI122.6.31PI futex로의 비교 후 requeue
FUTEX_LOCK_PI2135.14CLOCK_MONOTONIC 지원 PI 잠금
Futex 플래그(flags)
플래그비트설명
FUTEX_PRIVATE_FLAG128 (bit 7)프로세스 내부 전용 futex (공유 메모리 아님). 해시 조회 최적화
FUTEX_CLOCK_REALTIME256 (bit 8)CLOCK_REALTIME 기반 타임아웃 (기본: CLOCK_MONOTONIC)
/* include/uapi/linux/futex.h — 사용자 공간 인터페이스 정의 */
#define FUTEX_WAIT              0
#define FUTEX_WAKE              1
#define FUTEX_FD                2
#define FUTEX_REQUEUE           3
#define FUTEX_CMP_REQUEUE       4
#define FUTEX_WAKE_OP           5
#define FUTEX_LOCK_PI           6
#define FUTEX_UNLOCK_PI         7
#define FUTEX_TRYLOCK_PI        8
#define FUTEX_WAIT_BITSET       9
#define FUTEX_WAKE_BITSET       10
#define FUTEX_WAIT_REQUEUE_PI   11
#define FUTEX_CMP_REQUEUE_PI    12
#define FUTEX_LOCK_PI2          13

#define FUTEX_PRIVATE_FLAG      128
#define FUTEX_CLOCK_REALTIME    256
#define FUTEX_CMD_MASK          ~(FUTEX_PRIVATE_FLAG | FUTEX_CLOCK_REALTIME)

/* 편의 매크로 */
#define FUTEX_WAIT_PRIVATE      (FUTEX_WAIT | FUTEX_PRIVATE_FLAG)
#define FUTEX_WAKE_PRIVATE      (FUTEX_WAKE | FUTEX_PRIVATE_FLAG)
# futex 시스템 콜 추적
$ strace -e futex -p $(pidof my_threaded_app)
futex(0x7f1234567890, FUTEX_WAIT_PRIVATE, 0, NULL) = 0
futex(0x7f1234567890, FUTEX_WAKE_PRIVATE, 1) = 1

# glibc pthread_mutex_lock이 futex를 호출하는지 확인
$ strace -e futex ./mutex_contention_test
futex(0x55a8b234c040, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 2, NULL, FUTEX_BITSET_MATCH_ANY) = 0
futex(0x55a8b234c040, FUTEX_WAKE_PRIVATE, 1) = 1

단계별 이해: Futex 잠금/해제 흐름

Futex의 핵심 동작을 6단계로 분해하여 설명합니다. 사용자 공간의 원자적 연산(fast path)과 커널의 대기 큐(slow path)가 어떻게 협력하는지 이해하는 것이 핵심입니다.

단계 1: 잠금 획득 시도 (Fast Path — 사용자 공간)

스레드 A가 잠금을 획득하려 합니다. futex word의 현재 값이 0(잠금 해제 상태)이면, cmpxchg(&futex_word, 0, tid)를 실행하여 원자적으로 자신의 TID를 기록합니다. 이 과정은 사용자 공간에서 단일 원자적 명령어로 완료되며, 시스템 콜이 필요 없습니다.

/* glibc nptl/lowlevellock.h — 간소화 */
static inline void
__lll_lock(int *futex, int private)
{
    /* Fast path: 0 → 1 (잠금 획득) */
    if (atomic_compare_exchange_weak_acquire(futex, 0, 1))
        return;  /* 성공! 커널 진입 없이 완료 */

    /* Slow path: 이미 잠겨 있으므로 커널에 도움 요청 */
    __lll_lock_wait(futex, private);
}

단계 2: 경합 발생 (Slow Path — 커널 진입)

스레드 B가 같은 잠금을 획득하려 하지만, futex word가 이미 0이 아닙니다. cmpxchg가 실패하면 futex word를 2(대기자 있음)로 설정하고 futex(FUTEX_WAIT, &futex_word, 2, ...) 시스템 콜을 호출합니다.

단계 3: 커널 해시 조회 및 대기 큐 삽입

커널은 futex word의 가상 주소(프라이빗) 또는 물리 페이지(Page)+오프셋(공유)으로 futex_key를 생성합니다. 이 키를 해시하여 futex_hash_bucket을 찾고, futex_q 노드를 생성하여 버킷의 plist에 삽입합니다. 그런 다음 태스크를 TASK_INTERRUPTIBLE로 설정하고 schedule()을 호출합니다.

단계 4: 잠금 해제 (Fast Path — 사용자 공간)

스레드 A가 잠금을 해제합니다. futex word를 atomic_exchange(&futex_word, 0)으로 0으로 설정합니다. 이전 값이 1이었다면(대기자 없음) 커널 진입 없이 완료됩니다. 이전 값이 2이었다면(대기자 있음) futex(FUTEX_WAKE, &futex_word, 1)을 호출합니다.

단계 5: 웨이크업

커널은 해시 버킷에서 해당 futex_key와 일치하는 futex_q를 찾아 대기 중인 태스크를 wake_up_state()으로 깨웁니다. 깨어난 스레드 B는 다시 잠금 획득을 시도합니다.

단계 6: 깨어난 태스크의 잠금 재시도

스레드 B가 깨어나 다시 cmpxchg(&futex_word, 0, 2)를 시도합니다. 성공하면 잠금을 획득하고, 실패하면(다른 스레드가 먼저 획득) 다시 FUTEX_WAIT로 슬립합니다.

사용자 공간 (User Space) 커널 공간 (Kernel Space) ① cmpxchg(&futex, 0, tid) → 성공 ④ atomic_set(&futex, 0) → 대기자 없음 ↑ Fast Path (커널 진입 불필요) ↓ Slow Path (경합 발생 시) ② cmpxchg 실패 → futex = 2 (대기자 표시) syscall ③ futex_key 생성 → 해시 버킷 조회 ③ futex_q 삽입 → schedule() → SLEEP ④ 이전값=2 → FUTEX_WAKE 호출 ⑤ 해시 버킷에서 대기자 찾기 → wake_up return ⑥ 깨어난 스레드: cmpxchg 재시도 Fast Path Slow Path Wake
핵심 통찰: 대부분의 실제 워크로드에서 잠금 경합(Lock Contention)은 드물게 발생합니다. 따라서 futex의 fast path(시스템 콜 없이 cmpxchg 한 번)가 전체 잠금 연산의 90% 이상을 처리하며, 이것이 futex가 다른 커널 동기화 메커니즘 대비 압도적으로 빠른 이유입니다.

Futex 개요와 역사

Futex는 2002년 Hubertus Franke, Matthew Kirkwood, Ingo Molnar, Rusty Russell에 의해 Linux 2.5.7에 도입되었습니다. 그 이전의 리눅스에서 사용자 공간 동기화는 System V 세마포어(semget/semop)나 파일 잠금(flock)에 의존했으며, 이들은 경합 여부와 무관하게 항상 시스템 콜을 요구했습니다.

Futex 이전의 동기화

Linux 동기화 메커니즘 발전사
메커니즘시기경합 없는 경우 비용한계
System V 세마포어Linux 1.0+항상 syscall (수백 ns)무거운 커널 자원 소비, IPC 네임스페이스(Namespace) 의존
파일 잠금 (flock)Linux 1.0+항상 syscall파일 기반, 프로세스 간 only
POSIX 세마포어 (sem_wait)glibc 2.1+항상 syscall (초기)스레드 간 공유 제한적
FutexLinux 2.5.7 (2002)원자적 연산만 (~10ns)사용자 공간 프로토콜 구현 필요
futex2 / futex_waitvLinux 5.16 (2021)원자적 연산만새 syscall, 하위 호환성 제한

커널 소스 구조 변천

초기에는 kernel/futex.c 단일 파일(약 4,000줄)에 모든 구현이 있었지만, Linux 5.17에서 Peter Zijlstra에 의해 kernel/futex/ 디렉터리로 분리되었습니다.

kernel/futex/ 디렉터리 구조 (Linux 6.x)
파일역할
core.c해시 테이블, futex_key, futex_q, 공통 대기/웨이크 로직
waitwake.cFUTEX_WAIT, FUTEX_WAKE, FUTEX_WAIT_BITSET, FUTEX_WAKE_BITSET
pi.cFUTEX_LOCK_PI, FUTEX_UNLOCK_PI, rt_mutex 연동
requeue.cFUTEX_REQUEUE, FUTEX_CMP_REQUEUE, FUTEX_CMP_REQUEUE_PI
futex.h내부 헤더 — 구조체(Struct), 인라인 함수(Inline Function), 매크로(Macro)
syscalls.csys_futex(), sys_futex_waitv() 시스템 콜 진입점(Entry Point)

주요 마일스톤

Futex 커널 변경 타임라인
커널 버전연도변경 내용
2.5.72002초기 futex 도입 (Hubertus Franke, Rusty Russell)
2.5.702003FUTEX_REQUEUE 추가
2.6.72004FUTEX_CMP_REQUEUE (안전한 requeue)
2.6.142005FUTEX_WAKE_OP 추가
2.6.172006Robust Futex 도입 (robust_list_head)
2.6.182006PI Futex 도입 (FUTEX_LOCK_PI/UNLOCK_PI)
2.6.252008FUTEX_WAIT_BITSET / FUTEX_WAKE_BITSET
2.6.262008FUTEX_FD 제거 (보안/설계 결함)
2.6.272008FUTEX_PRIVATE_FLAG 도입 (프라이빗 futex 최적화)
2.6.312009FUTEX_WAIT_REQUEUE_PI, FUTEX_CMP_REQUEUE_PI
5.142021FUTEX_LOCK_PI2 (CLOCK_MONOTONIC 지원)
5.162021futex_waitv() 시스템 콜 도입
5.172022kernel/futex.c → kernel/futex/ 디렉터리 분리
6.72024futex 해시 테이블 크기 조정 최적화
설계 철학: Futex의 핵심 설계 원칙은 “경합이 없는 일반적인 경우를 최대한 빠르게”입니다. 커널은 슬립/웨이크업이라는 “느린” 작업만 담당하고, 잠금 상태 관리는 전적으로 사용자 공간의 원자적 연산에 위임합니다. 이 분업이 futex를 O(1) 비경합 비용으로 만듭니다.

Linux 2.5.7 ~ 6.x Futex 변경 상세 타임라인

Futex는 2002년 도입 이후 20년 넘게 지속적으로 진화해왔습니다. 각 커널 버전에서 추가된 기능, 수정된 버그, 성능 개선 사항을 상세히 살펴봅니다.

Futex 진화 타임라인 (2002~2024) 2002 — Linux 2.5.7 초기 futex 도입: FUTEX_WAIT, FUTEX_WAKE, FUTEX_FD Hubertus Franke, Rusty Russell, Ingo Molnar 2004 — Linux 2.6.7 FUTEX_CMP_REQUEUE (ABA-safe requeue) 2006 — Linux 2.6.17~18 Robust Futex (2.6.17) + PI Futex (2.6.18) 실시간 리눅스와 프로세스 안정성 핵심 기능 2008 — Linux 2.6.25~27 FUTEX_WAIT_BITSET/WAKE_BITSET (2.6.25) FUTEX_FD 제거 (2.6.26), PRIVATE_FLAG (2.6.27) 2009 — Linux 2.6.31 FUTEX_WAIT_REQUEUE_PI, FUTEX_CMP_REQUEUE_PI condvar + PI mutex 통합 최적화 2014 — CVE-2014-3153 Towelroot exploit — futex 보안 대대적 점검 requeue PI 경로 전면 재설계 2021 — Linux 5.14~5.16 FUTEX_LOCK_PI2 (5.14), futex_waitv() 신규 syscall (5.16) Andre Almeida — Wine/Proton WaitForMultipleObjects 지원 2022 — Linux 5.17 kernel/futex.c → kernel/futex/ 디렉터리 분리 (Peter Zijlstra) core.c, waitwake.c, pi.c, requeue.c, syscalls.c 2024 — Linux 6.7~6.12 해시 테이블 크기 자동 조정 (6.7) PREEMPT_RT 메인라인 통합 (6.12) — PI futex 핵심 역할 범례 주요 기능 추가 보안 이벤트 아키텍처 변경 코드 리팩터링 성능 최적화 PI/Requeue 확장
각 버전별 futex 커밋 상세 내역
커널 버전주요 커밋변경 내용영향
2.5.7Franke/Russell초기 futex 인터페이스 (WAIT/WAKE/FD)glibc NPTL 가능하게 함
2.6.7Jakub JelinekFUTEX_CMP_REQUEUE: val3으로 값 비교condvar broadcast 안전성 확보
2.6.14Ingo MolnarFUTEX_WAKE_OP: 이중 futex 연산pthread_cond_signal syscall 50% 감소
2.6.17Thomas Gleixnerrobust_list_head, set/get_robust_list프로세스 사망 시 교착 방지
2.6.18Ingo MolnarPI futex (LOCK_PI/UNLOCK_PI) + rt_mutex 연동실시간(Real-time) 우선순위 역전 해결
2.6.25Thomas GleixnerBITSET 연산 + 절대 시간 타임아웃condvar 그룹별 선택적 깨움
2.6.27Eric DumazetFUTEX_PRIVATE_FLAG프라이빗 futex 30~50% 성능 향상
5.14Andre AlmeidaFUTEX_LOCK_PI2: CLOCK_MONOTONIC 지원PI 타임아웃 시계 선택 가능
5.16Andre Almeidasys_futex_waitv(): 벡터 대기 신규 syscallWine/Proton 게임 호환성 대폭 개선
5.17Peter Zijlstrakernel/futex/ 디렉터리 분리 리팩터링유지보수성 향상, 4000줄 단일 파일 해소
6.7Waiman Longfutex 해시 테이블 크기 동적 조정대규모 시스템 해시 충돌 감소

설계 철학: Fast Path vs Slow Path 분리 원칙

Futex의 설계 철학은 "일반적인 경우(common case)를 극한으로 최적화"하는 것입니다. 이 원칙은 세 가지 핵심 관찰에 기반합니다:

Futex 설계 원칙의 근거
관찰의미설계 결정
대부분의 잠금 획득은 비경합90%+ 시도에서 즉시 성공Fast path: 사용자 공간 cmpxchg만으로 완료
시스템 콜은 수백 ns 비용cmpxchg(10ns) 대비 20~50배 느림Slow path에서만 syscall 사용
커널은 대기/깨움만 담당잠금 상태 관리는 사용자 공간에 위임커널 코드 최소화, 프로토콜 유연성
/* Futex 설계의 핵심: 3-level 잠금 전략 */

/* Level 1: Fast Path (사용자 공간, ~10ns) */
/* 조건: futex_word == 0 (잠금 해제 상태) */
if (cmpxchg(&futex_word, 0, 1) == 0)
    return; /* 성공! syscall 불필요 */

/* Level 2: Medium Path (사용자 공간, ~20-50ns) */
/* 조건: 잠금 보유자가 곧 해제할 것으로 예상 (adaptive spin) */
for (int i = 0; i < SPIN_COUNT; i++) {
    if (futex_word == 0 && cmpxchg(&futex_word, 0, 2) == 0)
        return;
    cpu_relax(); /* PAUSE 명령 */
}

/* Level 3: Slow Path (커널 진입, ~1000-5000ns) */
/* 조건: 경합이 지속됨 — 스케줄러에 양보 */
futex_word = 2; /* 대기자 있음 표시 */
syscall(SYS_futex, &futex_word, FUTEX_WAIT_PRIVATE, 2, NULL, NULL, 0);
왜 futex word는 사용자 공간에 있는가? 커널 객체(커널 세마포어, 파일 잠금 등)는 생성/해제에 항상 syscall이 필요합니다. futex word를 사용자 공간에 두면 커널 개입 없이 상태를 확인/변경할 수 있어, 비경합 경로에서 syscall 비용을 완전히 제거합니다. 대신 사용자 공간의 원자적 연산과 커널의 대기 큐 사이 일관성을 보장하는 것이 futex 프로토콜의 핵심 어려움입니다(lost wakeup 방지).

Futex와 System V 세마포어 내부 비교

Futex가 System V 세마포어를 어떻게 대체했는지 내부 구현 수준에서 비교합니다.

/* System V 세마포어: 항상 커널 진입 */
/* ipc/sem.c — semop() 호출 경로 */
/* 1. sys_semop() → sys_semtimedop()
 * 2. sem_lock() — 전역 세마포어 배열 잠금
 * 3. perform_atomic_semop() — 세마포어 값 변경
 * 4. 실패 시 sem_queue에 삽입 → schedule()
 * 5. 성공해도 최소 1 syscall 오버헤드 (항상)
 *
 * 비용: 비경합 시에도 ~500-2000ns (syscall + 커널 잠금)
 */

/* Futex: 비경합 시 커널 미진입 */
/* kernel/futex/waitwake.c — futex_wait() 호출 경로
 * 비경합: cmpxchg 1회 (~10ns) → 끝
 * 경합: sys_futex() → get_futex_key() → futex_q_lock()
 *       → get_futex_value_locked() → schedule()
 *
 * 비용: 비경합 ~10ns, 경합 ~1000-5000ns
 */
상세 연산 비용 비교: System V semaphore vs Futex
연산SysV semopFutex비율
비경합 잠금 획득~800ns (syscall 필수)~10ns (cmpxchg)80x 빠름
비경합 잠금 해제~700ns (syscall 필수)~10ns (atomic_exchange)70x 빠름
경합 잠금 대기~2000ns~1500ns1.3x 빠름
경합 깨움~1800ns~1200ns1.5x 빠름
생성/초기화~5000ns (semget+semctl)~0ns (변수 선언)무한대
커널 자원 소비sem_array + sem_undo경합 시만 futex_q (스택)훨씬 적음

시스템 콜 인터페이스

Futex 시스템 콜의 원형은 다음과 같습니다. 단일 futex(2) 시스템 콜이 op 매개변수에 따라 다양한 연산을 다중화(multiplexing)합니다.

/* man 2 futex — 시스템 콜 원형 */
#include <linux/futex.h>
#include <sys/syscall.h>

long syscall(SYS_futex,
    uint32_t *uaddr,          /* futex word 주소 */
    int       futex_op,       /* 연산 | 플래그 */
    uint32_t  val,            /* 의미는 op에 따라 다름 */
    const struct timespec *timeout,  /* 또는 val2 (requeue 시) */
    uint32_t *uaddr2,        /* requeue/wake_op 시 두 번째 futex */
    uint32_t  val3            /* bitset 또는 비교 값 */
);

FUTEX_WAIT 상세

FUTEX_WAIT는 futex의 가장 기본 대기 연산입니다. *uaddr == val이면 슬립하고, 아니면 즉시 -EAGAIN을 반환합니다. 이 “비교 후 슬립”은 커널 내부에서 원자적으로 수행되어 wakeup lost 문제를 방지합니다.

/* kernel/futex/waitwake.c — FUTEX_WAIT 핵심 로직 (간소화) */
static int futex_wait(u32 __user *uaddr, unsigned int flags,
                      u32 val, ktime_t *abs_time, u32 bitset)
{
    struct futex_hash_bucket *hb;
    struct futex_q q = futex_q_init;
    int ret;

    /* 1. futex_key 생성 (주소 → 키) */
    ret = get_futex_key(uaddr, flags, &q.key, FUTEX_READ);
    if (ret)
        return ret;

retry:
    /* 2. 해시 버킷 조회 및 잠금 */
    hb = futex_q_lock(&q);

    /* 3. 사용자 공간 값 읽기 (원자적으로 비교) */
    ret = get_futex_value_locked(&uval, uaddr);
    if (uval != val) {
        futex_q_unlock(hb);
        return -EAGAIN;  /* 값이 변경됨: spurious wakeup 방지 */
    }

    /* 4. 대기 큐에 삽입 */
    futex_wait_queue(hb, &q);

    /* 5. 슬립 (schedule) */
    if (!signal_pending(current)) {
        /* 타임아웃 설정 후 스케줄러로 전환 */
        freezable_schedule();
    }

    /* 6. 깨어남 — 결과 반환 */
    return ret;
}

FUTEX_WAKE 상세

/* kernel/futex/waitwake.c — FUTEX_WAKE 핵심 로직 (간소화) */
static int futex_wake(u32 __user *uaddr, unsigned int flags,
                      int nr_wake, u32 bitset)
{
    struct futex_hash_bucket *hb;
    struct futex_q *this, *next;
    union futex_key key;
    int ret;

    /* 1. futex_key 생성 */
    ret = get_futex_key(uaddr, flags, &key, FUTEX_READ);

    /* 2. 해시 버킷 조회 및 잠금 */
    hb = futex_hash(&key);
    spin_lock(&hb->lock);

    /* 3. 대기자 순회하며 깨움 */
    plist_for_each_entry_safe(this, next, &hb->chain, list) {
        if (futex_match(&this->key, &key)) {
            if (!(this->bitset & bitset))
                continue;

            futex_wake_mark(&wake_q, this);

            if (++ret >= nr_wake)
                break;
        }
    }

    spin_unlock(&hb->lock);
    wake_up_q(&wake_q);
    return ret;  /* 깨운 태스크 수 반환 */
}

FUTEX_WAKE_OP: 원자적 연산과 조건부 깨움

FUTEX_WAKE_OP는 glibc의 pthread_cond_signal() 구현에서 사용됩니다. 두 개의 futex(조건 변수의 대기 큐와 뮤텍스(Mutex))를 하나의 시스템 콜로 처리하여 성능을 최적화합니다.

/* FUTEX_WAKE_OP의 동작:
 * 1. *uaddr2에 원자적 연산 수행 (op 인코딩에 따라)
 * 2. uaddr에서 nr_wake개 깨움
 * 3. 연산 결과가 조건을 만족하면 uaddr2에서 nr_wake2개 추가 깨움
 *
 * 인코딩: val3 = (op << 28) | (cmp << 24) | (oparg << 12) | cmparg
 */
#define FUTEX_OP_SET   0  /* *uaddr2 = oparg */
#define FUTEX_OP_ADD   1  /* *uaddr2 += oparg */
#define FUTEX_OP_OR    2  /* *uaddr2 |= oparg */
#define FUTEX_OP_ANDN  3  /* *uaddr2 &= ~oparg */
#define FUTEX_OP_XOR   4  /* *uaddr2 ^= oparg */

#define FUTEX_OP_CMP_EQ  0  /* oldval == cmparg */
#define FUTEX_OP_CMP_NE  1  /* oldval != cmparg */
#define FUTEX_OP_CMP_LT  2  /* oldval < cmparg */
#define FUTEX_OP_CMP_LE  3  /* oldval <= cmparg */
#define FUTEX_OP_CMP_GT  4  /* oldval > cmparg */
#define FUTEX_OP_CMP_GE  5  /* oldval >= cmparg */
FUTEX_PRIVATE_FLAG의 효과: FUTEX_PRIVATE_FLAG가 설정되면 커널은 futex word를 mm(메모리 맵(Memory Map))의 가상 주소만으로 식별합니다. 공유 futex는 물리 페이지와 오프셋으로 식별해야 하므로 get_user_pages()를 호출해 페이지를 pin하는 오버헤드가 있습니다. 프라이빗 futex는 이 과정을 생략하여 약 30~50% 빠릅니다.

FUTEX_WAIT 상세 흐름 (커널 코드 레벨)

FUTEX_WAIT는 단순해 보이지만 내부적으로 여러 복잡한 경로를 처리합니다. 페이지 폴트(Page Fault) 재시도, 시그널(Signal) 처리, 타임아웃, spurious wakeup 등을 모두 고려해야 합니다.

/* kernel/futex/waitwake.c — FUTEX_WAIT 전체 경로 (상세) */
static int futex_wait(u32 __user *uaddr, unsigned int flags,
                      u32 val, ktime_t *abs_time, u32 bitset)
{
    struct hrtimer_sleeper timeout, *to;
    struct futex_hash_bucket *hb;
    struct futex_q q = futex_q_init;
    int ret;

    if (!bitset)
        return -EINVAL;  /* bitset=0은 유효하지 않음 */

    q.bitset = bitset;

    /* 타임아웃 설정 */
    to = futex_setup_timer(abs_time, &timeout, flags, 0);

retry:
    /* 1. futex_key 생성 (프라이빗/공유 분기) */
    ret = get_futex_key(uaddr, flags & FLAGS_SHARED, &q.key,
                        FUTEX_READ);
    if (unlikely(ret != 0))
        goto out;

retry_private:
    /* 2. 해시 버킷 조회 + spinlock 획득 */
    hb = futex_q_lock(&q);

    /* 3. 사용자 공간 값 읽기 (spinlock 보유 상태) */
    ret = get_futex_value_locked(&uval, uaddr);
    if (ret) {
        /* 페이지 폴트! spinlock 해제 후 fault-in */
        futex_q_unlock(hb);
        ret = get_user(uval, uaddr);  /* 페이지 fault-in */
        if (ret)
            goto out;  /* EFAULT: 잘못된 주소 */

        /* 공유 futex: 페이지가 바뀌었을 수 있으므로 키 재생성 */
        if (!(flags & FLAGS_SHARED))
            goto retry_private;
        goto retry;
    }

    /* 4. 값 비교: 변경되었으면 즉시 반환 */
    if (uval != val) {
        futex_q_unlock(hb);
        ret = -EAGAIN;  /* 값이 이미 변경됨 */
        goto out;
    }

    /* 5. 대기 큐에 삽입 + schedule() */
    futex_wait_queue(hb, &q, to);

    /* 6. 깨어남 — 이유 분석 */
    ret = 0;
    if (!unqueue_me(&q))
        goto out;  /* 정상 wakeup */
    ret = -ETIMEDOUT;
    if (to && !to->task)
        goto out;  /* 타임아웃 */
    if (signal_pending(current)) {
        ret = -ERESTARTSYS;  /* 시그널 수신 */
        if (abs_time)
            ret = -ERESTARTNOHAND;
    }

out:
    if (to)
        hrtimer_cancel(&to->timer);
    return ret;
}
FUTEX_WAIT 반환값 상세
반환값의미사용자 공간 대응
0정상 wakeup (FUTEX_WAKE에 의해 깨어남)잠금 재시도
-EAGAIN*uaddr != val (이미 변경됨)즉시 잠금 재시도
-ETIMEDOUT타임아웃 만료에러 보고 또는 재시도
-ERESTARTSYS시그널 수신 (재시작(Reboot) 가능)커널이 자동 재시도
-EFAULTuaddr이 유효하지 않은 주소프로그래밍 에러
-EINVALbitset=0 등 잘못된 인자프로그래밍 에러

FUTEX_WAKE 상세 흐름

FUTEX_WAKE의 핵심은 해시 버킷에서 일치하는 대기자를 찾아 깨우는 것입니다. wake_q 메커니즘을 사용하여 spinlock 해제 후에 실제 wakeup을 수행합니다.

/* wake_q: spinlock 밖에서 wakeup 수행하는 메커니즘 */
/* 왜 필요한가?
 * - wake_up_state()는 스케줄러 잠금을 획득할 수 있음
 * - 해시 버킷 spinlock을 보유한 상태에서 스케줄러 잠금을 획득하면
 *   잠금 순서 위반(lock ordering violation) → 교착 가능
 * - 해결: wake_q에 태스크를 등록 후, spinlock 해제 후에 깨움 */

struct wake_q_head {
    struct wake_q_node *first;
    struct wake_q_node **lastp;
};

/* futex_wake 전체 흐름:
 * 1. spin_lock(&hb->lock)
 * 2. plist 순회 → 매칭 대기자 찾기
 * 3. wake_q_add(&wake_q, task)  // 등록만
 * 4. spin_unlock(&hb->lock)
 * 5. wake_up_q(&wake_q)         // 실제 wakeup
 */
Spurious Wakeup: futex 대기자는 FUTEX_WAKE 없이도 깨어날 수 있습니다. 시그널, 타임아웃, 또는 커널 내부 이유로 spurious wakeup이 발생할 수 있으므로, 항상 깨어난 후 futex word 값을 재확인하고 루프에서 대기해야 합니다. 이것이 glibc의 __lll_lock_wait()while 루프로 구현된 이유입니다.

FUTEX_FD (deprecated) 역사와 제거 이유

FUTEX_FD(op=2)는 futex word에 파일 디스크립터를 연결하여 poll()/select()/epoll로 futex 이벤트를 대기할 수 있게 했습니다. 그러나 심각한 설계 결함과 보안 문제로 Linux 2.6.26에서 제거되었습니다.

FUTEX_FD가 제거된 이유
문제설명영향
Race conditionfd 생성과 futex 대기 사이 wakeup이 손실될 수 있음Lost wakeup → 교착
자원 누수fd를 닫지 않으면 커널 자원 누수DoS 가능
복잡한 생명주기fd와 futex_q의 생명주기 관리가 복잡커널 버그 유발
대안 존재eventfd + epoll이 더 안전하고 유연FUTEX_FD 불필요
/* FUTEX_FD가 제거된 후의 대안 패턴 */
/* 1. futex + eventfd 조합: */
int efd = eventfd(0, EFD_NONBLOCK);
/* 스레드 A: futex 조건 충족 시 eventfd에 알림 */
uint64_t val = 1;
write(efd, &val, sizeof(val));
/* 스레드 B: epoll로 eventfd 대기 */
epoll_wait(epollfd, events, 1, -1);

/* 2. futex_waitv (Linux 5.16+): 다중 futex 동시 대기의 현대적 해법 */
struct futex_waitv waiters[4];
/* ... 설정 ... */
syscall(SYS_futex_waitv, waiters, 4, 0, timeout, CLOCK_MONOTONIC);

FUTEX_LOCK_PI / FUTEX_UNLOCK_PI 상세

PI futex의 시스템 콜 인터페이스는 일반 futex와 다른 의미론을 가집니다. futex word 자체에 소유자의 TID가 기록되며, 커널이 소유권을 추적합니다.

/* FUTEX_LOCK_PI 사용법 */
/* futex_word 초기값: 0 (잠금 해제)
 * 잠금 후: current_tid (소유자 TID)
 * 대기자 있을 때: current_tid | FUTEX_WAITERS
 *
 * 사용자 공간에서의 전형적 패턴: */
uint32_t futex_word = 0;
uint32_t tid = gettid();

/* 잠금 획득 */
uint32_t expected = 0;
if (cmpxchg(&futex_word, expected, tid) != 0) {
    /* Fast path 실패: 커널에 PI 잠금 요청 */
    syscall(SYS_futex, &futex_word,
            FUTEX_LOCK_PI | FUTEX_PRIVATE_FLAG,
            0, NULL, NULL, 0);
    /* 커널이 futex_word에 우리 TID를 기록해줌 */
}

/* 잠금 해제 */
uint32_t prev = cmpxchg(&futex_word, tid, 0);
if (prev != tid) {
    /* FUTEX_WAITERS 비트가 설정되어 있음: 커널에 해제 요청 */
    syscall(SYS_futex, &futex_word,
            FUTEX_UNLOCK_PI | FUTEX_PRIVATE_FLAG,
            0, NULL, NULL, 0);
    /* 커널이 다음 대기자에게 소유권 전달 */
}

/* FUTEX_LOCK_PI2 (Linux 5.14+): CLOCK_MONOTONIC 타임아웃 지원 */
struct timespec timeout = { .tv_sec = 5, .tv_nsec = 0 };
syscall(SYS_futex, &futex_word,
        FUTEX_LOCK_PI2 | FUTEX_PRIVATE_FLAG,
        0, &timeout, NULL, 0);
/* FUTEX_LOCK_PI는 CLOCK_REALTIME만 사용하므로
 * 시스템 시간 변경에 영향을 받음.
 * FUTEX_LOCK_PI2는 CLOCK_MONOTONIC으로 안정적 타임아웃 */
PI futex의 제한: PI futex는 반드시 프로세스 간 공유가 가능해야 하며, futex word에 소유자 TID를 정확히 기록해야 합니다. TID가 잘못되면 커널이 잘못된 태스크의 우선순위(Priority)를 부스트하거나, 해제 시 에러가 발생합니다. 또한 PI futex는 PTHREAD_PRIO_INHERIT 속성이 설정된 mutex에서만 사용되며, 일반 PTHREAD_MUTEX_NORMAL에서는 사용되지 않습니다.
Futex 해시 테이블 구조 (futex_queues[]) 해시 버킷 배열 bucket[0] bucket[1] bucket[h] bucket[...] bucket[N-2] bucket[N-1] N = 256 * num_cpus (최소 16, 최대 2^21) futex_hash_bucket spinlock_t lock; plist_head chain; /* 우선순위 정렬 리스트 */ /* PI futex용 plist_node */ int waiters; /* 대기자 수 */ futex_q (Thread A) task: <Thread A> key: {mm, addr, offset} prio: 120 (SCHED_NORMAL) bitset: 0xFFFFFFFF futex_q (Thread B) task: <Thread B> key: {mm, addr, offset} prio: 49 (SCHED_FIFO) bitset: 0x00000001 futex_q (Thread C) task: <Thread C> key: {inode, pgoff, off} prio: 120 (SCHED_NORMAL) bitset: 0xFFFFFFFF 해시 함수 hash = jhash2(key, 3, futex_shift) % futex_hashsize; 프라이빗: (mm, vaddr) 공유: (inode, pgoff, off) plist: 우선순위 정렬 Thread B (prio=49) Thread A (prio=120) Thread C (prio=120) futex_hash_bucket 체인: plist로 우선순위 순 정렬, spinlock으로 보호

내부 구현: 해시 테이블과 대기 큐

futex_key: 주소 식별

futex_key는 futex word를 고유하게 식별하는 3-워드 구조체입니다. 동일한 물리 메모리(Physical Memory)를 가리키는 서로 다른 가상 주소가 같은 키로 매핑되어야 프로세스 간 공유 futex가 올바르게 동작합니다.

/* kernel/futex/futex.h */
union futex_key {
    struct {
        u64 i_seq;        /* inode sequence number */
        unsigned long pgoff;  /* 페이지 오프셋 */
        unsigned int offset;  /* 페이지 내 바이트 오프셋 */
    } shared;               /* 공유 futex: mmap, shm, 파일 매핑 */

    struct {
        union {
            struct mm_struct *mm;
            u64 __tmp;
        };
        unsigned long address;  /* 가상 주소 */
        unsigned int offset;    /* 항상 0 */
    } private;                /* 프라이빗 futex: 프로세스/스레드 내부 */

    struct {
        u64 ptr;
        unsigned long word;
        unsigned int offset;
    } both;                   /* 해시 계산용 통합 뷰 */
};

get_futex_key(): 프라이빗 vs 공유

/* kernel/futex/core.c — get_futex_key() 핵심 로직 (간소화) */
int get_futex_key(u32 __user *uaddr, unsigned int flags,
                  union futex_key *key, enum futex_access rw)
{
    unsigned long address = (unsigned long)uaddr;
    struct mm_struct *mm = current->mm;

    key->both.offset = address % PAGE_SIZE;

    if (flags & FLAGS_SHARED) {
        /* 공유 futex: 물리 페이지 기반 식별 필요 */
        struct page *page;
        struct vm_area_struct *vma;

        /* GUP: 페이지 조회 (비용 높음) */
        ret = get_user_pages_fast(address, 1, 0, &page);

        if (PageAnon(page)) {
            /* 익명 매핑: mm + address */
            key->private.mm = mm;
            key->private.address = address;
        } else {
            /* 파일 매핑: inode + page offset */
            struct inode *inode = vma->vm_file->f_mapping->host;
            key->shared.i_seq = get_inode_sequence_number(inode);
            key->shared.pgoff = page_to_pgoff(page);
        }
    } else {
        /* 프라이빗 futex: 가상 주소만으로 충분 (빠름!) */
        key->private.mm = mm;
        key->private.address = address;
    }
    return 0;
}

해시 테이블 크기 결정

futex 해시 테이블은 부팅 시 할당되며, CPU 수에 비례하여 크기가 결정됩니다. 해시 충돌은 futex 성능의 주요 병목(Bottleneck)이 될 수 있으므로, 적절한 크기가 중요합니다.

/* kernel/futex/core.c — 해시 테이블 초기화 */
static int __init futex_init(void)
{
    unsigned long hashsize;

    /* CPU 수에 비례하여 해시 크기 결정 */
    hashsize = 256 * num_possible_cpus();
    hashsize = roundup_pow_of_two(hashsize);

    /* 최소 16, 최대 2^21 */
    if (hashsize < 16)
        hashsize = 16;

    futex_queues = alloc_large_system_hash("futex",
        sizeof(struct futex_hash_bucket),
        hashsize, 0, 0, &futex_shift, NULL,
        hashsize, hashsize);

    for (i = 0; i < hashsize; i++) {
        atomic_set(&futex_queues[i].waiters, 0);
        plist_head_init(&futex_queues[i].chain);
        spin_lock_init(&futex_queues[i].lock);
    }
    return 0;
}
CPU 수별 해시 테이블 크기 예시
CPU 수hashsize (256 * CPUs)반올림(2의 거듭제곱)메모리 사용량 (approx)
1256256~8 KB
41,0241,024~32 KB
164,0964,096~128 KB
6416,38416,384~512 KB
12832,76832,768~1 MB
25665,53665,536~2 MB

futex_q 구조체

/* kernel/futex/futex.h */
struct futex_q {
    struct plist_node list;      /* 해시 버킷 체인의 노드 */
    struct task_struct *task;    /* 대기 중인 태스크 */
    spinlock_t *lock_ptr;        /* 해시 버킷의 spinlock 포인터 */
    union futex_key key;         /* futex 식별 키 */
    struct futex_pi_state *pi_state;  /* PI futex 상태 */
    struct rt_mutex_waiter *rt_waiter; /* PI: rt_mutex 대기자 */
    union futex_key *requeue_pi_key;  /* PI requeue 키 */
    u32 bitset;                  /* WAIT_BITSET/WAKE_BITSET 마스크 */
    atomic_t requeue_state;      /* requeue 상태 추적 */
};
해시 충돌 주의: 다수의 futex word가 동일한 해시 버킷에 매핑되면 spinlock 경합이 심화되어 성능이 급격히 저하됩니다. 특히 수천 개의 스레드가 서로 다른 뮤텍스를 사용하는 환경에서 해시 테이블 크기가 작으면 futex_hash_bucket.lock 경합이 병목이 됩니다. Linux 6.7 이후 해시 크기 자동 조정 패치(Patch)가 추가되었습니다.

해시 버킷 내부 구조 상세

futex_hash_bucket은 futex 시스템의 핵심 자료 구조입니다. 각 버킷은 spinlock으로 보호되며, 우선순위 정렬 리스트(plist)로 대기자를 관리합니다.

/* kernel/futex/futex.h — futex_hash_bucket 상세 */
struct futex_hash_bucket {
    atomic_t waiters;       /* 이 버킷의 대기자 수 (최적화용) */
    spinlock_t lock;        /* 버킷 보호 spinlock */
    struct plist_head chain; /* 우선순위 정렬 대기 큐 */
} ____cacheline_aligned_in_smp;

/* ____cacheline_aligned_in_smp:
 * 각 버킷이 별도의 캐시라인에 위치하도록 정렬.
 * SMP 시스템에서 서로 다른 버킷의 spinlock이
 * 같은 캐시라인을 공유하면 false sharing으로 성능 저하.
 * sizeof(futex_hash_bucket) ≈ 32~48 bytes
 * 패딩으로 64 bytes (L1 캐시라인) 정렬 */

/* plist (Priority List):
 * - 일반 list_head와 달리 우선순위로 정렬된 이중 연결 리스트
 * - PI futex에서 가장 높은 우선순위 대기자를 O(1)에 찾기 위함
 * - SCHED_FIFO/SCHED_RR 태스크가 먼저 깨어남
 * - 같은 우선순위 내에서는 FIFO 순서 */
futex_key 생성 과정: Private vs Shared uaddr (futex word 주소) FLAGS_SHARED 확인 Private (FLAGS_SHARED 없음) Private futex_key key.private.mm = current->mm key.private.address = uaddr key.both.offset = addr % PAGE_SIZE 비용: ~5ns (GUP 불필요) 동일 프로세스 내 스레드만 공유 가능 Shared (FLAGS_SHARED 있음) Shared futex_key 1. get_user_pages_fast(uaddr) → 물리 페이지 pin (~50-200ns) 2. PageAnon(page) 확인: 익명: key = {mm, address, offset} 파일: key = {inode_seq, pgoff, offset} 3. put_page(page) — pin 해제 비용: ~50-200ns (GUP 필요) 프로세스 간 공유 가능 (mmap/shm) 다른 가상 주소 → 같은 물리 페이지 → 같은 key jhash2(key.both, 3, shift) futex_hash_bucket[index]

futex_key 구조 상세: 프라이빗 vs 공유 식별

futex_key의 핵심 과제는 "동일한 futex word를 가리키는 서로 다른 가상 주소가 같은 키로 매핑되어야 한다"는 것입니다. 이를 위해 공유 futex는 물리 페이지 기반으로 식별합니다.

/* futex_key 비교: futex_match() */
static inline int futex_match(union futex_key *key1,
                               union futex_key *key2)
{
    return (key1->both.ptr == key2->both.ptr &&
            key1->both.word == key2->both.word &&
            key1->both.offset == key2->both.offset);
}
/* 3개 필드 모두 일치해야 같은 futex
 * → 해시 충돌이 있어도 정확한 매칭 보장 */

/* 공유 futex 시나리오:
 * Process A: mmap(NULL, 4096, PROT_READ|PROT_WRITE,
 *                  MAP_SHARED, shm_fd, 0) → addr_A = 0x7f0000
 * Process B: mmap(NULL, 4096, PROT_READ|PROT_WRITE,
 *                  MAP_SHARED, shm_fd, 0) → addr_B = 0x7f8000
 *
 * addr_A != addr_B (다른 가상 주소)
 * 하지만 같은 물리 페이지를 가리킴
 * → futex_key.shared = {inode_seq, pgoff, offset}
 * → 같은 key → 같은 해시 버킷 → 올바른 wakeup */
PRIVATE_FLAG 성능 이점의 원인: 프라이빗 futex는 get_user_pages_fast()를 호출하지 않으므로: (1) 페이지 테이블(Page Table) 워크 불필요, (2) 페이지 pin/unpin 오버헤드 제거, (3) struct page 참조 카운트(Reference Count) 원자적 증감 불필요. 특히 고경합 시 get_user_pages_fast()의 atomic 연산이 캐시(Cache)라인 바운싱을 유발하므로, PRIVATE_FLAG의 이점은 경합이 높을수록 더 커집니다.

대기 큐 연산: enqueue / dequeue / unqueue

futex 대기 큐의 삽입/제거 연산은 해시 버킷의 spinlock으로 보호됩니다. 각 연산의 정확한 시점과 잠금 범위를 이해하는 것이 futex 코드 분석의 핵심입니다.

/* kernel/futex/core.c — 대기 큐 삽입 */
static inline void
futex_q_lock_and_enqueue(struct futex_q *q,
                          struct futex_hash_bucket *hb)
{
    /* spinlock 획득 */
    spin_lock(&hb->lock);

    /* 대기자 카운터 증가 (최적화: 빈 버킷 체크) */
    atomic_inc(&hb->waiters);

    /* plist에 삽입 (우선순위 순) */
    plist_node_init(&q->list, current->normal_prio);
    plist_add(&q->list, &hb->chain);

    q->lock_ptr = &hb->lock;

    /* spinlock 유지한 채 반환 → 값 비교 후 schedule() */
}

/* 대기 큐 제거 (wakeup 측) */
static inline void
futex_wake_mark(struct wake_q_head *wake_q,
                struct futex_q *q)
{
    /* plist에서 제거 */
    plist_del(&q->list, &hb->chain);

    /* 대기자 카운터 감소 */
    atomic_dec(&hb->waiters);

    /* wake_q에 등록 (spinlock 해제 후 깨움) */
    wake_q_add(wake_q, q->task);

    q->lock_ptr = NULL; /* unqueue_me() 방지 */
}

/* 자발적 제거 (타임아웃, 시그널) */
static int unqueue_me(struct futex_q *q)
{
    spinlock_t *lock_ptr;
    int ret = 0;

    lock_ptr = READ_ONCE(q->lock_ptr);
    if (!lock_ptr)
        return 1; /* 이미 wakeup 측에서 제거됨 */

    spin_lock(lock_ptr);
    if (q->lock_ptr != lock_ptr) {
        spin_unlock(lock_ptr);
        return 1; /* 동시에 wakeup됨 */
    }

    /* plist에서 자신을 제거 */
    plist_del(&q->list, &q->list.plist);
    atomic_dec(&hb->waiters);
    q->lock_ptr = NULL;

    spin_unlock(lock_ptr);
    return 0; /* 성공적으로 제거 */
}

get_futex_value_locked(): 페이지 폴트 처리

해시 버킷 spinlock을 보유한 상태에서 사용자 공간 메모리에 접근하는 것은 잠재적으로 위험합니다. 페이지가 스왑(Swap) 아웃되었으면 페이지 폴트가 발생하고, 페이지 폴트 핸들러(Handler)가 I/O 대기를 시도하면 sleep-in-atomic-context 버그가 됩니다.

/* futex의 "fault, retry" 패턴 상세 */
/* kernel/futex/waitwake.c */

retry:
    hb = futex_q_lock(&q); /* spinlock 획득 */

    /* pagefault_disable: 페이지 폴트 시 -EFAULT 반환 (sleep 불가) */
    ret = get_futex_value_locked(&uval, uaddr);

    if (ret) {
        /* 페이지 폴트 발생! */
        futex_q_unlock(hb); /* spinlock 해제 (중요!) */

        /* 이제 sleep 가능: 페이지를 fault-in */
        ret = get_user(uval, uaddr);
        /* get_user()는 필요시 I/O 대기 → 페이지 로드 */

        if (ret)
            return -EFAULT; /* 진짜 잘못된 주소 */

        /* 페이지가 로드됨: 처음부터 재시도 */
        /* 주의: 공유 futex는 키도 재생성해야 함
         * (페이지가 CoW되었을 수 있으므로) */
        if (shared)
            goto retry;     /* 키 재생성 포함 */
        else
            goto retry_private; /* 키 유지, spinlock만 재획득 */
    }

/* 이 패턴의 핵심:
 * 1. spinlock 보유 → 사용자 메모리 접근 시도
 * 2. 실패하면 spinlock 해제 → 안전하게 fault-in
 * 3. 전체 재시도 (값이 변경되었을 수 있으므로)
 *
 * 최악의 경우 2번 재시도 (스왑 페이지) 하지만
 * 대부분은 페이지가 메모리에 있으므로 0번 재시도 */
futex_q는 스택 변수: futex_qkmalloc()으로 할당하지 않고 시스템 콜 함수의 스택에 선언됩니다. 이는 슬립/깨움 시 할당/해제 오버헤드를 제거합니다. 스택 변수이므로 대기 중인 동안 해당 커널 스레드(Kernel Thread)의 커널 스택이 유지되어야 합니다. 이것이 futex 대기가 TASK_INTERRUPTIBLE/TASK_KILLABLE로 설정되는 이유 중 하나입니다(프로세스 종료 시 스택이 정리되어야 하므로).

PI Futex (우선순위 상속(Priority Inheritance))

PI(Priority Inheritance) Futex는 우선순위 역전(Priority Inversion) 문제를 해결합니다. 저우선순위 태스크가 잠금을 보유하고, 고우선순위 태스크가 대기하며, 중간 우선순위 태스크가 저우선순위 태스크를 선점(Preemption)하는 상황을 방지합니다.

우선순위 역전 시나리오

우선순위 역전의 전형적인 3-task 시나리오(High/Mid/Low)와 PI 프로토콜의 해결 과정은 PREEMPT_RT: PI chain 문서에서 단계별로 상세히 설명합니다.

PI 프로토콜의 해결

PI Futex는 커널 내부의 rt_mutex와 연동하여 우선순위 상속 체인을 관리합니다. 고우선순위 태스크 H가 잠금을 요청하면, 잠금 보유자 L의 우선순위를 H 수준으로 일시적으로 상승시킵니다. 이로써 L이 중간 우선순위 태스크 M에 의해 선점되지 않습니다.

PI Futex 우선순위 상속 체인 Task H (prio=10) SCHED_FIFO, RT FUTEX_LOCK_PI 대기 중 → rt_mutex_waiter 등록 waits Lock A rt_mutex owned Task M (prio=50) Lock A 보유 중 PI: prio 10으로 부스트! → Lock B 대기 중 Lock B rt_mutex Task L (prio=90) Lock B 보유, PI: prio 10으로 부스트! PI 체인 전파 (rt_mutex_adjust_prio_chain) H(10) → Lock A → M(50→10) → Lock B → L(90→10) 체인을 따라 모든 소유자의 우선순위를 H 수준으로 부스트 L이 Lock B를 해제하면 부스트 해제 → 원래 prio 복원 PI 체인: 최대 깊이 1024 (교착 탐지용 제한)

FUTEX_LOCK_PI / FUTEX_UNLOCK_PI 흐름

/* kernel/futex/pi.c — FUTEX_LOCK_PI 핵심 로직 (간소화) */
static int futex_lock_pi(u32 __user *uaddr, unsigned int flags,
                         ktime_t *time, int trylock)
{
    struct futex_hash_bucket *hb;
    struct futex_q q = futex_q_init;
    struct futex_pi_state *pi_state;
    u32 uval, curval;

    /* 1. Fast path: cmpxchg(uaddr, 0, current->pid) */
    if (cmpxchg_futex_value_locked(&curval, uaddr, 0,
                                    current->pid) == 0)
        return 0;  /* 경합 없이 획득! */

    /* 2. 해시 버킷 잠금 */
    hb = futex_q_lock(&q);

    /* 3. futex_pi_state 생성 또는 기존 것 참조 */
    ret = futex_lock_pi_atomic(uaddr, hb, &q.key, &ps,
                               current, &exiting, 0);

    /* 4. rt_mutex에서 대기 (우선순위 상속 발동) */
    ret = rt_mutex_futex_trylock(&q.pi_state->pi_mutex);
    if (!ret) {
        /* rt_mutex_slowlock → 우선순위 부스트 + 슬립 */
        ret = __rt_mutex_futex_lock(&q.pi_state->pi_mutex,
                                     &rt_waiter, timeout);
    }

    /* 5. 깨어남: futex word에 자신의 TID 기록 */
    if (!ret)
        futex_lock_pi_complete(uaddr, q.pi_state);

    return ret;
}

/* FUTEX_UNLOCK_PI */
static int futex_unlock_pi(u32 __user *uaddr, unsigned int flags)
{
    u32 curval;

    /* 1. Fast path: 대기자 없으면 cmpxchg(uaddr, pid, 0) */
    if (cmpxchg_futex_value_locked(&curval, uaddr,
                                    current->pid, 0) == 0)
        return 0;

    /* 2. 대기자가 있으면 rt_mutex_unlock → 우선순위 복원 */
    /* 다음 대기자에게 소유권 전달 */
    rt_mutex_futex_unlock(&pi_state->pi_mutex, &wake_q);
    wake_up_q(&wake_q);

    return 0;
}

futex_pi_state 구조체

/* kernel/futex/futex.h */
struct futex_pi_state {
    struct list_head list;        /* task->pi_state_list에 연결 */
    struct rt_mutex_base pi_mutex;  /* 실제 PI 잠금 */
    struct task_struct *owner;    /* 현재 소유자 */
    refcount_t refcount;          /* 참조 카운트 */
    union futex_key key;          /* futex 식별 키 */
};
PI 체인 깊이 제한: rt_mutex_adjust_prio_chain()은 최대 1024 단계까지 체인을 따라 우선순위를 전파합니다. 이는 교착 상태(deadlock) 탐지 역할도 겸합니다. 체인이 이 한계를 초과하면 -EDEADLK를 반환합니다.

PI 프로토콜 동작 상세: rt_mutex_waiter 연결

PI futex는 커널의 rt_mutex 인프라를 활용합니다. 사용자 공간의 PI futex 잠금 요청이 커널로 들어오면, futex_pi_state에 연결된 rt_mutex에서 대기합니다.

PI 프로토콜의 핵심 자료구조인 rt_mutex_waiter는 대기자의 우선순위, 대기 중인 잠금, 트리 노드 등을 관리합니다. 구조체 상세 필드는 PREEMPT_RT: RT Mutex 내부 구현을 참고하세요.

PI 체인 전파 다이어그램: 3-task PI 체인의 단계별 전파 과정은 PREEMPT_RT 문서의 PI chain 섹션에서 상세 다이어그램으로 확인할 수 있습니다.

PI 체인 워킹 알고리즘 코드

rt_mutex_adjust_prio_chain()은 PI 체인을 따라 우선순위를 전파하는 핵심 함수입니다. 잠금 소유자를 찾아 우선순위를 부스트하고, 해당 소유자가 다른 잠금을 대기 중이면 체인을 따라 재귀적으로 전파합니다.

상세 코드: rt_mutex_adjust_prio_chain()의 전체 알고리즘(6단계 워킹, 데드락 감지, 깊이 제한)은 PREEMPT_RT: PI 체인 워킹 알고리즘에서 코드 레벨로 분석합니다.

PI 데드락 감지

PI 체인 워킹 도중 순환이 감지되면 교착 상태입니다. 커널은 순환 감지(A→B→C→A)와 깊이 제한(max_lock_depth=1024) 두 가지 방법으로 데드락을 감지하며, 감지 시 -EDEADLK를 반환합니다.

상세 분석: PI 데드락 감지 메커니즘(max_lock_depth, -EDEADLK, 순환 검출)의 상세 구현은 PREEMPT_RT: PI 체인 깊이 제한을 참고하세요.
-EDEADLK의 한계: PI 데드락 감지는 FUTEX_LOCK_PI를 사용하는 잠금에 대해서만 동작합니다. 일반 FUTEX_WAIT 기반 잠금은 커널이 소유권을 추적하지 않으므로 데드락을 감지할 수 없습니다. 일반 뮤텍스의 데드락 감지는 PTHREAD_MUTEX_ERRORCHECK 타입 또는 외부 도구(lockdep, helgrind)에 의존해야 합니다.

Robust Futex (비정상 종료 대응)

일반 futex에서 잠금 보유자가 SIGKILL이나 비정상 종료로 잠금을 해제하지 못하면, 대기 중인 모든 스레드가 영원히 차단됩니다. Robust Futex는 이 문제를 해결합니다.

robust_list_head 메커니즘

각 스레드는 set_robust_list() 시스템 콜로 자신의 robust futex 리스트를 커널에 등록합니다. 스레드가 종료할 때 커널은 이 리스트를 순회하면서 보유 중인 futex를 정리합니다.

/* include/uapi/linux/futex.h */
struct robust_list {
    struct robust_list __user *next;  /* 다음 노드 */
};

struct robust_list_head {
    struct robust_list list;           /* 리스트 헤드 */
    long futex_offset;                 /* robust_list → futex word 오프셋 */
    struct robust_list __user *list_op_pending;  /* 진행 중인 잠금 연산 */
};
/* glibc nptl/pthread_mutex_lock.c — robust mutex 획득 (간소화) */
int __pthread_mutex_lock(pthread_mutex_t *mutex)
{
    /* 1. robust 리스트에 등록 */
    THREAD_SETMEM(THREAD_SELF, robust_head.list_op_pending,
                  &mutex->__data.__list.__next);

    /* 2. futex word에 TID 기록 */
    int newval = THREAD_GETMEM(THREAD_SELF, tid);
    oldval = atomic_compare_and_exchange_val_acq(
        &mutex->__data.__lock, newval, 0);

    if (oldval != 0) {
        /* 경합: FUTEX_LOCK_PI 또는 FUTEX_WAIT */
    }

    /* 3. robust 리스트에 연결 완료 */
    ENQUEUE_MUTEX(mutex);
    THREAD_SETMEM(THREAD_SELF, robust_head.list_op_pending, NULL);
    return 0;
}

커널의 종료 시 정리

/* kernel/futex/core.c — exit_robust_list() */
void exit_robust_list(struct task_struct *curr)
{
    struct robust_list_head __user *head;
    struct robust_list __user *entry, *next_entry;
    unsigned int limit = ROBUST_LIST_LIMIT; /* 2048개 제한 */

    if (get_user(entry, &head->list.next))
        return;

    /* robust 리스트 순회 */
    while (entry != (struct robust_list __user *)&head->list) {
        u32 __user *futex_addr;

        /* futex word 주소 계산: entry + futex_offset */
        futex_addr = (u32 __user *)((char *)entry + futex_offset);

        /* FUTEX_OWNER_DIED 비트 설정 + FUTEX_WAKE */
        handle_futex_death(futex_addr, curr);

        if (get_user(next_entry, &entry->next))
            break;
        entry = next_entry;

        if (!--limit)
            break;  /* 무한 루프 방지 */
    }

    /* list_op_pending 처리 (진행 중이던 잠금) */
    if (head->list_op_pending)
        handle_futex_death(pending_futex_addr, curr);
}

static int handle_futex_death(u32 __user *uaddr,
                               struct task_struct *curr)
{
    u32 uval, nval;

    /* futex word에 FUTEX_OWNER_DIED (bit 30) 설정 */
    do {
        get_user(uval, uaddr);
        if ((uval & FUTEX_TID_MASK) != task_pid_vnr(curr))
            return 0;  /* 소유자가 아님 */

        nval = (uval & FUTEX_WAITERS) | FUTEX_OWNER_DIED;
    } while (cmpxchg_futex_value_locked(&nval, uaddr, uval, nval));

    /* 대기자 깨움 */
    if (uval & FUTEX_WAITERS)
        futex_wake(uaddr, FLAGS_SIZE_32, 1, FUTEX_BITSET_MATCH_ANY);

    return 0;
}
Robust Futex List 구조와 종료 시 정리 task_struct robust_list: *head pid: 1234 robust_list_head list.next → mutex_A futex_offset: 16 list_op_pending: NULL mutex_A __list.next → mutex_B __lock: 1234 (TID) mutex_B __list.next → head __lock: 1234 (TID) 순환 리스트 프로세스 비정상 종료 시 (do_exit → exit_robust_list) 1. robust_list_head에서 시작하여 리스트 순회 2. 각 futex word에 FUTEX_OWNER_DIED (bit 30) 설정 3. FUTEX_WAITERS (bit 31) 확인 → 대기자 있으면 FUTEX_WAKE 4. 대기자가 깨어나면 EOWNERDEAD 반환 → 사용자가 복구
Futex word 비트 레이아웃 (PI / Robust)
비트이름설명
0~29FUTEX_TID_MASK소유자의 TID (최대 2^30-1)
30FUTEX_OWNER_DIED소유자가 비정상 종료했음
31FUTEX_WAITERS커널에 대기자가 있음
EOWNERDEAD 처리: Robust mutex의 대기자가 FUTEX_LOCK_PI에서 EOWNERDEAD를 받으면, 공유 자원의 일관성을 복구한 후 pthread_mutex_consistent()를 호출해야 합니다. 복구 불가능하면 pthread_mutex_unlock()으로 다른 대기자에게도 EOWNERDEAD를 전파합니다.

robust_list 순회 알고리즘 상세

커널이 프로세스 종료 시 robust list를 순회하는 과정은 여러 edge case를 처리해야 합니다. 사용자 공간 메모리를 읽어야 하므로 페이지 폴트, 잘못된 포인터, 무한 루프 등에 대비합니다.

exit_robust_list() 순회 알고리즘 do_exit() → exit_robust_list() 1. task->robust_list에서 head 주소 읽기 2. get_user(entry, &head->list.next) 루프: entry != &head->list (limit=2048) 3. futex_addr = entry + futex_offset 4. handle_futex_death(futex_addr, curr): a) (futex_word & TID_MASK) == our_tid? → OWNER_DIED 설정 b) FUTEX_WAITERS 비트 있으면 → FUTEX_WAKE(1) 호출 5. get_user(next_entry, &entry->next) → entry = next_entry 6. list_op_pending 처리 (진행 중이던 잠금 연산) 정리 완료 안전 장치 limit=2048: 무한 루프 방지 get_user 실패: 잘못된 포인터 → 중단 TID 불일치: 소유자 아닌 mutex 건너뜀 cmpxchg 실패: 동시 수정 시 재시도 list_op_pending: 미완성 잠금 정리

exit_robust_list() 보안 고려사항

exit_robust_list()는 사용자 공간의 리스트 포인터를 따라가므로, 악의적으로 조작된 robust list가 커널을 공격하는 벡터가 될 수 있습니다.

exit_robust_list() 보안 검증 항목
검증방어 대상구현
순회 횟수 제한 (2048)무한 루프 DoSROBUST_LIST_LIMIT 상수
get_user() 에러 체크잘못된 포인터 역참조(Dereference)실패 시 순회 중단
TID 일치 확인다른 스레드의 mutex 침범uval & TID_MASK == our_tid
cmpxchg 루프동시 수정 race원자적 OWNER_DIED 설정
futex_offset 검증임의 메모리 쓰기pthread_mutex_t 구조 내 오프셋만 허용

32비트 호환 Robust List

64비트 커널에서 32비트 프로세스가 실행될 때, robust list의 포인터 크기가 다릅니다. 커널은 compat_exit_robust_list()로 이를 처리합니다.

/* kernel/futex/core.c — 32비트 호환 처리 */
#ifdef CONFIG_COMPAT
void compat_exit_robust_list(struct task_struct *curr)
{
    struct compat_robust_list_head __user *head;
    /* compat_robust_list: 32비트 포인터 사용 */
    /* 로직은 exit_robust_list()와 동일하지만
     * compat_ptr()로 32비트→64비트 포인터 변환 */
}

/* set_robust_list / get_robust_list도 compat 버전 존재:
 * - COMPAT_SYSCALL_DEFINE2(set_robust_list, ...)
 * - COMPAT_SYSCALL_DEFINE3(get_robust_list, ...)
 * 32비트 프로세스에서 sizeof(robust_list_head)가 다르므로
 * len 매개변수로 크기 검증 */
#endif
list_op_pending의 역할: 스레드가 pthread_mutex_lock() 도중 (robust list에 등록하기 전에) 죽을 수 있습니다. list_op_pending은 "현재 진행 중인 잠금 연산"의 futex 주소를 가리킵니다. 스레드가 정상적으로 잠금을 완료하면 list_op_pending = NULL로 초기화됩니다. 종료 시 list_op_pending이 NULL이 아니면 미완성 잠금도 정리합니다.

FUTEX_WAIT_BITSET / FUTEX_WAKE_BITSET

FUTEX_WAIT_BITSETFUTEX_WAKE_BITSET은 32비트 비트 마스크를 사용하여 대기자를 선택적으로 분류하고 깨우는 기능을 제공합니다. glibc의 pthread_cond_wait()pthread_cond_signal()/pthread_cond_broadcast() 구현에서 핵심적으로 사용됩니다.

동작 원리

/* FUTEX_WAIT_BITSET: val3에 비트 마스크 전달 */
syscall(SYS_futex, uaddr, FUTEX_WAIT_BITSET | FUTEX_PRIVATE_FLAG,
        expected_val, timeout, NULL, bitset_mask);

/* FUTEX_WAKE_BITSET: val3에 비트 마스크 전달 */
syscall(SYS_futex, uaddr, FUTEX_WAKE_BITSET | FUTEX_PRIVATE_FLAG,
        nr_wake, NULL, NULL, bitset_mask);

/* 웨이크업 조건: waiter.bitset & wake_bitset != 0
 * 즉, 비트가 하나라도 겹치면 깨움 */

/* FUTEX_BITSET_MATCH_ANY = 0xFFFFFFFF
 * → 모든 대기자를 깨움 (FUTEX_WAKE와 동일 효과) */

condvar에서의 활용 (glibc 2.34+)

glibc의 새로운 condvar 구현에서는 비트셋을 사용하여 pthread_cond_signal()(단일 깨움)과 pthread_cond_broadcast()(전체 깨움)을 효율적으로 구분합니다. 각 대기 그룹(generation)에 서로 다른 비트셋을 할당하여 정확한 타겟팅이 가능합니다.

FUTEX_WAIT vs FUTEX_WAIT_BITSET 비교
항목FUTEX_WAITFUTEX_WAIT_BITSET
비트 마스크없음 (전체 매칭)32비트 마스크
타임아웃 기준상대 시간 (duration)절대 시간 (timespec)
선택적 깨움불가 (순서대로 N개)비트 교집합으로 선택
glibc 사용처초기 구현condvar, barrier (현재 기본)
절대 시간 vs 상대 시간: FUTEX_WAIT의 timeout은 상대 시간(예: 5초 후)이지만, FUTEX_WAIT_BITSET의 timeout은 절대 시간(예: 13:05:30.000)입니다. glibc가 FUTEX_WAIT_BITSET을 선호하는 이유 중 하나가 이 절대 시간 지원입니다. 상대 시간은 시스템 콜 재시도 시 재계산이 필요하지만, 절대 시간은 그럴 필요가 없습니다.

BITSET을 활용한 condvar 그룹 관리

glibc 2.25+의 새로운 condvar 구현은 비트셋을 활용하여 대기자를 세대(generation)별로 분류합니다. 이를 통해 pthread_cond_signal()이 정확히 원하는 세대의 대기자만 깨울 수 있습니다.

/* glibc condvar의 비트셋 활용 상세 */
/* condvar 내부에 2개의 그룹(g1, g2)이 교대로 사용됨 */

/* 세대 0의 대기자: bitset = (1u << 0) = 0x00000001 */
/* 세대 1의 대기자: bitset = (1u << 16) = 0x00010000 */

/* pthread_cond_signal()에서:
 * 현재 활성 세대의 비트셋만 사용하여 FUTEX_WAKE_BITSET
 * → 다른 세대의 대기자는 영향 없음 */

/* 예시: 세대 0이 활성인 경우 */
futex_wake_bitset(&cond->g_signals[0],
                  1,           /* nr_wake = 1 */
                  0x00000001); /* 세대 0 비트셋 */

/* pthread_cond_broadcast()에서:
 * 세대 전환 + 해당 세대 전체 깨움 (FUTEX_CMP_REQUEUE 사용) */

/* 이 구현의 장점:
 * - signal/broadcast가 올바른 대기자에게만 도달
 * - 세대 전환으로 spurious wakeup 최소화
 * - WAIT_BITSET의 절대 타임아웃으로 시그널 재시도 불필요 */

BITSET의 다른 활용 사례

FUTEX_WAIT_BITSET 활용 패턴
패턴비트셋 할당깨움 방법사용처
condvar 세대 분리그룹별 다른 비트해당 그룹 비트로 WAKE_BITSETglibc pthread_cond
다중 이벤트 타입이벤트 종류별 비트원하는 이벤트 비트로 깨움커스텀 이벤트 시스템
reader/writer 분리reader=bit0, writer=bit1writer 우선 깨움 가능커스텀 rwlock
절대 타임아웃 필요MATCH_ANY (0xFFFFFFFF)일반 WAKE와 동일타임아웃이 있는 모든 대기
/* 비트셋을 활용한 다중 이벤트 타입 시스템 예시 */
#define EVENT_DATA_READY   (1u << 0)  /* 데이터 준비됨 */
#define EVENT_ERROR        (1u << 1)  /* 에러 발생 */
#define EVENT_SHUTDOWN     (1u << 2)  /* 종료 요청 */
#define EVENT_ALL          0xFFFFFFFF

/* 특정 이벤트만 대기 */
syscall(SYS_futex, &event_word, FUTEX_WAIT_BITSET_PRIVATE,
        0, &timeout, NULL, EVENT_DATA_READY | EVENT_ERROR);

/* 에러 이벤트 대기자만 깨움 */
event_word = 1;
syscall(SYS_futex, &event_word, FUTEX_WAKE_BITSET_PRIVATE,
        INT_MAX, NULL, NULL, EVENT_ERROR);

FUTEX_REQUEUE / FUTEX_CMP_REQUEUE

Requeue 연산은 thundering herd 문제를 해결하는 핵심 메커니즘입니다. 조건 변수에서 broadcast 시 모든 대기자를 깨우면, 깨어난 스레드들이 뮤텍스를 동시에 획득하려 하여 극심한 경합이 발생합니다. Requeue는 대기자를 깨우지 않고 다른 futex의 대기 큐로 직접 이동시켜 이 문제를 회피합니다.

Thundering Herd 시나리오

/* pthread_cond_broadcast() without requeue (비효율) */
/* 1. condvar에서 100개 스레드를 전부 FUTEX_WAKE
 * 2. 100개 스레드가 동시에 깨어남
 * 3. 100개 스레드가 동시에 mutex 획득 시도
 * 4. 1개만 성공, 99개는 다시 FUTEX_WAIT → 극심한 경합 */

/* pthread_cond_broadcast() with CMP_REQUEUE (효율적) */
/* 1. condvar에서 1개 WAKE + 99개를 mutex 대기 큐로 REQUEUE
 * 2. 1개 스레드만 깨어나 mutex 획득
 * 3. 해제 시 mutex 대기 큐에서 다음 1개 WAKE
 * 4. 순차적으로 처리: 99회의 불필요한 wake-sleep 사이클 제거 */
FUTEX_CMP_REQUEUE: Thundering Herd 해결 Before (condvar broadcast) condvar 대기 큐 (uaddr) T1 T2 T3 T4 4개 스레드 대기 중 mutex 대기 큐 (uaddr2) (비어있음) FUTEX_CMP_REQUEUE(uaddr, 1, 3, uaddr2, expected) After condvar 대기 큐 (비어있음) mutex 대기 큐 T2 T3 T4 T1: WAKE (실행!) nr_wake=1(T1 깨움), nr_requeue=3(T2,T3,T4를 mutex 큐로 이동)

FUTEX_REQUEUE vs FUTEX_CMP_REQUEUE

FUTEX_REQUEUE는 값 비교 없이 requeue를 수행하므로 race condition이 존재합니다. FUTEX_CMP_REQUEUE*uaddr == val3을 먼저 확인하여 안전합니다. 현재 glibc는 FUTEX_CMP_REQUEUE만 사용합니다.

FUTEX_REQUEUE 사용 금지: FUTEX_REQUEUE는 ABA 문제에 취약합니다. condvar의 시퀀스 번호가 requeue 도중 변경되면 대기자가 잘못된 대기 큐로 이동할 수 있습니다. 항상 FUTEX_CMP_REQUEUE를 사용하세요.

CMP_REQUEUE 커널 구현 상세

/* kernel/futex/requeue.c — futex_requeue() 핵심 (간소화) */
static int futex_requeue(u32 __user *uaddr1, unsigned int flags,
    u32 __user *uaddr2, int nr_wake, int nr_requeue,
    u32 *cmpval, int requeue_pi)
{
    struct futex_hash_bucket *hb1, *hb2;
    union futex_key key1, key2;
    struct futex_q *this, *next;
    DEFINE_WAKE_Q(wake_q);
    int task_count = 0, ret;

    /* 1. 두 futex의 키 생성 */
    ret = get_futex_key(uaddr1, flags, &key1, FUTEX_READ);
    ret = get_futex_key(uaddr2, flags, &key2,
                        requeue_pi ? FUTEX_WRITE : FUTEX_READ);

    /* 2. 두 해시 버킷 잠금 (주소 순서로 교착 방지) */
    hb1 = futex_hash(&key1);
    hb2 = futex_hash(&key2);
    double_lock_hb(hb1, hb2);

    /* 3. 비교값 확인 (CMP_REQUEUE만) */
    if (cmpval) {
        u32 curval;
        ret = get_futex_value_locked(&curval, uaddr1);
        if (curval != *cmpval) {
            ret = -EAGAIN; /* 값이 변경됨: requeue 취소 */
            goto out;
        }
    }

    /* 4. uaddr1 대기 큐에서 처리 */
    plist_for_each_entry_safe(this, next, &hb1->chain, list) {
        if (!futex_match(&this->key, &key1))
            continue;

        if (task_count < nr_wake) {
            /* nr_wake개는 실제로 깨움 */
            futex_wake_mark(&wake_q, this);
            task_count++;
        } else if (task_count < nr_wake + nr_requeue) {
            /* 나머지는 uaddr2 대기 큐로 이동 */
            requeue_futex(this, hb1, hb2, &key2);
            task_count++;
        } else {
            break;
        }
    }

out:
    double_unlock_hb(hb1, hb2);
    wake_up_q(&wake_q);
    return ret;
}

/* requeue_futex: 대기자를 다른 해시 버킷으로 이동 */
static void requeue_futex(struct futex_q *q,
    struct futex_hash_bucket *hb1,
    struct futex_hash_bucket *hb2,
    union futex_key *key2)
{
    /* hb1에서 제거 */
    plist_del(&q->list, &hb1->chain);
    atomic_dec(&hb1->waiters);

    /* 키 변경 */
    q->key = *key2;

    /* hb2에 삽입 */
    plist_add(&q->list, &hb2->chain);
    atomic_inc(&hb2->waiters);
    q->lock_ptr = &hb2->lock;
}

FUTEX_CMP_REQUEUE_PI: PI futex로의 requeue

FUTEX_CMP_REQUEUE_PI는 condvar에서 PI mutex로 대기자를 이동할 때 사용됩니다. 일반 requeue와 달리 대상 futex가 PI futex이므로 futex_pi_state를 설정하고 rt_mutex에 대기자를 등록해야 합니다. 이것이 CVE-2014-3153의 원인이 된 복잡한 경로입니다.

REQUEUE_PI 안전 조건: FUTEX_CMP_REQUEUE_PI는 반드시 FUTEX_WAIT_REQUEUE_PI로 대기 중인 태스크에 대해서만 사용해야 합니다. 일반 FUTEX_WAIT로 대기 중인 태스크를 PI futex로 requeue하면 futex_pi_state가 올바르게 설정되지 않아 use-after-free 또는 잘못된 우선순위 상속이 발생합니다. 커널은 이를 검증하기 위해 requeue_state 원자 변수를 사용합니다.

NUMA-aware Futex 최적화

NUMA(Non-Uniform Memory Access) 시스템에서 futex 해시 테이블의 배치는 성능에 큰 영향을 미칩니다. futex word가 위치한 메모리 노드와 해시 버킷이 위치한 메모리 노드가 다르면 원격 메모리 접근 지연(Latency)이 발생합니다.

해시 테이블 NUMA 배치

Linux 커널의 alloc_large_system_hash()는 해시 테이블을 부팅 시 할당하며, 기본적으로 노드 0에 집중됩니다. 대형 NUMA 시스템에서는 이것이 병목이 될 수 있습니다.

NUMA 환경에서 futex 성능 영향 요인
요인로컬 노드원격 노드영향
해시 버킷 접근~100ns~300nsspinlock 보유 시간 증가
futex_q 할당스택(로컬)스택(로컬)영향 없음 (futex_q는 스택 변수)
futex word 접근~100ns~300nsget_user 비용 증가
캐시라인 바운싱L1/L2 히트디렉토리 조회고경합 시 큰 영향

최적화 전략

# NUMA 노드별 futex 관련 캐시 미스 측정
$ perf stat -e cache-misses,cache-references \
    -C 0-15  taskset -c 0-15 ./futex_bench

# numactl로 메모리 바인딩하여 futex word를 로컬 노드에 배치
$ numactl --membind=0 --cpunodebind=0 ./my_app

# NUMA 토폴로지 확인
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 32768 MB
node 1 cpus: 8 9 10 11 12 13 14 15
node 1 size: 32768 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10
NUMA 최적화 팁: 멀티소켓 시스템에서 FUTEX_PRIVATE_FLAG를 사용하면 해시 키가 가상 주소 기반이므로 동일 NUMA 노드의 CPU들이 같은 캐시라인을 공유할 가능성이 높아 유리합니다. 또한, 스레드 풀을 NUMA 노드별로 분리하고 각 풀에 별도의 뮤텍스를 사용하면 cross-node futex 경합을 크게 줄일 수 있습니다.

NUMA-aware 해시 분산 메커니즘

현재 Linux의 futex 해시 테이블은 단일 전역 배열입니다. NUMA 시스템에서 원격 노드의 해시 버킷에 접근하면 메모리 지연이 2~3배 증가합니다. 향후 노드별 해시 테이블 분리가 논의되고 있습니다.

# NUMA 시스템에서 futex 관련 원격 메모리 접근 측정
$ perf stat -e 'node-loads,node-load-misses' \
    taskset -c 0-7 ./futex_bench --threads=8
#  Performance counter stats:
#       1,234,567  node-loads
#         345,678  node-load-misses  (28% 원격 접근!)

# 개선: 스레드를 동일 NUMA 노드에 배치
$ numactl --cpunodebind=0 --membind=0 ./futex_bench --threads=8
#       1,234,567  node-loads
#          12,345  node-load-misses  (1% 미만!)

# futex 해시 테이블의 물리 메모리 위치 확인
$ cat /proc/pagetypeinfo | grep futex
# (직접 확인은 어렵지만, dmesg에서 할당 로그 확인 가능)
$ dmesg | grep "futex hash"
# futex hash table entries: 65536 (order: 10, 4194304 bytes, linear)
NUMA false sharing: futex word 자체가 NUMA 노드 경계를 넘어 공유되면, 캐시 코히어런스 프로토콜(MESIF/MOESI)이 캐시라인을 원격 노드로 전송해야 합니다. 이 비용은 로컬 캐시 접근의 3~5배입니다. numactl --membind로 futex word를 포함한 공유 메모리를 특정 노드에 배치하면 이 문제를 완화할 수 있습니다.

futex2 / futex_waitv (벡터 대기)

futex_waitv()는 Linux 5.16(2021년, Andre Almeida)에 도입된 새 시스템 콜로, 여러 futex word를 동시에 대기할 수 있습니다. select()/poll()의 futex 버전입니다. Wine/Proton의 Windows WaitForMultipleObjects() 에뮬레이션과 게임 엔진의 다중 이벤트 대기에서 핵심적으로 사용됩니다.

시스템 콜 인터페이스

/* include/uapi/linux/futex.h */
struct futex_waitv {
    __u64 val;       /* 기대 값 */
    __u64 uaddr;     /* futex word 주소 */
    __u32 flags;     /* FUTEX_32, FUTEX_PRIVATE_FLAG 등 */
    __u32 __reserved;
};

/* syscall 원형 */
long sys_futex_waitv(
    struct futex_waitv __user *waiters,  /* 대기 벡터 배열 */
    unsigned int nr_futexes,             /* 배열 크기 (최대 128) */
    unsigned int flags,                  /* 현재 0 */
    struct __kernel_timespec __user *timeout,
    clockid_t clockid                    /* CLOCK_MONOTONIC 등 */
);

/* 반환값: 깨어난 futex의 인덱스 (0-based), 에러 시 음수 */

사용 예: WaitForMultipleObjects 에뮬레이션

/* Wine/Proton — Windows WaitForMultipleObjects 에뮬레이션 */
#include <linux/futex.h>
#include <sys/syscall.h>

int wait_for_multiple_events(uint32_t *events[], int count,
                              struct timespec *timeout)
{
    struct futex_waitv waiters[128];
    int i;

    for (i = 0; i < count && i < 128; i++) {
        waiters[i].uaddr = (uintptr_t)events[i];
        waiters[i].val = 0;  /* 아직 시그널되지 않은 상태 */
        waiters[i].flags = FUTEX_32 | FUTEX_PRIVATE_FLAG;
        waiters[i].__reserved = 0;
    }

    /* 여러 futex 중 하나라도 변경되면 깨어남 */
    long ret = syscall(SYS_futex_waitv, waiters, count,
                       0, timeout, CLOCK_MONOTONIC);

    if (ret >= 0)
        return ret;  /* 깨어난 이벤트의 인덱스 */

    return -errno;
}
futex_waitv: 다중 Futex 벡터 대기 Thread futex_waitv() futex[0]: event_A val=0, flags=PRIVATE futex[1]: event_B val 변경됨! futex[2]: event_C val=0, flags=PRIVATE futex[3]: event_D val=0, flags=SHARED 커널: futex_wait_multiple() 1. 각 futex에 대해 futex_key 생성 2. 각 해시 버킷에 futex_q 삽입 3. schedule() → SLEEP 4. 어느 하나라도 WAKE되면: → 나머지 모두 큐에서 제거 → 깨어난 인덱스(1) 반환 return 1 (event_B가 시그널됨) 최대 128개 futex 동시 대기, 하나라도 변경되면 즉시 반환
Steam Deck / Proton: Valve의 Proton(Wine 포크)은 futex_waitv()를 Windows 게임의 WaitForMultipleObjects() 에뮬레이션에 사용합니다. 이전에는 이를 위해 각 이벤트마다 별도 스레드를 생성했으나, futex_waitv() 덕분에 단일 스레드로 처리할 수 있게 되어 게임 호환성과 성능이 크게 개선되었습니다.

futex_waitv 내부 구현: 벡터 대기 상태 머신

futex_waitv()는 여러 futex를 동시에 대기하는 복잡한 상태 머신을 관리합니다. 모든 futex에 대해 원자적으로 대기 큐에 삽입하고, 하나라도 깨어나면 나머지를 정리합니다.

futex_waitv 벡터 대기 상태 머신 INIT futex_vector[] 할당 사용자 데이터 복사 ENQUEUE 각 futex_key 생성 해시 버킷에 futex_q 삽입 값 변경 감지! 즉시 반환 (index) SLEEP set_current_state(INTERRUPTIBLE) freezable_schedule() WOKEN 어떤 futex가 시그널? futexv[i].q.woken 확인 UNQUEUE ALL 나머지 모든 futex_q 해시 버킷에서 제거 return index 깨어나는 원인과 처리 FUTEX_WAKE: 다른 스레드가 해당 futex에 WAKE → q.woken = true → 해당 인덱스 반환 타임아웃: hrtimer 만료 → 깨어남 → -ETIMEDOUT 반환 시그널: SIGINT 등 수신 → -ERESTARTSYS 반환 → 커널이 자동 재시도 또는 EINTR 전달 값 변경 (enqueue 중): i번째 futex 등록 중 *uaddr != val 감지 → 0..i-1 unqueue 후 즉시 i 반환

futex2 크기 변형: u8/u16/u32/u64

futex_waitv의 flags 필드는 futex word의 크기를 지정할 수 있도록 설계되었습니다. 현재 Linux에서는 FUTEX_32만 지원되지만, 향후 다른 크기가 추가될 수 있습니다.

/* include/uapi/linux/futex.h — futex2 크기 플래그 */
#define FUTEX_SIZE_MASK   0x03
#define FUTEX_8           0x00  /* 8비트 futex (미구현) */
#define FUTEX_16          0x01  /* 16비트 futex (미구현) */
#define FUTEX_32          0x02  /* 32비트 futex (현재 유일하게 지원) */
#define FUTEX_64          0x03  /* 64비트 futex (미구현) */

/* futex_waitv에서 사용:
 * waiters[i].flags = FUTEX_32 | FUTEX_PRIVATE_FLAG;
 *
 * 왜 다른 크기가 필요한가?
 * - u8/u16: 메모리 절약 (임베디드, 대량 futex)
 * - u64: 64비트 원자적 연산이 필요한 경우 (시퀀스 번호 등)
 * - Windows 호환: WaitOnAddress()는 1/2/4/8 바이트 지원
 *
 * 현재 상태: 커널에 검증 코드만 있고 실제 u8/u16/u64 경로는 미구현
 * → FUTEX_32 이외를 전달하면 -EINVAL */

futex2 NUMA 확장 (제안)

futex2 설계에는 NUMA-aware 해시 분산이 고려되어 있습니다. 현재 구현에서는 아직 활성화되지 않았지만, 향후 대규모 NUMA 시스템에서 해시 버킷을 NUMA 노드별로 분리하여 원격 메모리 접근을 줄이는 방안이 논의 중입니다.

futex2 확장 로드맵 (커뮤니티 논의 중)
확장상태목적예상 효과
FUTEX_8/16/64 크기플래그 정의됨, 미구현다양한 크기 futex word메모리 절약, Windows 호환
NUMA-aware 해시논의 중노드별 해시 테이블 분리원격 메모리 접근 감소
FUTEX_NUMA 플래그제안 단계NUMA 노드 힌트 전달해시 지역성 향상
futex_wake_multiple논의 중벡터 깨움 (waitv의 대칭)다중 이벤트 시그널링

Wine/Proton의 futex_waitv 활용 상세

Wine/Proton에서 futex_waitv가 없던 시절의 비효율성과 도입 후의 개선을 비교합니다.

/* Wine/Proton: futex_waitv 도입 전 (비효율적) */
/* WaitForMultipleObjects(3, handles, FALSE, INFINITE) 에뮬레이션:
 *
 * 방법 1: 스레드 풀 사용
 * → 각 핸들마다 대기 스레드 1개 생성 (3개 스레드 추가!)
 * → 어떤 스레드가 깨어나면 나머지에 signal
 * → 문제: 스레드 생성/해제 오버헤드, 메모리 사용
 *
 * 방법 2: busy polling
 * → 모든 핸들을 번갈아 FUTEX_WAIT(timeout=짧음) 시도
 * → 문제: CPU 낭비, 지연 시간 증가
 */

/* Wine/Proton: futex_waitv 도입 후 (효율적) */
NTSTATUS wait_for_multiple_objects(DWORD count, HANDLE *handles,
                                    BOOLEAN wait_all, LARGE_INTEGER *timeout)
{
    struct futex_waitv waiters[MAXIMUM_WAIT_OBJECTS]; /* 최대 64 */
    int i;

    for (i = 0; i < count; i++) {
        struct wine_object *obj = handle_to_object(handles[i]);
        waiters[i].uaddr = (uint64_t)&obj->signaled;
        waiters[i].val = 0; /* 아직 시그널되지 않은 값 */
        waiters[i].flags = FUTEX_32 | FUTEX_PRIVATE_FLAG;
        waiters[i].__reserved = 0;
    }

    long idx = syscall(SYS_futex_waitv, waiters, count,
                       0, timeout_ptr, CLOCK_MONOTONIC);

    if (idx >= 0)
        return WAIT_OBJECT_0 + idx;

    if (errno == ETIMEDOUT)
        return WAIT_TIMEOUT;

    return STATUS_UNSUCCESSFUL;
}
/* → 추가 스레드 없음, 단일 syscall, CPU 낭비 없음
 * → 게임 호환성 대폭 개선 (특히 DirectX 동기화 관련) */
Steam Deck 성과: Valve는 futex_waitv를 도입한 Linux 5.16 커널을 Steam Deck의 기본 커널로 채택했습니다. 이전에 호환성 문제로 실행되지 않던 여러 Windows 게임이 futex_waitv 덕분에 Proton에서 정상 동작하게 되었습니다. 특히 WaitForMultipleObjects()를 집중적으로 사용하는 멀티스레드 게임 엔진(Unreal Engine, Unity 등)에서 효과가 컸습니다.

glibc pthread → futex 매핑

glibc의 NPTL(Native POSIX Threads Library)은 POSIX 동기화 API를 내부적으로 futex 시스템 콜로 변환합니다. 이 매핑을 이해하면 pthread를 사용하는 애플리케이션의 커널 수준 동작을 정확히 추적할 수 있습니다.

glibc NPTL pthread → futex 시스템 콜 매핑 POSIX API (glibc) futex 연산 Futex word 의미 pthread_mutex_lock() FUTEX_WAIT_BITSET 0:free, 1:locked, 2:contended pthread_mutex_unlock() FUTEX_WAKE nr_wake=1 pthread_mutex_lock(PI) FUTEX_LOCK_PI TID | WAITERS | DIED bits pthread_cond_wait() FUTEX_WAIT_BITSET 시퀀스 번호 (generation) pthread_cond_signal() FUTEX_WAKE_BITSET nr_wake=1, 해당 그룹 비트셋 pthread_cond_broadcast() CMP_REQUEUE condvar → mutex 큐로 이동 pthread_rwlock_rdlock() FUTEX_WAIT_BITSET readers count + writer flag pthread_rwlock_wrlock() FUTEX_WAIT_BITSET writer 전용 비트셋 pthread_barrier_wait() FUTEX_WAIT / WAKE 참가자 count + 세대 번호 sem_wait() FUTEX_WAIT_BITSET 세마포어 count (0일 때 대기) sem_post() FUTEX_WAKE nr_wake=1

glibc mutex 내부 상태 (lowlevellock)

/* glibc nptl/lowlevellock.h — 일반 mutex의 futex word 상태 */
/* 0: 잠금 해제 (unlocked)
 * 1: 잠겼으나 대기자 없음 (locked, no waiters)
 * 2: 잠기고 대기자 있음 (locked, with waiters)
 *
 * unlock 시:
 *   atomic_exchange(&futex, 0)의 이전값이 2이면 → FUTEX_WAKE 호출
 *   이전값이 1이면 → 커널 호출 불필요 */

static inline void
__lll_lock(int *futex, int private)
{
    /* Fast path: 0 → 1 */
    if (__glibc_likely(atomic_compare_exchange_weak_acquire(
            futex, 0, 1)))
        return;

    /* Medium path: 이미 잠겼지만 대기자 없음 → 1 → 2 */
    if (*futex != 2)
        *futex = atomic_exchange_acquire(futex, 2);

    /* Slow path: 커널 대기 */
    while (*futex == 2 || atomic_exchange_acquire(futex, 2) != 0) {
        futex_wait((unsigned int *)futex, 2, private);
    }
}

static inline void
__lll_unlock(int *futex, int private)
{
    /* 이전값이 2가 아니면 (대기자 없음) → 커널 호출 불필요 */
    if (atomic_exchange_release(futex, 0) != 1)
        futex_wake((unsigned int *)futex, 1, private);
}

glibc condvar 내부 (새 구현, glibc 2.25+)

/* glibc nptl/pthread_cond_wait.c — 간소화 */
int __pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
{
    /* 1. 현재 세대(generation)의 시퀀스 번호 읽기 */
    uint64_t seq = atomic_load_relaxed(&cond->__data.__wseq);
    unsigned int g = seq & 1;  /* 현재 그룹 번호 */

    /* 2. 대기자 수 증가 */
    atomic_fetch_add_relaxed(&cond->__data.__g_refs[g], 2);

    /* 3. mutex 해제 */
    pthread_mutex_unlock(mutex);

    /* 4. futex 대기 (비트셋으로 그룹 구분) */
    unsigned int bitset = 1u << (g * 16);
    futex_wait_bitset(&cond->__data.__g_signals[g],
                      signals_val, private, NULL, bitset);

    /* 5. 깨어남 → mutex 재획득 */
    pthread_mutex_lock(mutex);
    return 0;
}

pthread_mutex 내부 구현 상세 (lowlevellock.h)

glibc의 pthread_mutex_t는 내부적으로 여러 타입을 지원하며, 각 타입에 따라 futex 호출 패턴이 다릅니다.

pthread_mutex_t 타입별 futex 사용 패턴
타입futex word 의미Lock 연산Unlock 연산
NORMAL0:free, 1:locked, 2:contendedcmpxchg 0→1, 실패시 FUTEX_WAIT_BITSET(2)xchg→0, 이전값=2이면 FUTEX_WAKE(1)
ERRORCHECK0:free, TID:locked, TID|2:contended소유자 체크 후 NORMAL과 동일소유자 불일치 시 EPERM
RECURSIVE0:free, TID:locked + count 별도TID 일치시 count++, 아니면 대기count-- 후 0이면 해제
ADAPTIVE_NPNORMAL과 동일스핀 후 FUTEX_WAITNORMAL과 동일
PI (PRIO_INHERIT)0:free, TID|WAITERS|DIEDFUTEX_LOCK_PIFUTEX_UNLOCK_PI
PP (PRIO_PROTECT)ceiling priority 인코딩우선순위 ceiling 설정 후 lock우선순위 복원 후 unlock

pthread_cond 내부: condvar와 futex의 관계

glibc 2.25+의 새로운 condvar 구현(Torvald Riegel 설계)은 이전 구현의 여러 문제점(steal wakeup, ABA)을 해결합니다. 2개의 대기 그룹과 비트셋을 사용합니다.

/* glibc pthread_cond_t 내부 구조 (간소화) */
struct pthread_cond_t.__data {
    unsigned long long __wseq;       /* 대기 시퀀스 번호 */
    unsigned long long __g1_start;   /* 그룹 1 시작 위치 */
    unsigned int __g_refs[2];        /* 그룹별 참조 카운트 */
    unsigned int __g_size[2];        /* 그룹별 대기자 수 */
    unsigned int __g1_orig_size;     /* 그룹 1 원래 크기 */
    unsigned int __wrefs;            /* 총 참조 카운트 */
    unsigned int __g_signals[2];     /* 그룹별 시그널 (futex word!) */
    /* __g_signals[g]가 각 그룹의 futex word 역할 */
};

/* pthread_cond_wait() 핵심 흐름:
 * 1. wseq에서 현재 그룹 g 결정 (bit 0)
 * 2. g_refs[g]++ (그룹 참조)
 * 3. mutex 해제
 * 4. FUTEX_WAIT_BITSET(&g_signals[g], expected, bitset=1<<(g*16))
 * 5. 깨어남 → mutex 재획득
 * 6. g_refs[g]-- (그룹 참조 해제)
 */

/* pthread_cond_signal() 핵심 흐름:
 * 1. 현재 활성 그룹 g 확인
 * 2. g_signals[g] 업데이트
 * 3. FUTEX_WAKE_BITSET(&g_signals[g], 1, bitset=1<<(g*16))
 */

/* pthread_cond_broadcast() 핵심 흐름:
 * 1. 세대 전환: wseq++ (g 값 반전)
 * 2. g_signals[g] 업데이트
 * 3. FUTEX_CMP_REQUEUE(&g_signals[g], 1, INT_MAX, &mutex.__lock, ...)
 *    → 1개만 깨우고 나머지는 mutex 대기 큐로 이동
 */

pthread_rwlock 내부: 읽기/쓰기 잠금의 futex 매핑

/* glibc pthread_rwlock_t 내부 (간소화) */
/* __readers: 현재 reader 수 + writer flag + 대기 플래그
 * 비트 레이아웃:
 *   bits 0-30: reader count
 *   bit 31: writer flag (WRPHASE)
 *
 * __writers_futex: writer 대기용 futex word
 * __wrhandover_futex: writer → reader 전환용
 *
 * 읽기 잠금:
 *   1. __readers++ (원자적)
 *   2. WRPHASE 비트 확인: writer 있으면 FUTEX_WAIT_BITSET(__readers, ...)
 *
 * 쓰기 잠금:
 *   1. __readers에 WRPHASE 설정 시도
 *   2. reader count > 0이면 FUTEX_WAIT_BITSET(__writers_futex, ...)
 *
 * 읽기 해제:
 *   1. __readers-- (원자적)
 *   2. 마지막 reader이고 writer 대기 시 FUTEX_WAKE(__writers_futex, 1)
 *
 * 쓰기 해제:
 *   1. WRPHASE 클리어
 *   2. reader 대기 시 FUTEX_WAKE(__readers, INT_MAX) 또는
 *      writer 대기 시 FUTEX_WAKE(__writers_futex, 1)
 */

pthread_barrier 내부: 장벽 동기화

/* glibc pthread_barrier_t 내부 (간소화) */
/* 동작 원리:
 * N개 스레드가 모두 도착할 때까지 대기,
 * 마지막 스레드가 도착하면 모두 깨움
 *
 * __count: 남은 대기 스레드 수
 * __current_round: 현재 라운드 번호 (세대)
 * __futex: 대기/깨움용 futex word
 */

/* pthread_barrier_wait() 흐름:
 * 1. round = __current_round 기록
 * 2. __count-- (원자적)
 * 3. __count > 0이면:
 *      FUTEX_WAIT(&__futex, round) → 슬립
 * 4. __count == 0이면 (마지막 도착자):
 *      __count = barrier_count (리셋)
 *      __current_round++ (다음 세대)
 *      __futex++ (값 변경으로 대기자 깨움)
 *      FUTEX_WAKE(&__futex, INT_MAX)
 * 5. return PTHREAD_BARRIER_SERIAL_THREAD (마지막 도착자만)
 *    또는 return 0 (나머지)
 */

사용자 공간 동기화 라이브러리 패턴

Futex를 직접 사용하는 것은 오류 가능성이 높으므로 일반적으로 glibc pthread를 권장하지만, 특수한 성능 요구사항이 있는 경우 futex 위에 맞춤 동기화 프리미티브를 구축합니다.

간단한 Futex 기반 Mutex 구현

/* 최소한의 사용자 공간 futex mutex */
#include <linux/futex.h>
#include <sys/syscall.h>
#include <stdatomic.h>

typedef struct { atomic_int val; } futex_mutex_t;

#define FUTEX_MUTEX_INIT {0}

static inline long futex_syscall(int *addr, int op, int val,
    const struct timespec *to, int *addr2, int val3) {
    return syscall(SYS_futex, addr, op, val, to, addr2, val3);
}

void futex_mutex_lock(futex_mutex_t *m) {
    int c;

    /* Fast path: 0 → 1 */
    c = 0;
    if (atomic_compare_exchange_strong_explicit(&m->val, &c, 1,
            memory_order_acquire, memory_order_relaxed))
        return;

    /* 이미 잠겨 있음: 대기자 있음(2)으로 표시 */
    if (c != 2)
        c = atomic_exchange_explicit(&m->val, 2,
                memory_order_acquire);

    /* Slow path: 커널 대기 */
    while (c != 0) {
        futex_syscall((int *)&m->val,
            FUTEX_WAIT | FUTEX_PRIVATE_FLAG, 2, NULL, NULL, 0);
        c = atomic_exchange_explicit(&m->val, 2,
                memory_order_acquire);
    }
}

void futex_mutex_unlock(futex_mutex_t *m) {
    /* 2 → 0: 대기자가 있을 수 있으므로 깨움 */
    if (atomic_exchange_explicit(&m->val, 0,
            memory_order_release) != 1) {
        futex_syscall((int *)&m->val,
            FUTEX_WAKE | FUTEX_PRIVATE_FLAG, 1, NULL, NULL, 0);
    }
}

Futex 기반 이벤트 (One-shot Signal)

/* 간단한 이벤트: 하나의 생산자가 여러 소비자에게 시그널 */
typedef struct { atomic_int signaled; } futex_event_t;

void futex_event_wait(futex_event_t *ev) {
    while (atomic_load_explicit(&ev->signaled,
            memory_order_acquire) == 0) {
        futex_syscall((int *)&ev->signaled,
            FUTEX_WAIT | FUTEX_PRIVATE_FLAG, 0, NULL, NULL, 0);
    }
}

void futex_event_signal(futex_event_t *ev) {
    atomic_store_explicit(&ev->signaled, 1, memory_order_release);
    /* INT_MAX: 모든 대기자 깨움 */
    futex_syscall((int *)&ev->signaled,
        FUTEX_WAKE | FUTEX_PRIVATE_FLAG, INT_MAX, NULL, NULL, 0);
}

Futex 기반 Read-Write Lock

/* 간소화된 read-write lock
 * state: 0 = free, >0 = reader count, -1 = writer locked
 */
typedef struct {
    atomic_int state;
    atomic_int writer_wait;  /* 대기 중인 writer 수 */
} futex_rwlock_t;

void futex_rwlock_rdlock(futex_rwlock_t *rw) {
    for (;;) {
        int s = atomic_load_acquire(&rw->state);
        if (s >= 0 &&
            atomic_load_acquire(&rw->writer_wait) == 0) {
            if (atomic_compare_exchange_weak_acquire(
                    &rw->state, &s, s + 1))
                return;
        } else {
            futex_syscall((int *)&rw->state,
                FUTEX_WAIT | FUTEX_PRIVATE_FLAG, s,
                NULL, NULL, 0);
        }
    }
}

void futex_rwlock_wrlock(futex_rwlock_t *rw) {
    atomic_fetch_add(&rw->writer_wait, 1);
    int expected = 0;
    while (!atomic_compare_exchange_weak_acquire(
            &rw->state, &expected, -1)) {
        futex_syscall((int *)&rw->state,
            FUTEX_WAIT | FUTEX_PRIVATE_FLAG, expected,
            NULL, NULL, 0);
        expected = 0;
    }
    atomic_fetch_sub(&rw->writer_wait, 1);
}
주의: 위 예제들은 교육 목적의 간소화된 구현입니다. 프로덕션에서는 glibc의 pthread_mutex_t, pthread_rwlock_t를 사용하세요. 이들은 수십 년간의 버그 수정, 최적화, 아키텍처별 튜닝이 반영되어 있습니다.

Futex 기반 카운팅 세마포어

/* Futex 기반 카운팅 세마포어 구현 */
typedef struct {
    atomic_int count;  /* 세마포어 카운트 */
} futex_sem_t;

void futex_sem_init(futex_sem_t *sem, int initial) {
    atomic_store(&sem->count, initial);
}

void futex_sem_wait(futex_sem_t *sem) {
    for (;;) {
        int c = atomic_load_acquire(&sem->count);
        if (c > 0) {
            /* 카운트 > 0: 즉시 획득 시도 */
            if (atomic_compare_exchange_weak(&sem->count, &c, c - 1))
                return;  /* Fast path 성공 */
        } else {
            /* 카운트 == 0: 커널 대기 */
            futex_syscall((int *)&sem->count,
                FUTEX_WAIT | FUTEX_PRIVATE_FLAG, 0,
                NULL, NULL, 0);
        }
    }
}

void futex_sem_post(futex_sem_t *sem) {
    int old = atomic_fetch_add_release(&sem->count, 1);
    if (old == 0) {
        /* 이전 카운트가 0이었으면 대기자가 있을 수 있음 */
        futex_syscall((int *)&sem->count,
            FUTEX_WAKE | FUTEX_PRIVATE_FLAG, 1,
            NULL, NULL, 0);
    }
}
/* glibc sem_wait/sem_post도 내부적으로 유사한 패턴 사용 */

스핀록-Futex 하이브리드 패턴

짧은 임계 구역에서는 순수 스핀록이, 긴 임계 구역에서는 futex가 효율적입니다. 하이브리드 접근법은 짧은 스핀 후 futex로 전환합니다.

/* 스핀-futex 하이브리드: 튜닝 가능한 스핀 횟수 */
typedef struct {
    atomic_int val;
    int max_spin;  /* 스핀 횟수 상한 */
} hybrid_mutex_t;

void hybrid_lock(hybrid_mutex_t *m) {
    int c = 0;

    /* Fast path: 즉시 획득 */
    if (atomic_compare_exchange_strong(&m->val, &c, 1))
        return;

    /* Spin phase: CPU에서 짧은 대기 */
    for (int i = 0; i < m->max_spin; i++) {
        c = 0;
        if (atomic_compare_exchange_weak(&m->val, &c, 1))
            return;
        __asm__ volatile("pause" ::: "memory"); /* x86 */
    }

    /* Futex phase: 커널 대기로 전환 */
    if (c != 2)
        c = atomic_exchange(&m->val, 2);
    while (c != 0) {
        futex_syscall((int *)&m->val,
            FUTEX_WAIT | FUTEX_PRIVATE_FLAG, 2, NULL, NULL, 0);
        c = atomic_exchange(&m->val, 2);
    }
}

/* max_spin 튜닝:
 * - CS < 100ns: max_spin = 100~200
 * - CS 100ns~1us: max_spin = 50~100
 * - CS > 1us: max_spin = 0 (즉시 futex)
 * glibc ADAPTIVE_NP는 max_spin=100 기본값 사용 */

Lock Elision (TSX/HLE와 Futex)

Lock Elision은 Intel TSX(Transactional Synchronization Extensions)의 HLE(Hardware Lock Elision) 또는 RTM(Restricted Transactional Memory)을 사용하여 실제 잠금 없이 트랜잭션(Transaction)으로 임계 구역을 실행하는 기법입니다. glibc는 이를 futex 기반 mutex의 최적화 경로로 지원했습니다.

TSX Lock Elision 동작 원리

/* glibc의 TSX Lock Elision (간소화) */
void mutex_lock_with_elision(mutex_t *m) {
    int ret;

retry_elision:
    /* 1. 트랜잭션 시작 (XBEGIN) */
    ret = _xbegin();

    if (ret == _XBEGIN_STARTED) {
        /* 2. 트랜잭션 내에서 잠금 상태 확인 */
        if (m->lock == 0) {
            /* 잠금을 실제로 획득하지 않고 임계 구역 실행
             * 트랜잭션이 성공하면 잠금 없이 완료 */
            return;
        }
        /* 잠금이 이미 획득됨: 트랜잭션 중단 */
        _xabort(0xFF);
    }

    /* 3. 트랜잭션 실패 시 일반 futex 경로로 폴백 */
    if (--elision_retries > 0)
        goto retry_elision;

    futex_mutex_lock(m);  /* 일반 잠금 */
}
Lock Elision 현황 (2024)
항목상태비고
Intel TSX (HLE)사실상 폐기보안 취약점(TAA, MDS)으로 대부분의 CPU에서 마이크로코드로 비활성화
Intel TSX (RTM)제한적 지원일부 Xeon에서만 사용 가능, 클라이언트 CPU에서 제거
glibc Lock Elision비활성화 기본값glibc 2.35+ 에서 glibc.elision.enable=1로 명시적 활성화 필요
POWER HTM제한적 지원POWER8/9에서 지원, POWER10에서는 제거
ARM TME사양 존재아직 상용 구현 없음
TSX 보안 이슈: Intel TSX는 TAA(TSX Asynchronous Abort), MDS(Microarchitectural Data Sampling), Zombieload 등 여러 사이드 채널 공격 벡터로 활용될 수 있어, 인텔이 대부분의 CPU에서 마이크로코드 업데이트로 비활성화했습니다. 따라서 현재 Lock Elision은 실용적 가치가 크게 감소했습니다.

TSX 이후의 대안: RTM 대체 기법

TSX가 사실상 폐기된 후, 비슷한 효과를 얻기 위한 소프트웨어 기법들이 연구되고 있습니다.

Lock Elision 대안 기법 비교
기법원리적용 조건성능 효과
Adaptive spinning소유자 실행 중이면 짧은 스핀짧은 임계 구역30~60% 처리량(Throughput) 향상
Lock coarsening인접 잠금을 하나로 통합JIT 컴파일러잠금 횟수 감소
Biased locking단일 스레드 접근 시 잠금 생략단일 스레드 접근 빈번비경합 비용 제거
Flat combining하나의 결합 스레드가 연산 수행커밋 순서 불요고경합 시 수배 향상
RCU 전환읽기 경로에서 잠금 제거읽기 위주 워크로드읽기 경로 무잠금
# glibc elision 설정 확인 및 제어
$ /lib64/ld-linux-x86-64.so.2 --help 2>&1 | grep elision
# glibc.elision.enable: 0 (기본값: 비활성화)

# TSX 지원 여부 확인
$ grep -o 'rtm\|hle' /proc/cpuinfo | sort -u
# (출력 없으면 TSX 미지원 또는 비활성화)

# TSX 강제 비활성화 (보안 권장)
# /etc/default/grub: GRUB_CMDLINE_LINUX="tsx=off"

Futex 성능 특성과 벤치마킹

Fast Path vs Slow Path 지연 비교

Futex 연산별 지연 시간 (x86_64, 대략적 수치)
연산경합 없음 (Fast Path)경합 있음 (Slow Path)비고
mutex lock (uncontended)~10-25 nsN/Acmpxchg 1회
mutex unlock (no waiters)~10-15 nsN/Aatomic_exchange 1회
mutex lock (contended)N/A~1,000-5,000 nssyscall + schedule + wake
FUTEX_WAIT (immediate return)N/A~200-500 ns값 불일치로 즉시 -EAGAIN
FUTEX_WAKE (no waiters)N/A~150-400 ns해시 조회 후 빈 버킷
FUTEX_WAKE (1 waiter)N/A~800-2,000 ns해시 조회 + wake_up + context switch
FUTEX_CMP_REQUEUEN/A~500-1,500 ns두 버킷 잠금 + 리스트 이동
System V semopN/A~2,000-5,000 ns항상 syscall, 커널 구조체 접근

벤치마킹 도구와 방법

# perf bench futex — 커널 내장 futex 벤치마크
$ perf bench futex hash
# Run summary [PID 12345]: 4 threads, each operating on 1024 futexes for 10 secs.
# [thread  0] futexes: 0x55a8b0001000 ... 0x55a8b0001ffc
# Averaged 12345678 operations/sec (+- 0.12%), total secs = 10

$ perf bench futex wake
# Wokeup 1 of 1024 threads in 0.0023 ms

$ perf bench futex wake-parallel
# 4 threads waking up 1024 others

$ perf bench futex requeue
# Requeued 1023 of 1024 threads in 0.0089 ms

# 특정 프로세스의 futex syscall 빈도 측정
$ perf stat -e 'syscalls:sys_enter_futex' -p $(pidof myapp) -- sleep 10
 Performance counter stats for process id '12345':
         1,234,567      syscalls:sys_enter_futex

# strace로 futex 호출 분석
$ strace -e futex -c -p $(pidof myapp)
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- --------
 52.34    0.123456           1     98765           futex(WAIT)
 47.66    0.112345           1     89012           futex(WAKE)

성능 최적화 가이드라인

Futex 성능 최적화 체크리스트
최적화효과적용 방법
FUTEX_PRIVATE_FLAG 사용30~50% 향상프로세스 간 공유 불필요 시 기본 적용
잠금 분할 (Lock Striping)선형 확장성단일 뮤텍스 대신 배열 기반 분할
Read-Write Lock 활용읽기 위주 시 수배 향상읽기:쓰기 비율 10:1 이상일 때 효과적
캐시라인 정렬False sharing 제거futex word를 64바이트 경계에 정렬
NUMA 지역성원격 접근 감소numactl, mbind 활용
스핀 후 대기 (adaptive)짧은 경합 시 syscall 회피glibc adaptive mutex 설정
/* Adaptive mutex: 소유자가 다른 CPU에서 실행 중이면 스핀 */
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ADAPTIVE_NP);
pthread_mutex_init(&mutex, &attr);
/* glibc의 ADAPTIVE mutex는 소유자가 CPU에서 실행 중이면
 * 짧은 스핀(기본 100회)을 시도한 후 futex_wait로 전환합니다.
 * 짧은 임계 구역에서 syscall 오버헤드를 크게 줄입니다. */

벤치마크 방법론

Futex 벤치마크는 워크로드 특성에 따라 결과가 크게 달라집니다. 경합률, 임계 구역 길이, 스레드 수, NUMA 토폴로지(Topology)를 모두 고려해야 합니다.

/* 올바른 futex 벤치마크 작성법 */
#include <pthread.h>
#include <time.h>

#define ITERATIONS 10000000
#define NUM_THREADS 4
#define CRITICAL_SECTION_NS 50  /* 임계 구역 시뮬레이션 */

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
volatile long shared_counter = 0;

void *worker(void *arg) {
    struct timespec ts = {0, CRITICAL_SECTION_NS};

    for (int i = 0; i < ITERATIONS / NUM_THREADS; i++) {
        pthread_mutex_lock(&mutex);

        /* 임계 구역: 실제 작업 시뮬레이션 */
        shared_counter++;
        if (CRITICAL_SECTION_NS > 0)
            nanosleep(&ts, NULL);

        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

/* 주요 벤치마크 매개변수:
 * 1. CRITICAL_SECTION_NS: 0=uncontended 위주, 1000+=고경합
 * 2. NUM_THREADS: 1=단일 스레드, CPU수 이상=과경합
 * 3. ITERATIONS: 충분히 크게 (통계적 유의미성)
 * 4. CPU affinity: taskset으로 고정 (재현성)
 * 5. 주파수 고정: cpupower frequency-set -g performance
 */
Futex 성능 특성: 경합 수준별 지연 비교 지연 (ns) 5000 4000 3000 2000 1000 0 경합 시나리오 비경합 10ns 15ns EAGAIN 300ns 400ns 2-way 경합 1200ns 1800ns 8-way 경합 2700ns 3600ns SysV semop 5000ns 5000ns Futex (PRIVATE) Futex (SHARED) SysV (비경합도 느림)

경합 시 확장성 분석

Futex의 경합 시 확장성은 해시 테이블 크기, spinlock 보유 시간, 캐시라인 경합에 의해 결정됩니다. 스레드 수가 증가할수록 성능 저하가 발생하는 원인을 분석합니다.

스레드 수별 futex mutex 처리량 (ops/sec, x86_64 8코어)
스레드 수비경합(CS=0)짧은 CS(50ns)긴 CS(1us)병목 원인
1100M20M1M없음 (fast path only)
230M10M0.9Mcmpxchg 캐시라인 바운싱
412M5M0.8M해시 버킷 spinlock + wakeup
85M2.5M0.7Mcontext switch + 캐시 무효화(Invalidation)
162.5M1.2M0.6M스케줄러(Scheduler) 오버헤드 지배적
321.2M0.6M0.5M해시 충돌 + NUMA 원격 접근
확장성 개선 전략: 1) 잠금 분할(Lock Striping): 데이터를 파티셔닝하여 각 파티션에 별도 mutex 사용 2) Read-Copy-Update: 읽기 위주 워크로드에서 잠금 완전 제거 3) per-CPU 변수: 각 CPU가 로컬 복사본을 수정하고 나중에 합산 4) Lock-free 알고리즘: CAS 기반 자료 구조 (큐, 스택, 해시맵) 5) 캐시라인 정렬: futex word를 64바이트 경계에 정렬하여 false sharing 제거

해시 충돌의 성능 영향

# 해시 충돌 분석: bpftrace로 버킷 분포 확인
$ bpftrace -e '
kprobe:futex_hash {
    /* arg0은 futex_key의 해시값 */
    @buckets[arg0 % 4096] = count();
}
interval:s:5 {
    print(@buckets);
    clear(@buckets);
}
'

# 이상적: 균등 분포 (각 버킷 ~동일한 count)
# 문제: 특정 버킷에 집중 → spinlock 경합

# 해시 테이블 크기 확인
$ dmesg | grep futex
[    0.123456] futex hash table entries: 65536 (order: 10, 4194304 bytes, linear)

# 크기 부족 시 커널 파라미터로 조정 (6.7+)
# 또는 애플리케이션에서 futex word 주소를 캐시라인 단위로 분산

/proc 파라미터와 sysctl

Futex 자체에는 직접적인 sysctl 파라미터가 거의 없지만, 관련 커널 서브시스템의 설정이 futex 동작에 영향을 미칩니다.

Futex 관련 커널 파라미터
파라미터기본값설명
/proc/sys/kernel/sched_rt_runtime_us950000RT 스케줄러 대역폭(Bandwidth) 제한. PI futex에 영향
/proc/sys/kernel/sched_rt_period_us1000000RT 스케줄러 주기
/proc/sys/kernel/pid_max4194304최대 PID. Robust futex TID 범위에 영향
/proc/sys/vm/max_map_count65530프로세스당 최대 VMA 수. 공유 futex의 mmap에 영향

futex 관련 /proc 디버그 인터페이스

futex 자체에는 전용 /proc 파일이 없지만, 관련 정보를 여러 /proc 파일에서 수집할 수 있습니다.

futex 디버깅을 위한 /proc 파일
/proc 경로정보futex 관련 용도
/proc/PID/syscall현재 실행 중인 syscallfutex(202) 대기 확인
/proc/PID/wchan대기 중인 커널 함수futex_wait, __lll_lock_wait
/proc/PID/stack커널 스택 트레이스futex 호출 경로 확인
/proc/PID/maps메모리 매핑futex word 주소의 매핑 유형 확인
/proc/PID/status프로세스 상태스레드 수, 스케줄링 정책

futex 통계 확인

# 시스템 전체 futex syscall 횟수 (ftrace)
$ echo 1 > /sys/kernel/debug/tracing/events/syscalls/sys_enter_futex/enable
$ cat /sys/kernel/debug/tracing/trace_pipe | head -20

# perf로 futex 이벤트 집계
$ perf stat -e 'syscalls:sys_enter_futex' -a -- sleep 5

# /proc/<pid>/syscall로 현재 대기 중인 syscall 확인
$ cat /proc/12345/syscall
202 0x7f1234567890 0x80 0x2 0x0 0x0 0x0 0x7ffd12345678 0x7f1234001234

# /proc/<pid>/status에서 robust_list 확인 (gdb)
$ cat /proc/12345/status | grep -i robust
# (직접 노출되지 않지만, gdb로 확인 가능)

# /proc/<pid>/maps에서 futex word 주소의 매핑 확인
$ grep '7f1234567' /proc/12345/maps
7f1234560000-7f1234570000 rw-p 00000000 00:00 0     [stack:12346]

Futex 디버깅 (ftrace, perf, strace)

Futex 관련 문제(교착 상태, 성능 저하, 잘못된 wakeup)를 디버깅하는 실전 기법을 다룹니다.

strace를 이용한 futex 추적

# futex 시스템 콜만 추적
$ strace -e futex -f -tt -T ./my_threaded_app
[pid 12345] 14:30:05.123456 futex(0x55a8b234c040, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 2, NULL, FUTEX_BITSET_MATCH_ANY) = 0 <0.001234>
[pid 12346] 14:30:05.124690 futex(0x55a8b234c040, FUTEX_WAKE_PRIVATE, 1) = 1 <0.000012>

# -c로 통계 요약
$ strace -e futex -c -f ./my_app
% time     seconds  usecs/call     calls    errors syscall
 60.00    0.600000          60     10000           futex(WAIT)
 30.00    0.300000          30     10000           futex(WAKE)
 10.00    0.100000         100      1000      500  futex(WAIT)  EAGAIN

ftrace를 이용한 커널 내부 추적

# futex 관련 커널 함수 추적
$ echo 'futex_wait*' > /sys/kernel/debug/tracing/set_ftrace_filter
$ echo 'futex_wake*' >> /sys/kernel/debug/tracing/set_ftrace_filter
$ echo function > /sys/kernel/debug/tracing/current_tracer
$ echo 1 > /sys/kernel/debug/tracing/tracing_on

# 특정 프로세스만 추적
$ echo $PID > /sys/kernel/debug/tracing/set_ftrace_pid

# 결과 확인
$ cat /sys/kernel/debug/tracing/trace
# tracer: function
#   TASK-PID   CPU#  TIMESTAMP  FUNCTION
#   -------    ----  ---------  --------
  myapp-12345 [002] 12345.678: futex_wait_setup <-futex_wait
  myapp-12345 [002] 12345.678: get_futex_key <-futex_wait_setup
  myapp-12346 [003] 12345.679: futex_wake <-do_futex
  myapp-12346 [003] 12345.679: futex_wake_mark <-futex_wake

# futex tracepoint 사용 (더 구조화된 데이터)
$ echo 1 > /sys/kernel/debug/tracing/events/lock/futex_wait/enable
$ echo 1 > /sys/kernel/debug/tracing/events/lock/futex_wake/enable

perf를 이용한 futex 프로파일링(Profiling)

# futex contention 프로파일링
$ perf lock record -- ./my_app
$ perf lock report
# Name              acquired   contended  avg wait   total wait
# mutex@0x55a8b234  10000      500        1.2us      600.0us

# futex syscall에서 CPU 소비 시간 분석
$ perf record -g -e 'syscalls:sys_enter_futex' ./my_app
$ perf report --sort=dso,symbol
# Overhead  Shared Object  Symbol
#   35.00%  libc.so.6      __lll_lock_wait
#   25.00%  libc.so.6      pthread_cond_wait@@GLIBC_2.3.2
#   15.00%  libc.so.6      __lll_unlock_wake

# futex 해시 버킷 충돌 분석 (bpftrace)
$ bpftrace -e '
kprobe:futex_wait_setup {
    @hash_bucket[arg0 % 256] = count();
}
END { print(@hash_bucket); }
'

GDB를 이용한 Futex 교착 상태 분석

# 교착 상태 발생 시 GDB로 분석
$ gdb -p $(pidof deadlocked_app)
(gdb) info threads
  Id   Target Id         Frame
  1    Thread 0x7f123  __lll_lock_wait (futex=0x55a8b234)
  2    Thread 0x7f456  __lll_lock_wait (futex=0x55a8b238)

# 각 스레드가 어떤 뮤텍스를 대기하는지 확인
(gdb) thread 1
(gdb) bt
#0  __lll_lock_wait (futex=0x55a8b234c040, private=0)
#1  pthread_mutex_lock (mutex=0x55a8b234c040)
#2  worker_function (arg=0x0)
...

# mutex 소유자 확인 (futex word의 TID)
(gdb) x/1wx 0x55a8b234c040
0x55a8b234c040: 0x00003042     # TID=12354 (스레드 2의 TID)

(gdb) x/1wx 0x55a8b238c040
0x55a8b238c040: 0x00003041     # TID=12353 (스레드 1의 TID)

# → 스레드 1(TID=12353)이 mutex A를 대기, mutex B를 보유
# → 스레드 2(TID=12354)가 mutex B를 대기, mutex A를 보유
# → 교착 상태!
bpftrace 원라이너: futex 대기 시간(Latency) 분포를 히스토그램으로 확인하려면:
bpftrace -e 'tracepoint:syscalls:sys_enter_futex { @start[tid] = nsecs; } tracepoint:syscalls:sys_exit_futex /@start[tid]/ { @us = hist((nsecs - @start[tid]) / 1000); delete(@start[tid]); }'

ftrace로 futex 이벤트 추적

ftrace는 futex 내부의 커널 함수 호출 체인을 추적하는 가장 강력한 도구입니다. function_graph tracer를 사용하면 각 함수의 실행 시간까지 확인할 수 있습니다.

# function_graph로 futex 호출 체인과 실행 시간 추적
$ echo function_graph > /sys/kernel/debug/tracing/current_tracer
$ echo 'futex_*' > /sys/kernel/debug/tracing/set_graph_function
$ echo $PID > /sys/kernel/debug/tracing/set_ftrace_pid
$ echo 1 > /sys/kernel/debug/tracing/tracing_on

# 잠시 후 확인
$ cat /sys/kernel/debug/tracing/trace
# CPU  DURATION                  FUNCTION CALLS
#  |     |   |                     |   |   |   |
 2)               |  futex_wait() {
 2)   0.234 us    |    get_futex_key();
 2)               |    futex_q_lock() {
 2)   0.089 us    |      futex_hash();
 2)   0.456 us    |    }
 2)   0.123 us    |    get_futex_value_locked();
 2)               |    futex_wait_queue() {
 2)   0.067 us    |      plist_add();
 2) ! 1234.5 us   |      schedule();  /* ← 여기서 슬립! */
 2)   0.098 us    |    }
 2)   0.078 us    |    unqueue_me();
 2) ! 1235.8 us   |  }

# trace-cmd로 더 편리하게:
$ trace-cmd record -p function_graph -g 'futex_wait' \
    -P $PID -- sleep 5
$ trace-cmd report

perf로 futex 병목 분석

# perf lock: 뮤텍스 경합 핫스팟 분석
$ perf lock record -a -- sleep 10
$ perf lock report --sort acquired --key contended
# Name               acquired  contended  avg wait  total wait
# 0x55a8b234c040       123456       5432    2.3us     12.5ms
# → 이 주소의 mutex가 가장 심한 경합

# perf lock contention: 경합 소스 찾기
$ perf lock contention --output perf.data -- sleep 10
$ perf lock contention -i perf.data
# contended   total wait     max wait     avg wait   type      caller
#      5432       12.5ms       45us        2.3us      mutex     worker_func+0x34

# perf로 futex 호출 스택 분석
$ perf record -e 'syscalls:sys_enter_futex' -g -p $PID -- sleep 5
$ perf report --sort comm,dso,symbol --call-graph folded
# 어떤 코드 경로에서 futex syscall이 가장 많이 호출되는지 확인

# flamegraph 생성
$ perf script | stackcollapse-perf.pl | flamegraph.pl > futex_flame.svg

strace futex 분석 실전

# futex 연산 종류별 통계
$ strace -e futex -f -c -p $PID 2>&1 | head -20
# % time     seconds  usecs/call     calls    errors syscall
# ------ ----------- ----------- --------- --------- --------
#  45.12    0.451200           4    112800           futex(WAIT)
#  35.67    0.356700           3    118900           futex(WAKE)
#  10.23    0.102300           5     20460           futex(WAIT_BITSET)
#   5.34    0.053400           2     26700           futex(CMP_REQUEUE)
#   3.64    0.036400          18      2022      1011 futex(WAIT) EAGAIN

# EAGAIN이 많으면? → spurious wakeup 또는 잠금 경합이 매우 높음
# CMP_REQUEUE가 많으면? → condvar broadcast 빈번

# 특정 futex 주소의 wait/wake 패턴 분석
$ strace -e futex -f -tt -T -p $PID 2>&1 | \
    grep '0x55a8b234' | head -10
# 14:30:05.123 futex(0x55a8b234, WAIT, 2, NULL) = 0 <0.001234>
# 14:30:05.124 futex(0x55a8b234, WAKE, 1) = 1 <0.000012>
# → WAIT 시간 1.234ms: 경합 대기 시간
# → WAKE 시간 0.012ms: 깨우는 데 걸리는 시간

bpftrace 기반 futex 모니터링 도구

# 1. futex 연산별 호출 빈도 (실시간)
$ bpftrace -e '
tracepoint:syscalls:sys_enter_futex {
    $op = args->op & 0x7F;  /* PRIVATE_FLAG 마스크 */
    if ($op == 0)       { @ops["WAIT"] = count(); }
    else if ($op == 1)  { @ops["WAKE"] = count(); }
    else if ($op == 4)  { @ops["CMP_REQUEUE"] = count(); }
    else if ($op == 6)  { @ops["LOCK_PI"] = count(); }
    else if ($op == 7)  { @ops["UNLOCK_PI"] = count(); }
    else if ($op == 9)  { @ops["WAIT_BITSET"] = count(); }
    else if ($op == 10) { @ops["WAKE_BITSET"] = count(); }
    else                { @ops["OTHER"] = count(); }
}
interval:s:3 { print(@ops); clear(@ops); }
'

# 2. 프로세스별 futex 대기 시간 TOP 5
$ bpftrace -e '
tracepoint:syscalls:sys_enter_futex {
    @start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_futex /@start[tid]/ {
    @wait_us[comm, pid] = stats((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
interval:s:10 { print(@wait_us); exit(); }
'

# 3. futex 주소별 경합 핫스팟
$ bpftrace -e '
tracepoint:syscalls:sys_enter_futex /args->op == 0 || args->op == 128/ {
    @hotspot[args->uaddr] = count();
}
interval:s:5 { print(@hotspot, 10); clear(@hotspot); }
'
Futex 디버깅 워크플로: 문제 유형별 도구 선택 Futex 관련 문제 발생! 교착 상태 (Deadlock) 증상: 프로세스가 멈춤 1단계: strace -e futex -f → FUTEX_WAIT에서 모든 스레드 대기 2단계: GDB attach → 각 스레드의 mutex 소유/대기 관계 3단계: futex word 값 확인 → TID 교차 참조로 순환 확인 성능 저하 (Contention) 증상: CPU 사용률 높지만 처리량 낮음 1단계: perf stat -e syscalls:*futex → futex 호출 빈도 확인 2단계: perf lock record/report → 경합 핫스팟 주소 식별 3단계: bpftrace 대기 시간 히스토그램 → 대기 시간 분포 분석 잘못된 동작 (Bug) 증상: 데이터 손상, 잘못된 순서 1단계: valgrind --tool=helgrind → data race 탐지 2단계: TSAN (ThreadSanitizer) → 메모리 순서 위반 탐지 3단계: 커널 KCSAN/lockdep → 커널 내부 race 탐지 공통 진단 도구 체크리스트 strace -e futex -f -tt -T perf bench futex all perf lock record/report bpftrace (대기 시간, 호출 빈도) GDB: thread apply all bt GDB: x/1wx <futex_addr> (TID 확인) ftrace: function_graph + futex_* /proc/PID/syscall (현재 syscall 확인)

실전 패턴: eventfd vs futex vs io_uring

사용자 공간 동기화에는 futex 외에도 eventfd, io_uring 등 다양한 메커니즘이 있습니다. 각 메커니즘의 특성과 적합한 사용 사례를 비교합니다.

사용자 공간 동기화 메커니즘 비교
항목futexeventfdio_uringpipe
비경합 비용~10-25 ns항상 syscallSQ 링 커밋만항상 syscall
경합 비용~1-5 us~0.5-1 us~0.5-2 us~1-3 us
epoll 통합불가가능네이티브가능
다중 대기futex_waitv (5.16+)epoll네이티브epoll
프로세스 간공유 메모리 필요fd 전달fd 전달fd 전달
의미론정수 비교 + 대기카운터완료 이벤트바이트 스트림
주 사용처mutex, condvar이벤트 알림비동기 I/OIPC
커널 오버헤드최소fd 관리SQ/CQ 링버퍼(Buffer) 관리

선택 가이드

futex vs eventfd 내부 구조 비교

/* eventfd 내부 구조 */
struct eventfd_ctx {
    struct kref kref;           /* 참조 카운트 */
    wait_queue_head_t wqh;      /* 커널 대기 큐 */
    __u64 count;                /* 카운터 (항상 커널에서 관리) */
    unsigned int flags;
    int id;
};
/* → 항상 커널 객체 접근 필요 (fd 기반)
 * → 비경합 시에도 write()/read() syscall 필수
 * → epoll_wait()로 다중 이벤트 대기 가능 (장점) */

/* futex 내부 구조 */
/* → futex word는 사용자 공간 변수 (커널 객체 없음)
 * → 비경합 시 syscall 불필요 (핵심 장점)
 * → epoll 통합 불가 (FUTEX_FD 제거됨)
 * → futex_waitv()로 다중 대기 가능 (5.16+) */

/* 결론: 동기화 프리미티브(mutex/condvar)에는 futex가 최적.
 * 이벤트 기반 비동기 프로그래밍에는 eventfd가 최적.
 * 두 패턴이 모두 필요하면 eventfd + futex 조합 사용. */

io_uring의 futex 통합 (Linux 6.7+)

Linux 6.7부터 io_uring에 IORING_OP_FUTEX_WAITIORING_OP_FUTEX_WAKE 연산이 추가되었습니다. 이를 통해 io_uring의 SQ/CQ 링에서 futex 대기/깨움을 비동기적으로 처리할 수 있습니다.

/* io_uring futex 연산 (Linux 6.7+) */
#include <linux/io_uring.h>

/* FUTEX_WAIT를 io_uring SQE로 제출 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_futex_wait(sqe,
    &futex_word,      /* futex 주소 */
    expected_val,     /* 기대값 */
    FUTEX_BITSET_MATCH_ANY,
    FUTEX_32 | FUTEX_PRIVATE_FLAG,
    0);
sqe->user_data = MY_FUTEX_ID;

/* io_uring_submit() → 커널에 제출 */
io_uring_submit(&ring);

/* CQE로 결과 수신 (다른 I/O와 함께) */
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
if (cqe->user_data == MY_FUTEX_ID) {
    /* futex가 시그널됨! */
}

/* 장점:
 * - futex 대기와 I/O 대기를 단일 io_uring에서 통합
 * - epoll 없이 futex + 파일 I/O + 네트워크 I/O 동시 대기
 * - SQ polling 모드에서 syscall 오버헤드 제거 */
io_uring futex의 의의: io_uring의 futex 지원은 FUTEX_FD가 제거된 이후 처음으로 futex를 이벤트 기반 프레임워크에 통합한 것입니다. 고성능 서버에서 네트워크 I/O와 스레드 동기화를 하나의 이벤트 루프에서 처리할 수 있게 되어, 이전에 eventfd + epoll로 우회했던 패턴을 더 효율적으로 대체할 수 있습니다.

보안 고려사항 (CVE 사례)

Futex 코드는 사용자 공간의 입력을 직접 처리하는 시스템 콜이므로 보안에 매우 민감합니다. 복잡한 상태 기계와 다중 잠금으로 인해 역사적으로 여러 심각한 취약점이 발견되었습니다.

주요 Futex CVE 사례
CVE연도CVSS유형영향
CVE-2014-315320147.2Use-after-free (requeue PI)로컬 권한 상승. Towelroot Android 루팅에 악용됨
CVE-2014-020520146.9참조 카운트 오버플로futex_pi_state refcount 오버플로로 커널 크래시
CVE-2015-363620154.9Null pointer dereferenceFUTEX_REQUEUE로 커널 패닉(Kernel Panic) 유발
CVE-2018-692720184.6정수 오버플로(Integer Overflow)futex_requeue()에서 int 오버플로로 의도치 않은 동작
CVE-2021-334720217.8Use-after-free (PI state)PI futex 상태 해제 후 재사용으로 권한 상승
CVE-2023-3108420235.5Deadlock특정 futex 연산 조합으로 커널 교착 상태

CVE-2014-3153 상세 분석

이 취약점은 FUTEX_REQUEUE와 PI futex의 상호작용에서 발생했습니다. 일반 futex에서 PI futex로 requeue하면서 futex_pi_state의 소유권 관리가 잘못되어 use-after-free가 발생했습니다. 이를 통해 커널 메모리를 조작하여 로컬 권한 상승이 가능했습니다.

/* CVE-2014-3153 공격 원리 (개념적) */
/* 1. 일반 futex(uaddr)에 스레드를 대기시킴 */
futex(uaddr, FUTEX_WAIT, val, NULL, NULL, 0);

/* 2. FUTEX_CMP_REQUEUE_PI로 PI futex(uaddr2)로 requeue */
futex(uaddr, FUTEX_CMP_REQUEUE_PI, 1, INT_MAX, uaddr2, val);

/* 3. 조작된 순서로 해제/재사용하여 dangling pointer 생성 */
/* → futex_pi_state가 해제된 후에도 참조되어 use-after-free */

/* 수정: requeue 시 PI 상태 참조 카운트를 정확히 관리하고,
 *       일반 futex에서 PI futex로의 requeue 경로를 엄격히 검증 */

CVE-2014-3153 상세 분석 (Towelroot)

CVE-2014-3153은 futex 역사상 가장 유명한 취약점입니다. FUTEX_REQUEUEFUTEX_CMP_REQUEUE_PI의 상호작용에서 use-after-free가 발생하여, Android 기기의 루트 권한을 획득하는 Towelroot 익스플로잇으로 악용되었습니다.

/* CVE-2014-3153 취약점 메커니즘 (개념적 설명) */
/*
 * 문제: 일반 futex 대기자를 PI futex로 requeue할 때
 * futex_pi_state의 소유권이 올바르게 관리되지 않음
 *
 * 공격 순서:
 * 1. Thread A: FUTEX_WAIT(uaddr1, val) → 일반 futex에서 대기
 * 2. Thread B: FUTEX_CMP_REQUEUE_PI(uaddr1, 1, INT_MAX, uaddr2, val)
 *    → Thread A를 PI futex(uaddr2)로 requeue
 * 3. requeue 과정에서 futex_pi_state 생성
 * 4. Thread A: FUTEX_LOCK_PI(uaddr2) → PI state 사용
 * 5. 특정 조건에서 futex_pi_state가 해제된 후에도 참조됨
 *    → use-after-free!
 * 6. 해제된 메모리를 공격자가 제어하는 데이터로 재할당
 *    → 커널 메모리 임의 쓰기 → 권한 상승
 */

/* 수정 패치 (Thomas Gleixner, 2014):
 * - requeue 시 PI state 참조 카운트 정확히 관리
 * - 일반 futex에서 PI futex로의 requeue 경로에 추가 검증
 * - requeue_state 원자 변수로 상태 전이 추적
 * - FUTEX_WAIT_REQUEUE_PI로 대기 중인 태스크만 requeue 허용
 */

CVE-2021-3347 PI futex 취약점

CVE-2021-3347은 PI futex의 futex_pi_state 해제 후 재사용 취약점입니다. 이 취약점은 FUTEX_LOCK_PI의 에러 경로에서 PI 상태 정리가 불완전하여 발생했습니다.

주요 Futex 취약점 기술 상세
CVE취약 함수원인수정 방법
CVE-2014-3153futex_requeue()PI state refcount 누락requeue_state 추적 변수 추가
CVE-2014-0205futex_lock_pi()refcount 오버플로refcount_t 사용 (saturating)
CVE-2021-3347futex_lock_pi()에러 경로 PI state 정리 누락에러 경로에서 pi_state_put() 추가
CVE-2023-31084futex_wait_requeue_pi()잠금 순서 위반으로 교착잠금 순서 재설계

취약점 대응 패치와 방어 강화 히스토리

# syzkaller로 futex 퍼징 시 발견된 버그 예시
# syzkaller는 다양한 futex 연산을 무작위 조합하여 커널 크래시를 탐지
# 대표적 패턴:
# - FUTEX_WAIT + FUTEX_CMP_REQUEUE_PI 동시 호출
# - 잘못된 정렬 주소에 futex 연산
# - PI futex의 TID 조작
# - robust list의 포인터 조작
# - futex_waitv에 128개 futex + 타임아웃 동시 사용

# Docker 컨테이너에서 futex 보안 강화
$ docker run --security-opt seccomp=custom-profile.json ...
# custom-profile.json에서 FUTEX_REQUEUE(op=3) 차단 권장
# (안전한 FUTEX_CMP_REQUEUE만 허용)
# seccomp으로 futex 연산 제한 (컨테이너 프로필)
# Docker 기본 seccomp 프로필은 futex를 허용합니다.
# 커스텀 프로필에서 특정 futex_op만 허용:
{
    "names": ["futex", "futex_waitv"],
    "action": "SCMP_ACT_ALLOW"
}

# KASAN 활성화 빌드로 futex 버그 탐지
$ make defconfig
$ scripts/config -e KASAN
$ scripts/config -e KASAN_INLINE
$ make -j$(nproc)

아키텍처별 구현 차이 (x86, ARM64, RISC-V)

Futex의 커널 코드는 대부분 아키텍처 독립적이지만, 원자적 연산, 메모리 배리어, 사용자 공간 접근 방식에서 아키텍처별 차이가 있습니다.

아키텍처별 Futex 원자적 연산 비교 x86_64 cmpxchg (사용자): LOCK CMPXCHG [addr], new 단일 명령, LOCK 접두사 cmpxchg (커널): futex_atomic_cmpxchg_inatomic() → LOCK CMPXCHG + 예외 핸들링 메모리 모델: TSO (Total Store Ordering) 추가 배리어 대부분 불필요 성능 특성: LOCK CMPXCHG: ~10-20 cycles 캐시라인 경합 시: ~100+ cycles ARM64 (AArch64) cmpxchg (사용자): LDXR / STXR 루프 또는 CAS (ARMv8.1-A LSE) cmpxchg (커널): __futex_atomic_op() (asm) → LDXR/STXR + UAO/PAN 처리 메모리 모델: 약한 순서 (Weak Ordering) DMB/DSB 배리어 필수 성능 특성: LSE CAS: ~10-15 cycles LL/SC: ~15-30 cycles RISC-V cmpxchg (사용자): LR.W / SC.W 루프 (Zacas 확장: AMOCAS.W) cmpxchg (커널): futex_atomic_cmpxchg_inatomic() → LR.W/SC.W + 예외 처리 메모리 모델: RVWMO (약한 순서) FENCE 명령 필수 성능 특성: LR/SC: 구현 의존 AMOCAS: 아직 초기 단계 모든 아키텍처에서 32비트 자연 정렬 futex word 필수 (alignment fault 방지)
아키텍처별 futex 커널 함수
함수역할아키텍처 의존 구현
futex_atomic_cmpxchg_inatomic()사용자 공간 futex word에 원자적 cmpxchgarch/*/include/asm/futex.h
futex_atomic_op_inuser()FUTEX_WAKE_OP의 원자적 연산arch/*/include/asm/futex.h
get_user() / put_user()사용자 공간 메모리 접근아키텍처별 예외 처리 테이블
/* arch/arm64/include/asm/futex.h — ARM64 원자적 연산 */
static inline int
arch_futex_atomic_op_inuser(int op, int oparg, int *oval,
                            u32 __user *uaddr)
{
    int oldval = 0, ret, tmp;

    uaccess_enable_privileged();

    switch (op) {
    case FUTEX_OP_SET:
        asm volatile(
        "   prfm    pstl1strm, %2\n"
        "1: ldxr    %w1, %2\n"
        "   stlxr   %w0, %w3, %2\n"
        "   cbnz    %w0, 1b\n"
        "   dmb     ish\n"
        : "=&r" (ret), "=&r" (oldval), "+Q" (*uaddr)
        : "r" (oparg)
        : "memory");
        break;
    /* ... FUTEX_OP_ADD, OR, ANDN, XOR ... */
    }

    uaccess_disable_privileged();
    *oval = oldval;
    return ret;
}

x86 구현 상세: LOCK CMPXCHG와 예외 처리

x86에서 futex의 사용자 공간 cmpxchg는 LOCK CMPXCHG 명령어를 사용합니다. 이 명령어는 TSO(Total Store Ordering) 메모리 모델과 결합하여 추가 메모리 배리어 없이도 올바른 동기화를 보장합니다.

/* arch/x86/include/asm/futex.h — x86 futex 원자적 연산 */
static inline int
futex_atomic_cmpxchg_inatomic(u32 *uval, u32 __user *uaddr,
                               u32 oldval, u32 newval)
{
    int ret = 0;

    asm volatile(
        "1:\t" LOCK_PREFIX "cmpxchgl %4, %2\n"
        "2:\n"
        "\t.section .fixup, \"ax\"\n"
        "3:\tmov     %3, %0\n"    /* -EFAULT */
        "\tjmp     2b\n"
        "\t.previous\n"
        _ASM_EXTABLE_UA(1b, 3b)
        : "+r" (ret), "=a" (oldval), "+m" (*uaddr)
        : "i" (-EFAULT), "r" (newval), "1" (oldval)
        : "memory");

    *uval = oldval;
    return ret;
}

/* 핵심 포인트:
 * 1. LOCK_PREFIX: 캐시라인 잠금으로 원자성 보장
 * 2. LOCK CMPXCHG: compare-and-swap (EAX vs *uaddr)
 * 3. .fixup 섹션: 페이지 폴트 시 -EFAULT 반환
 * 4. _ASM_EXTABLE_UA: 사용자 주소 접근 예외 테이블 등록
 *
 * x86 TSO 모델에서:
 * - 모든 store는 프로그램 순서대로 관측됨
 * - LOCK 접두사는 전체 메모리 배리어 역할
 * - 따라서 추가 mfence/lfence 불필요 */

ARM64 구현: UAO, PAN과 futex의 관계

ARM64에서는 사용자 공간 메모리에 커널이 접근할 때 UAO(User Access Override)와 PAN(Privileged Access Never) 기능이 보안 경계를 강화합니다. futex 코드에서 사용자 메모리 접근 시 이러한 보호 기능을 올바르게 관리해야 합니다.

/* ARM64 futex에서의 사용자 공간 접근 제어 */
/* PAN (Privileged Access Never):
 * - 커널이 사용자 공간 주소에 직접 접근하면 폴트 발생
 * - 보안: 커널이 실수로 사용자 데이터를 읽는 것 방지
 *
 * futex 코드에서:
 * uaccess_enable_privileged()  → PAN 비활성화 (접근 허용)
 * ... LDXR/STXR 사용자 메모리 연산 ...
 * uaccess_disable_privileged() → PAN 재활성화
 *
 * UAO (User Access Override, ARMv8.2):
 * - LDTR/STTR 대신 일반 LDR/STR로 사용자 접근 가능
 * - unprivileged load/store가 자동으로 적용됨
 */

/* ARM64 LSE (Large System Extensions) 사용 여부:
 * alternatives 메커니즘으로 런타임에 결정 */
asm volatile(
ALTERNATIVE(
    /* LL/SC fallback (모든 ARM64) */
    "   prfm    pstl1strm, %2\n"
    "1: ldxr    %w1, %2\n"
    "   sub     %w0, %w1, %w3\n"
    "   cbnz    %w0, 2f\n"
    "   stlxr   %w0, %w4, %2\n"
    "   cbnz    %w0, 1b\n"
    "   dmb     ish\n"
    "2:",
    /* LSE CAS (ARMv8.1+ 지원 시) */
    "   mov     %w0, %w3\n"
    "   casal   %w0, %w4, %2\n"
    "   sub     %w0, %w0, %w3\n",
    ARM64_HAS_LSE_ATOMICS)
);

RISC-V 구현: LR/SC와 Zacas 확장

RISC-V의 futex 구현은 현재 A(Atomic) 확장의 LR/SC(Load-Reserved/Store-Conditional)를 주로 사용합니다. 향후 Zacas 확장이 보급되면 CAS 명령어로 대체될 예정입니다.

RISC-V 원자적 확장과 futex
확장명령어상태futex 관련성
A (Atomics)LR.W, SC.W, AMO*필수 확장현재 futex cmpxchg 구현
ZacasAMOCAS.W/D/Q비준됨(ratified)CAS 명령으로 LL/SC 루프 대체
Zabha바이트/하프워드 AMO비준됨8/16비트 futex 지원 가능
ZtsoTSO 메모리 모델비준됨fence 명령 감소, x86 호환
/* RISC-V 메모리 모델과 futex:
 *
 * RVWMO (RISC-V Weak Memory Ordering):
 * - 기본 약한 순서 모델
 * - 원자적 연산에 .aq (acquire), .rl (release) 수식어 필요
 *
 * futex cmpxchg에서:
 * LR.W       → load-reserved (다른 코어의 수정 감지 가능)
 * SC.W.RL    → store-conditional + release 순서
 * FENCE RW,RW → 전체 배리어 (acquire 보장)
 *
 * Ztso (Total Store Ordering) 확장이 있으면:
 * - fence 대부분 제거 가능
 * - x86과 동일한 메모리 모델
 * - futex 성능 약간 향상 (fence 오버헤드 감소)
 */
아키텍처별 Futex cmpxchg 비용 (cycles) cycles 500 300 150 50 0 비경합 2-way 경합 8-way 경합 15 15 20 12 80 80 60 40 500+ 300 250 150 x86 LOCK CMPXCHG ARM64 LL/SC RISC-V LR/SC ARM64 LSE CAS
아키텍처 선택의 영향: ARM64 LSE CAS가 고경합 시 가장 좋은 성능을 보이는 이유는 LL/SC 루프의 재시도 오버헤드가 없기 때문입니다. x86의 LOCK CMPXCHG는 고경합 시 캐시라인 잠금 경합이 심화되어 cycles가 급격히 증가합니다. RISC-V는 구현체에 따라 차이가 크므로 실제 하드웨어에서의 측정이 중요합니다.

Futex와 PREEMPT_RT 상호작용

PREEMPT_RT 패치(Linux 6.12에서 메인라인 통합)는 커널 내부의 거의 모든 spinlock을 rt_mutex 기반의 sleeping lock으로 변환합니다. 이는 futex의 내부 구현에도 영향을 미칩니다.

PREEMPT_RT에서의 futex 변경점

일반 커널 vs PREEMPT_RT 커널의 futex 차이
항목일반 커널PREEMPT_RT 커널
해시 버킷 lockspinlock_t (실제 스핀)spinlock_trt_mutex 기반 sleeping lock
선점 불가 구간spinlock 보유 중 선점 불가sleeping lock이므로 선점 가능
PI futexrt_mutex 사용 (동일)동일 (PREEMPT_RT의 핵심 인프라)
인터럽트(Interrupt) 컨텍스트하드 IRQ에서 futex_wake 가능IRQ 스레드화, sleeping 가능
최악 지연수백 us (spinlock 구간)수십 us (모든 잠금이 선점 가능)
PI 체인 복잡도사용자 공간 PI만커널 잠금까지 PI 체인 확장 가능

PREEMPT_RT에서 PI Futex의 중요성

PREEMPT_RT 환경에서는 거의 모든 커널 잠금이 PI를 지원하므로, 사용자 공간의 PI futex가 커널 내부 잠금까지 연쇄적으로 우선순위 상속을 전파할 수 있습니다. 이는 실시간 애플리케이션에서 결정론적 지연 시간을 보장하는 데 필수적입니다.

/* PREEMPT_RT에서의 futex 해시 버킷 잠금 */
/* 일반 커널에서: */
spin_lock(&hb->lock);  /* 선점 불가, 실제 스핀 */
/* ... 해시 버킷 작업 ... */
spin_unlock(&hb->lock);

/* PREEMPT_RT 커널에서: */
spin_lock(&hb->lock);  /* rt_mutex 기반: 선점 가능, 슬립 가능 */
/* ... 해시 버킷 작업 ... */
spin_unlock(&hb->lock);
/* 문법은 동일하지만 내부 동작이 완전히 다름!
 * - 다른 RT 태스크가 선점할 수 있음
 * - PI가 적용되어 잠금 보유자의 우선순위가 부스트됨 */
# PREEMPT_RT 커널에서 futex 지연 측정
$ cyclictest -m -S -p 98 -i 1000 -l 100000
# T: 0 ( 1234) P:98 I:1000 C: 100000 Min:   1 Act:   3 Avg:   4 Max:  15
cyclictest: cyclictest 상세 옵션, 히스토그램 분석, osnoise/hwlat_detector 등 RT 지연 측정 도구는 PREEMPT_RT: Latency 측정 도구를 참고하세요.
실시간 애플리케이션 권장: PREEMPT_RT 환경에서 실시간 스레드가 뮤텍스를 사용할 때는 반드시 PTHREAD_PRIO_INHERIT 속성으로 PI mutex를 사용하세요. 일반 mutex는 우선순위 역전을 방지하지 못합니다.

pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);

PREEMPT_RT에서의 futex 지연 시간 분석

PREEMPT_RT 커널에서 futex의 최악 지연 시간(worst-case latency)을 측정하고 일반 커널과 비교합니다.

일반 커널 vs PREEMPT_RT 커널: futex 지연 시간
측정 항목일반 커널PREEMPT_RT비고
비경합 잠금 (avg)~10 ns~10 ns동일 (사용자 공간 cmpxchg)
비경합 잠금 (worst)~50 ns~15 nsRT에서 선점 불가 구간 감소
경합 잠금 (avg)~1.5 us~2.0 usRT에서 sleeping lock 오버헤드
경합 잠금 (worst)~100 us~15 usRT에서 최악 지연 대폭 개선!
FUTEX_WAKE (avg)~0.8 us~1.0 us약간 느림 (sleeping lock)
FUTEX_WAKE (worst)~50 us~10 us결정론적 지연
PI futex 부스트~1.0 us~1.0 us동일 (rt_mutex 공유)

PREEMPT_RT 환경 futex 모범 사례

PREEMPT_RT 환경 체크리스트:
  • 모든 공유 mutex에 PTHREAD_PRIO_INHERIT 설정 (PI 필수)
  • Robust 속성 활성화: 프로세스 사망 시 교착 방지
  • RT 스레드의 메모리를 mlockall(MCL_CURRENT | MCL_FUTURE)로 잠금
  • 스택 크기를 충분히 설정 (페이지 폴트 최소화)
  • futex word를 가능한 한 PRIVATE으로 사용 (해시 비용 감소)
  • 긴 임계 구역에서는 sched_yield() 대신 pthread_mutex_unlock() 사용
  • cyclictest로 정기적으로 최악 지연 시간 검증

FUTEX_WAKE_OP 상세 인코딩

FUTEX_WAKE_OP는 glibc의 pthread_cond_signal() 구현에서 조건 변수의 시퀀스 카운터를 원자적으로 업데이트하면서 동시에 대기자를 깨우는 데 사용됩니다. 하나의 시스템 콜로 두 개의 futex에 대한 연산을 수행하여 syscall 오버헤드를 절반으로 줄입니다.

/* kernel/futex/waitwake.c — futex_wake_op() 핵심 로직 */
static int futex_wake_op(u32 __user *uaddr1, unsigned int flags,
    u32 __user *uaddr2, int nr_wake, int nr_wake2, int op)
{
    struct futex_hash_bucket *hb1, *hb2;
    union futex_key key1, key2;
    int ret, oldval;

    /* 1. 두 futex의 키 생성 */
    ret = get_futex_key(uaddr1, flags, &key1, FUTEX_READ);
    ret = get_futex_key(uaddr2, flags, &key2, FUTEX_WRITE);

    /* 2. 두 해시 버킷 모두 잠금 (deadlock 방지를 위해 주소 순) */
    double_lock_hb(hb1, hb2);

    /* 3. uaddr2에 원자적 연산 수행 */
    ret = futex_atomic_op_inuser(op, uaddr2);
    /* op 인코딩:
     *   (op_type << 28) | (cmp_type << 24) | (oparg << 12) | cmparg
     * op_type: SET, ADD, OR, ANDN, XOR
     * cmp_type: EQ, NE, LT, LE, GT, GE */

    /* 4. uaddr1에서 nr_wake개 깨움 */
    ret = futex_wake_mark_all(hb1, &key1, nr_wake);

    /* 5. 연산 결과가 비교 조건을 만족하면 uaddr2에서도 깨움 */
    if (futex_op_cmp(oldval, op, cmparg))
        ret += futex_wake_mark_all(hb2, &key2, nr_wake2);

    double_unlock_hb(hb1, hb2);
    return ret;
}

해시 버킷 이중 잠금과 교착 방지

FUTEX_WAKE_OPFUTEX_CMP_REQUEUE는 두 개의 해시 버킷을 동시에 잠가야 합니다. 교착 상태를 방지하기 위해 항상 주소가 작은 버킷을 먼저 잠급니다.

/* kernel/futex/core.c — double_lock_hb() */
static inline void
double_lock_hb(struct futex_hash_bucket *hb1,
               struct futex_hash_bucket *hb2)
{
    if (hb1 <= hb2) {
        spin_lock(&hb1->lock);
        if (hb1 < hb2)
            spin_lock_nested(&hb2->lock, SINGLE_DEPTH_NESTING);
    } else {
        spin_lock(&hb2->lock);
        spin_lock_nested(&hb1->lock, SINGLE_DEPTH_NESTING);
    }
    /* lockdep: SINGLE_DEPTH_NESTING으로 중첩 잠금 허용 표시 */
}

사용자 공간 주소 유효성 검증

커널은 사용자 공간의 futex word에 접근할 때 여러 단계의 유효성 검증을 수행합니다. 잘못된 주소, 정렬되지 않은 주소, 매핑되지 않은 페이지 등을 처리해야 합니다.

/* kernel/futex/core.c — 주소 검증 */
static int futex_check_address(u32 __user *uaddr)
{
    /* 1. 자연 정렬 확인 (4바이트 경계) */
    if ((unsigned long)uaddr % sizeof(u32))
        return -EINVAL;

    /* 2. 사용자 공간 주소 범위 확인 */
    if (!access_ok(uaddr, sizeof(u32)))
        return -EFAULT;

    return 0;
}

/* get_futex_value_locked — 해시 버킷 잠금 하에서 값 읽기 */
static int get_futex_value_locked(u32 *dest, u32 __user *from)
{
    int ret;

    /* pagefault_disable: 잠금 보유 중 페이지 폴트 금지 */
    pagefault_disable();
    ret = __get_user(*dest, from);
    pagefault_enable();

    /* 페이지 폴트 시 잠금 해제 후 재시도 */
    return ret ? -EFAULT : 0;
}
pagefault_disable 주의: 해시 버킷의 spinlock을 보유한 상태에서 사용자 공간 메모리에 접근할 때 pagefault_disable()을 호출합니다. 만약 해당 페이지가 스왑 아웃되었다면 페이지 폴트를 처리할 수 없으므로 -EFAULT를 반환합니다. 이 경우 커널은 spinlock을 해제하고, 페이지를 fault-in한 후 전체 연산을 재시도합니다. 이 “fault, retry” 패턴은 futex 코드 전체에서 반복됩니다.

PI Futex: Futex Word 인코딩 상세

PI futex에서 futex word는 단순한 잠금 상태가 아니라 소유자의 TID와 상태 비트를 인코딩합니다.

/* PI Futex word 비트 레이아웃 (32비트) */
/*
 * Bits 0-29:  TID (Thread ID) of the lock owner
 *             0이면 잠금 해제 상태
 *
 * Bit 30:     FUTEX_OWNER_DIED
 *             소유자가 robust futex를 보유한 채 사망
 *             다음 소유자는 EOWNERDEAD를 받음
 *
 * Bit 31:     FUTEX_WAITERS
 *             커널에 대기자가 있음을 표시
 *             이 비트가 설정되면 unlock 시 반드시 커널 호출 필요
 */

#define FUTEX_TID_MASK    0x3FFFFFFFU  /* bits 0-29 */
#define FUTEX_OWNER_DIED  0x40000000U  /* bit 30 */
#define FUTEX_WAITERS     0x80000000U  /* bit 31 */

/* 예시:
 * futex_word = 0x00001234  → TID 0x1234가 소유, 대기자 없음
 * futex_word = 0x80001234  → TID 0x1234가 소유, 대기자 있음
 * futex_word = 0x40001234  → TID 0x1234가 소유했으나 사망
 * futex_word = 0xC0001234  → 사망 + 대기자 있음
 * futex_word = 0x00000000  → 잠금 해제 상태
 */

Robust Futex: glibc의 등록 흐름

/* glibc nptl/nptl_setxid.c — robust list 등록 (간소화) */
/* 각 스레드 생성 시 자동으로 호출됨 */
void __nptl_set_robust_list_internal(void)
{
    struct robust_list_head *head;

    head = &THREAD_SELF->robust_head;

    /* 커널에 robust list 위치 등록 */
    INTERNAL_SYSCALL(set_robust_list, 2, head, sizeof(*head));
}

/* 스레드 종료 시 커널이 자동으로 exit_robust_list() 호출
 * → 보유 중인 모든 robust mutex 정리 */

pthread_cond_broadcast와 CMP_REQUEUE의 조합

glibc의 pthread_cond_broadcast()FUTEX_CMP_REQUEUE를 사용하여 thundering herd를 방지합니다. 구체적인 동작은 다음과 같습니다:

/* glibc nptl/pthread_cond_broadcast.c — 간소화 */
int __pthread_cond_broadcast(pthread_cond_t *cond)
{
    /* 1. 시퀀스 번호 증가 (다음 세대로 전환) */
    unsigned int g = cond->__data.__wseq & 1;
    cond->__data.__wseq += 1;  /* 세대 전환 */

    /* 2. 대기자에게 시그널 */
    atomic_store_release(&cond->__data.__g_signals[g],
                         cond->__data.__g_size[g] << 1);

    /* 3. FUTEX_CMP_REQUEUE: 1개 깨우고 나머지는 mutex 큐로 이동 */
    futex_cmp_requeue(&cond->__data.__g_signals[g],
                      1,  /* nr_wake: 1개만 깨움 */
                      INT_MAX,  /* nr_requeue: 나머지 전부 이동 */
                      &mutex->__data.__lock,
                      signals_val);  /* 비교 값 */

    return 0;
}

futex_waitv 커널 구현 상세

/* kernel/futex/syscalls.c — sys_futex_waitv() */
SYSCALL_DEFINE5(futex_waitv,
    struct futex_waitv __user *, waiters,
    unsigned int, nr_futexes,
    unsigned int, flags,
    struct __kernel_timespec __user *, timeout,
    clockid_t, clockid)
{
    struct futex_vector *futexv;
    struct hrtimer_sleeper to;
    int ret;

    /* 유효성 검사 */
    if (nr_futexes < 2 || nr_futexes > FUTEX_WAITV_MAX) /* 128 */
        return -EINVAL;

    /* futex_vector 배열 할당 (스택 또는 kmalloc) */
    futexv = kcalloc(nr_futexes, sizeof(*futexv), GFP_KERNEL);

    /* 사용자 공간에서 대기 벡터 복사 */
    if (copy_from_user(futexv, waiters,
                       nr_futexes * sizeof(*waiters)))
        goto err;

    /* 각 futex에 대해 키 생성 및 대기 큐 삽입 */
    for (i = 0; i < nr_futexes; i++) {
        ret = get_futex_key(futexv[i].uaddr, futexv[i].flags,
                           &futexv[i].q.key, FUTEX_READ);

        /* 값 비교: 이미 변경되었으면 즉시 반환 */
        if (*futexv[i].uaddr != futexv[i].val) {
            ret = i;  /* 변경된 futex의 인덱스 */
            goto unqueue_all;
        }

        futex_q_lock_and_enqueue(&futexv[i].q);
    }

    /* 모든 futex가 아직 유효하면 슬립 */
    set_current_state(TASK_INTERRUPTIBLE);
    if (timeout)
        hrtimer_sleeper_start_expires(&to, HRTIMER_MODE_ABS);

    /* schedule: 어느 하나라도 WAKE되면 깨어남 */
    freezable_schedule();

    /* 깨어남: 어떤 futex가 시그널되었는지 확인 */
    for (i = 0; i < nr_futexes; i++) {
        if (futexv[i].q.woken) {
            ret = i;
            break;
        }
    }

unqueue_all:
    /* 나머지 모든 futex_q를 해시 버킷에서 제거 */
    for (j = 0; j < nr_futexes; j++)
        futex_unqueue(&futexv[j].q);

    kfree(futexv);
    return ret;  /* 깨어난 futex의 인덱스 */
}

NUMA-aware 잠금 설계 패턴

대규모 NUMA 시스템에서 최적의 futex 성능을 위한 설계 패턴을 소개합니다.

/* NUMA-aware 잠금 분할 예시 */
#include <numa.h>

#define MAX_NUMA_NODES 8
#define CACHE_LINE_SIZE 64

/* 각 NUMA 노드별 독립적인 잠금 */
struct numa_lock_set {
    struct {
        pthread_mutex_t lock;
        char padding[CACHE_LINE_SIZE - sizeof(pthread_mutex_t)];
    } __attribute__((aligned(CACHE_LINE_SIZE))) per_node[MAX_NUMA_NODES];
};

/* 현재 NUMA 노드에 해당하는 잠금 사용 */
void numa_aware_lock(struct numa_lock_set *ls) {
    int node = numa_node_of_cpu(sched_getcpu());
    pthread_mutex_lock(&ls->per_node[node].lock);
}

void numa_aware_unlock(struct numa_lock_set *ls) {
    int node = numa_node_of_cpu(sched_getcpu());
    pthread_mutex_unlock(&ls->per_node[node].lock);
}

/* 이 패턴은 노드 간 캐시라인 바운싱을 제거하여
 * 경합 시 성능을 수 배 개선합니다.
 * 단, 모든 노드의 잠금을 동시에 획득해야 하는
 * 전역 연산이 있다면 추가 조정이 필요합니다. */

Futex 경합 프로파일링 실전 예제

# 1. perf bench futex 전체 실행
$ perf bench futex all

# hash: Operations/second for 4 threads hashing on 1024 futexes:
#   15234567 ops/sec

# wake: Wokeup 1 of 1024 threads in 0.0021 ms
# wake-parallel: 4 threads waking 1024 in 0.0045 ms
# requeue: Requeued 1023 of 1024 in 0.0078 ms
# lock-pi: 4 threads locking on PI futex for 10 secs: 567890 ops/sec

# 2. bpftrace로 futex 대기 시간 히스토그램
$ bpftrace -e '
tracepoint:syscalls:sys_enter_futex /args->op == 0 || args->op == 128/ {
    @start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_futex /@start[tid]/ {
    @wait_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
interval:s:10 { exit(); }
' 2>/dev/null

# 3. Lock contention 핫스팟 찾기
$ perf lock record -a -- sleep 10
$ perf lock report --sort acquired --key contended
#              acquired   contended  avg wait   total wait
# 0x55a8...       98765       5432    2.3us     12.5ms
# 0x55a8...       87654       1234    1.1us      1.4ms

# 4. futex 시스템 콜 빈도 by PID
$ bpftrace -e '
tracepoint:syscalls:sys_enter_futex {
    @count[comm, pid] = count();
}
interval:s:5 { print(@count); clear(@count); }
'

교착 상태 자동 탐지 스크립트

#!/bin/bash
# futex_deadlock_detector.sh — 교착 상태 의심 프로세스 분석

PID=${1:?Usage: $0 <pid>}

echo "=== Futex Deadlock Analysis for PID $PID ==="

# 1. 모든 스레드의 현재 syscall 확인
echo -e "\n--- Threads in futex wait ---"
for tid in /proc/$PID/task/*/; do
    tid_num=$(basename $tid)
    syscall=$(cat $tid/syscall 2>/dev/null | awk '{print $1}')
    if [ "$syscall" = "202" ] || [ "$syscall" = "98" ]; then
        futex_addr=$(cat $tid/syscall 2>/dev/null | awk '{print $2}')
        echo "  TID $tid_num: FUTEX_WAIT on $futex_addr"
    fi
done

# 2. 스택 트레이스 수집
echo -e "\n--- Thread stack traces ---"
for tid in /proc/$PID/task/*/; do
    tid_num=$(basename $tid)
    if grep -q 'futex_wait\|__lll_lock_wait' $tid/wchan 2>/dev/null; then
        echo "  TID $tid_num ($(cat $tid/wchan)):"
        cat $tid/stack 2>/dev/null | head -10 | sed 's/^/    /'
    fi
done

# 3. Mutex 소유자 추적 (gdb 필요)
echo -e "\n--- Use GDB for mutex owner analysis: ---"
echo "  gdb -batch -ex 'thread apply all bt' -p $PID"

Futex 관련 보안 강화 설정

# 1. seccomp BPF로 futex 연산 제한
# 위험한 FUTEX_REQUEUE (op=3) 차단, 안전한 CMP_REQUEUE (op=4)만 허용
# (libseccomp 사용)

# 2. KASAN으로 futex use-after-free 탐지 (커널 빌드 옵션)
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y  # 또는 CONFIG_KASAN_SW_TAGS (ARM64)
CONFIG_KASAN_INLINE=y

# 3. lockdep으로 PI 체인 교착 탐지
CONFIG_PROVE_LOCKING=y
CONFIG_DEBUG_LOCK_ALLOC=y
CONFIG_LOCKDEP=y

# 4. futex 관련 커널 디버그 옵션
CONFIG_DEBUG_PI_LIST=y
CONFIG_DEBUG_RT_MUTEXES=y

# 5. syzkaller 퍼저로 futex 엣지 케이스 테스트
$ cat syzkaller_futex.cfg
{
    "enable_syscalls": [
        "futex", "futex_waitv",
        "set_robust_list", "get_robust_list"
    ]
}

PREEMPT_RT 환경에서의 실시간 뮤텍스 설정

/* 실시간 애플리케이션을 위한 PI mutex 설정 */
#include <pthread.h>
#include <sched.h>

int setup_rt_mutex(pthread_mutex_t *mutex, int ceiling_prio)
{
    pthread_mutexattr_t attr;
    int ret;

    ret = pthread_mutexattr_init(&attr);

    /* PI (우선순위 상속) 활성화 — PREEMPT_RT에서 필수 */
    ret = pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);

    /* Robust 활성화 — 비정상 종료 대응 */
    ret = pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST);

    /* 에러 체킹 타입 — 디버깅 시 유용 */
    ret = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);

    ret = pthread_mutex_init(mutex, &attr);
    pthread_mutexattr_destroy(&attr);

    return ret;
}

/* RT 스레드 생성 */
void create_rt_thread(pthread_t *thread, void *(*func)(void *),
                       int priority)
{
    pthread_attr_t attr;
    struct sched_param param;

    pthread_attr_init(&attr);
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
    param.sched_priority = priority;
    pthread_attr_setschedparam(&attr, &param);

    pthread_create(thread, &attr, func, NULL);
    pthread_attr_destroy(&attr);
}

/* EOWNERDEAD 처리 예시 */
int safe_mutex_lock(pthread_mutex_t *mutex)
{
    int ret = pthread_mutex_lock(mutex);

    if (ret == EOWNERDEAD) {
        /* 이전 소유자가 비정상 종료함 */
        /* 공유 자원 일관성 복구 수행 */
        recover_shared_state();

        /* mutex를 일관된 상태로 표시 */
        pthread_mutex_consistent(mutex);
        return 0;
    }

    return ret;
}

/* 복구 불가능한 경우 */
int unrecoverable_mutex_lock(pthread_mutex_t *mutex)
{
    int ret = pthread_mutex_lock(mutex);

    if (ret == EOWNERDEAD) {
        /* 복구 불가능: mutex를 ENOTRECOVERABLE 상태로 전환 */
        /* 이후 모든 lock 시도는 ENOTRECOVERABLE 반환 */
        pthread_mutex_unlock(mutex);  /* → ENOTRECOVERABLE 전파 */
        return -1;
    }

    if (ret == ENOTRECOVERABLE) {
        /* mutex가 영구적으로 손상됨 — 재초기화 필요 */
        return -1;
    }

    return ret;
}

ARM64 LSE 원자적 연산과 Futex 성능

ARMv8.1-A의 LSE(Large System Extensions)는 LL/SC(Load-Linked/Store-Conditional) 루프를 단일 원자적 명령어로 대체합니다. 이는 futex의 cmpxchg 성능에 직접적인 영향을 미칩니다.

ARM64 LL/SC vs LSE 성능 비교 (futex cmpxchg)
명령 모드비경합 (cycles)2-way 경합 (cycles)8-way 경합 (cycles)
LL/SC (LDXR/STXR)~15~80~500+
LSE (CAS)~12~40~150
LSE (CASAL, acquire+release)~15~45~160

LSE 명령어는 특히 높은 경합 상황에서 LL/SC 대비 3~4배 빠릅니다. 이는 LL/SC 루프가 경합 시 재시도를 반복하는 반면, LSE CAS는 하드웨어 수준에서 원자성을 보장하기 때문입니다.

/* arch/arm64/include/asm/atomic_lse.h — LSE CAS 사용 */
static inline u32 __lse_cmpxchg_case_32(volatile void *ptr,
                                         u32 old, u32 new)
{
    u32 tmp;
    asm volatile(
        "   cas     %w[old], %w[new], %[v]\n"
        : [v] "+Q" (*(u32 *)ptr), [old] "+r" (old)
        : [new] "r" (new)
        : "memory");
    return old;
}

/* vs LL/SC 방식 */
static inline u32 __ll_sc_cmpxchg_case_32(volatile void *ptr,
                                            u32 old, u32 new)
{
    u32 tmp, oldval;
    asm volatile(
        "1: ldxr    %w[oldval], %[v]\n"
        "   eor     %w[tmp], %w[oldval], %w[old]\n"
        "   cbnz    %w[tmp], 2f\n"
        "   stxr    %w[tmp], %w[new], %[v]\n"
        "   cbnz    %w[tmp], 1b\n"  /* 실패 시 재시도 루프 */
        "2:"
        : [v] "+Q" (*(u32 *)ptr), [oldval] "=&r" (oldval),
          [tmp] "=&r" (tmp)
        : [old] "r" (old), [new] "r" (new)
        : "memory");
    return oldval;
}

RISC-V에서의 Futex 고려사항

RISC-V의 원자적 확장(A 확장)은 LR/SC(Load-Reserved/Store-Conditional)와 AMO(Atomic Memory Operations)를 제공합니다. 현재 대부분의 RISC-V futex 구현은 LR/SC를 사용하며, Zacas 확장의 AMOCAS는 아직 초기 단계입니다.

/* arch/riscv/include/asm/futex.h — RISC-V futex 원자적 연산 */
static inline int
futex_atomic_cmpxchg_inatomic(u32 *uval, u32 __user *uaddr,
                               u32 oldval, u32 newval)
{
    int ret = 0;
    u32 val;

    __enable_user_access();

    asm volatile(
        "1: lr.w    %[val], %[addr]\n"
        "   bne     %[val], %[old], 2f\n"
        "   sc.w.rl %[ret], %[new], %[addr]\n"
        "   bnez    %[ret], 1b\n"
        "   fence   rw, rw\n"
        "2:\n"
        _ASM_EXTABLE_UACCESS_ERR(1b, 2b, %[ret])
        : [val] "=&r" (val), [ret] "+r" (ret),
          [addr] "+A" (*uaddr)
        : [old] "r" (oldval), [new] "r" (newval)
        : "memory");

    __disable_user_access();

    *uval = val;
    return ret;
}

futex vs eventfd vs io_uring: 벤치마크 결과

# 스레드 간 알림(ping-pong) 지연 비교 테스트
# 테스트 환경: x86_64, Intel Xeon Gold 6342, Linux 6.6

# 1. futex (FUTEX_WAIT/WAKE)
$ ./ipc_bench --method=futex --iterations=1000000
  Avg latency: 1.23 us
  P99 latency: 2.45 us
  P99.9 latency: 5.67 us

# 2. eventfd (read/write)
$ ./ipc_bench --method=eventfd --iterations=1000000
  Avg latency: 1.56 us
  P99 latency: 3.12 us
  P99.9 latency: 7.89 us

# 3. io_uring (IORING_OP_MSG_RING, Linux 6.0+)
$ ./ipc_bench --method=io_uring --iterations=1000000
  Avg latency: 0.89 us  (SQ polling mode)
  P99 latency: 1.67 us
  P99.9 latency: 3.45 us

# 4. pipe (write/read)
$ ./ipc_bench --method=pipe --iterations=1000000
  Avg latency: 2.34 us
  P99 latency: 4.56 us
  P99.9 latency: 12.34 us
벤치마크 해석: 비경합 시 futex가 가장 빠르지만(syscall 없음), ping-pong 테스트에서는 매 번 경합이 발생하므로 io_uring의 SQ polling이 유리합니다. 실제 워크로드에서는 경합 비율에 따라 최적 메커니즘이 달라집니다. 경합이 5% 미만이면 futex, 이벤트 기반 알림이 필요하면 eventfd, I/O와 결합된 동기화면 io_uring을 권장합니다.

glibc Adaptive Mutex의 스핀 전략

TSX Lock Elision이 사실상 무력화된 현재, glibc의 PTHREAD_MUTEX_ADAPTIVE_NP가 짧은 경합에 대한 실질적인 최적화입니다.

/* glibc nptl/pthread_mutex_lock.c — adaptive 스핀 (간소화) */
static int
adaptive_spin(pthread_mutex_t *mutex)
{
    int max_spin = 100;  /* 기본 스핀 횟수 */
    int cnt = 0;

    /* 소유자가 다른 CPU에서 실행 중인지 확인 */
    int owner_tid = mutex->__data.__owner;
    if (owner_tid == 0)
        return 0;  /* 이미 해제됨 */

    /* /proc/[tid]/status를 통해 확인하는 것이 아니라,
     * 커널이 FUTEX_WAIT에서 반환할 때의 조건으로 판단 */
    while (cnt++ < max_spin) {
        if (atomic_load_relaxed(&mutex->__data.__lock) == 0)
            return 0;  /* 잠금 해제됨: cmpxchg 시도 */

        /* CPU 힌트: x86에서 PAUSE 명령 */
        __asm__ volatile("pause" ::: "memory");
    }

    return 1;  /* 스핀 실패: futex_wait로 전환 */
}
Adaptive mutex 벤치마크: 임계 구역이 100ns 미만인 워크로드에서 adaptive mutex는 일반 mutex 대비 30~60% 높은 처리량을 보입니다. 반면 임계 구역이 1us 이상이면 스핀 오버헤드만 추가되므로 일반 mutex가 더 효율적입니다. PTHREAD_MUTEX_ADAPTIVE_NP는 GNU 확장이므로 이식성이 필요하면 PTHREAD_MUTEX_NORMAL을 사용하세요.

참고 자료

Futex의 설계, 구현, 성능에 대한 공식 문서, 논문, 심층 기사입니다.

커널 공식 문서 및 man 페이지

LWN.net 심층 기사

학술 자료 및 외부 참고

Futex와 관련된 커널 동기화 주제를 참고하려면 아래 문서를 참조하세요.
관련 문서 링크
문서연관성
동기화 프리미티브 개요커널 내부 동기화의 전체 그림. Futex와 커널 잠금의 관계
메모리 배리어Futex의 원자적 연산과 메모리 순서 보장(Ordering)
RCU (Read-Copy-Update)읽기 위주 동기화의 대안적 접근
원자적 연산Futex fast path의 cmpxchg, atomic_exchange 기반
PREEMPT_RT실시간 커널에서의 PI futex 활용
동시성 디버깅lockdep, KCSAN을 활용한 futex 관련 버그 탐지
Lock-free 알고리즘Futex 없이 동기화하는 대안적 기법

Futex 커널 디버그 빌드 옵션

Futex 관련 커널 디버그 빌드 옵션
옵션역할성능 영향
CONFIG_FUTEXfutex 지원 활성화 (기본 y)없음
CONFIG_FUTEX_PIPI futex 지원없음 (PREEMPT_RT에서 필수)
CONFIG_DEBUG_PI_LISTPI 리스트 정합성 검증PI 연산 시 ~10% 오버헤드
CONFIG_DEBUG_RT_MUTEXESrt_mutex 디버그 검증PI futex 연산 시 ~20%
CONFIG_PROVE_LOCKINGlockdep 잠금 순서 검증모든 잠금 연산 ~30%
CONFIG_KASAN메모리 접근 오류 탐지전체 ~2-3x 느림
CONFIG_KCSAN데이터 레이스 탐지전체 ~2x 느림
개발 시 권장 설정: futex 관련 코드를 개발하거나 디버깅할 때는 CONFIG_PROVE_LOCKING=y, CONFIG_DEBUG_PI_LIST=y, CONFIG_DEBUG_RT_MUTEXES=y를 활성화하세요. lockdep은 futex 내부의 잠금 순서 위반을 초기에 탐지하여 교착 상태 버그를 방지합니다. 프로덕션 배포 시에는 비활성화합니다.