Real-Time Linux (PREEMPT_RT)

PREEMPT_RT는 Linux 커널을 낮은 지터의 실시간 시스템으로 운용하기 위한 핵심 기반입니다. 이 문서는 스핀락의 rtmutex 전환, IRQ 스레딩, 우선순위 상속, SCHED_FIFO/RR/DEADLINE 운용, 고해상도 타이머와 주기 태스크 설계, cyclictest/ftrace 계측, CPU 격리와 운영 안정성 점검 포인트까지 상세히 설명합니다.

전제 조건: 프로세스 스케줄러프로세스 문서를 먼저 읽으세요. 실행 단위 관리 주제는 태스크 상태 전이와 큐 정책이 핵심이므로, 스케줄링 기준과 wakeup 경로를 먼저 이해해야 합니다.
일상 비유: 이 주제는 작업장 인력 배치와 호출 순서 관리와 비슷합니다. 긴급 작업, 대기열, 우선순위를 함께 조정하듯이 커널도 태스크 분류와 깨우기 정책이 전체 반응성을 결정합니다.

핵심 요약

  • Threaded IRQ — 인터럽트를 스레드화해 우선순위 제어 가능
  • RT Mutex — priority inheritance로 inversion 완화
  • Preemptible kernel — 비선점 구간 최소화
  • Latency 측정 — cyclictest/ftrace 기반 검증 필수
  • CPU isolation — 실시간 태스크 전용 코어 분리

단계별 이해

  1. 기준 측정
    기존 커널에서 레이턴시 baseline을 먼저 측정합니다.
  2. RT 커널 구성
    PREEMPT_RT 설정과 IRQ 스레딩을 활성화합니다.
  3. 정책 튜닝
    스케줄링 클래스와 CPU 격리, 메모리 잠금 정책을 조정합니다.
  4. 회귀 검증
    stress + trace 기반으로 worst-case latency를 반복 확인합니다.
관련 문서: 프로세스 스케줄러 (실시간 스케줄링), 인터럽트 (인터럽트 처리), 타이머 (고해상도 타이머), cpusets & CPU Isolation (CPU 격리)

개요

Real-Time Linux는 시간 제약이 있는 작업(산업 제어, 로봇, 오디오/비디오, 통신)을 위해 결정론적 응답 시간을 제공합니다.

하드웨어 IRQ 상위 절반(hardirq) 일반 커널 softirq/tasklet 경유 PREEMPT_RT IRQ Thread로 전환 우선순위 스케줄링 결정론적 레이턴시

실시간 시스템 특성

구분 설명
Hard Real-Time Deadline 위반 시 시스템 실패 (예: ABS 브레이크, 의료기기)
Soft Real-Time Deadline 위반 시 성능 저하 (예: 비디오 스트리밍, VoIP)
Firm Real-Time Deadline 위반 결과는 무용지물 (예: 주식 거래)

Preemption 모드

모드 설명 레이턴시
PREEMPT_NONE 서버/배치 워크로드. 명시적 preemption point만 ~수 ms
PREEMPT_VOLUNTARY 데스크탑. 추가 preemption point ~수백 μs
PREEMPT 저지연. 커널 대부분이 선점 가능 ~수십 μs
PREEMPT_RT Hard Real-Time. 인터럽트도 선점 가능 ~수 μs

PREEMPT_RT 패치셋

주요 기능

PREEMPT_RT 설치

# 1. RT 패치 다운로드
$ wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/6.6/patch-6.6-rt15.patch.xz
$ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz

# 2. 커널 소스 압축 해제 및 패치 적용
$ tar xf linux-6.6.tar.xz
$ cd linux-6.6
$ xzcat ../patch-6.6-rt15.patch.xz | patch -p1

# 3. 커널 설정
$ make menuconfig
# General setup → Preemption Model → Fully Preemptible Kernel (Real-Time)

# 4. 빌드 및 설치
$ make -j$(nproc)
$ sudo make modules_install install

Interrupt Threading

PREEMPT_RT는 하드웨어 인터럽트를 커널 스레드로 처리하여 우선순위 기반 스케줄링을 가능하게 합니다.

Threaded IRQ 구조

/* 일반 커널 */
Hardware IRQ → hardirq → softirq → process

/* PREEMPT_RT 커널 */
Hardware IRQ → 최소 hardirq → IRQ thread (스케줄 가능) → process

IRQ 스레드 확인

# IRQ 스레드 목록 (PREEMPT_RT에서)
$ ps aux | grep irq
root         3  0.0  0.0      0     0 ?        S    Jan01   0:00 [irq/9-acpi]
root        12  0.0  0.0      0     0 ?        S    Jan01   0:01 [irq/16-ehci_hcd]
root        14  0.0  0.0      0     0 ?        S    Jan01   0:05 [irq/19-eth0]

# IRQ 스레드 우선순위 설정
$ sudo chrt -f -p 90 $(pgrep -f "irq/19-eth0")

request_threaded_irq() API

#include <linux/interrupt.h>

/* Threaded IRQ 등록 */
int request_threaded_irq(
    unsigned int irq,
    irq_handler_t handler,        /* hardirq handler (빠른 처리) */
    irq_handler_t thread_fn,     /* thread handler (스케줄 가능) */
    unsigned long irqflags,
    const char *devname,
    void *dev_id
);

/* 예시: 네트워크 드라이버 */
static irqreturn_t eth_irq_handler(int irq, void *dev_id)
{
    /* 최소한의 처리: IRQ 승인, 레지스터 읽기 */
    u32 status = read_reg(STATUS_REG);
    if (!(status & IRQ_PENDING))
        return IRQ_NONE;

    /* IRQ 승인 */
    write_reg(STATUS_REG, status);

    return IRQ_WAKE_THREAD;  /* thread_fn 호출 요청 */
}

static irqreturn_t eth_irq_thread(int irq, void *dev_id)
{
    /* 시간이 걸리는 처리: 패킷 수신, 메모리 할당 등 */
    process_rx_packets();
    return IRQ_HANDLED;
}

/* 등록 */
request_threaded_irq(dev->irq, eth_irq_handler, eth_irq_thread,
                      IRQF_SHARED, "eth0", dev);

RT Mutex (Priority Inheritance)

PREEMPT_RT는 spinlock을 rt_mutex로 대체하여 우선순위 역전(Priority Inversion)을 방지합니다.

Priority Inversion 문제

❌ Spinlock (Priority Inversion 발생) t=0 시간 → Low (Lock 보유) Low 완료 Medium 선점 High 대기 (블록됨!) High 실행 H가 M 때문에 지연! ✅ RT Mutex (Priority Inheritance) t=0 Low (Lock 보유) Low ↑ High 우선순위 상승 Medium 대기 High 실행 H 대기 → L 우선순위 상승 High 우선순위 Medium 우선순위 Low 우선순위
/* 시나리오: High(H), Medium(M), Low(L) 우선순위 태스크 */
1. L이 spinlock 획득
2. H가 같은 spinlock 대기 → L 완료까지 블록
3. M이 실행 가능 → M이 L을 선점
4. 결과: H가 M이 끝날 때까지 대기 (우선순위 역전!)

/* Priority Inheritance 해결책 */
1. L이 rt_mutex 획득
2. H가 같은 rt_mutex 대기
3. L의 우선순위가 일시적으로 H 수준으로 상승
4. L이 M을 선점하여 빠르게 완료
5. H가 mutex 획득, L은 원래 우선순위로 복귀

rt_mutex API

#include <linux/rtmutex.h>

struct rt_mutex {
    raw_spinlock_t wait_lock;
    struct rb_root_cached waiters;  /* 우선순위 정렬 대기 큐 */
    struct task_struct *owner;
};

/* rt_mutex 초기화 */
struct rt_mutex lock;
rt_mutex_init(&lock);

/* 획득/해제 */
rt_mutex_lock(&lock);
/* critical section */
rt_mutex_unlock(&lock);

/* trylock */
if (rt_mutex_trylock(&lock)) {
    /* 성공 */
    rt_mutex_unlock(&lock);
}

RT Mutex vs Spinlock 비교

특성 Spinlock RT Mutex
대기 방식 Busy-wait (CPU 점유) Sleep (스케줄 아웃)
Context Atomic (인터럽트 가능) Non-atomic (sleep 가능)
Priority Inversion 발생 가능 ❌ Priority Inheritance로 방지 ✅
Preemption 비활성화 (spinlock_t) 또는 활성화 (raw_spinlock_t) 활성화 (선점 가능)
사용 시기 매우 짧은 임계 구간 (<수십 μs) 긴 임계 구간, 실시간 요구사항
오버헤드 낮음 (단순 busy-wait) 높음 (스케줄링 오버헤드)
PREEMPT_RT 대부분 rt_mutex로 대체됨 기본 락 메커니즘
Spinlock Lock 요청 Busy-Wait Loop (CPU 100% 소비) Lock 획득까지 반복 Critical Section Unlock RT Mutex Lock 요청 대기 큐에 추가 (Sleep, CPU 양보) Wake-up 후 Critical Section Unlock + Wake Next
PREEMPT_RT에서의 변화: 일반 커널의 spinlock_t는 PREEMPT_RT에서 자동으로 rt_mutex로 대체됩니다. 진짜 spinlock이 필요한 경우(예: 인터럽트 핸들러)에만 raw_spinlock_t를 사용하세요.

실시간 스케줄링

실시간 스케줄링 정책

정책 설명 우선순위
SCHED_FIFO First-In First-Out. 같은 우선순위 내 순서대로 1~99 (99 최고)
SCHED_RR Round-Robin. FIFO + Time Slice 1~99
SCHED_DEADLINE EDF (Earliest Deadline First). CBS 기반 동적

SCHED_FIFO 예시

#include <sched.h>
#include <pthread.h>

void set_realtime_priority(int priority)
{
    struct sched_param param;
    param.sched_priority = priority;  /* 1~99 */

    if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) {
        perror("sched_setscheduler");
    }
}

/* pthread 사용 */
pthread_t thread;
struct sched_param param = { .sched_priority = 80 };
pthread_setschedparam(thread, SCHED_FIFO, ¶m);

SCHED_DEADLINE

SCHED_DEADLINE은 EDF(Earliest Deadline First) 알고리즘과 CBS(Constant Bandwidth Server)를 결합한 스케줄러입니다.

#include <sched.h>
#include <linux/sched.h>

struct sched_attr {
    u32 size;
    u32 sched_policy;     /* SCHED_DEADLINE */
    u64 sched_flags;
    s32 sched_nice;
    u32 sched_priority;

    /* SCHED_DEADLINE 파라미터 */
    u64 sched_runtime;    /* 나노초 단위 실행 시간 */
    u64 sched_deadline;   /* 상대 deadline */
    u64 sched_period;     /* 주기 */
};

/* 예: 10ms마다 3ms 실행 시간, 10ms deadline */
struct sched_attr attr = {
    .size = sizeof(attr),
    .sched_policy = SCHED_DEADLINE,
    .sched_runtime  = 3 * 1000000,   /* 3ms */
    .sched_deadline = 10 * 1000000,  /* 10ms */
    .sched_period   = 10 * 1000000,  /* 10ms */
};

sched_setattr(0, &attr, 0);

High-Resolution Timers

PREEMPT_RT는 나노초 정밀도의 고해상도 타이머를 제공합니다.

hrtimer API

#include <linux/hrtimer.h>

struct hrtimer {
    struct timerqueue_node node;
    ktime_t _softexpires;
    enum hrtimer_restart (*function)(struct hrtimer *);
    struct hrtimer_clock_base *base;
    u8 state;
};

/* hrtimer 초기화 */
struct hrtimer timer;
hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
timer.function = &my_hrtimer_callback;

/* 타이머 시작 (100μs 후) */
ktime_t ktime = ktime_set(0, 100000);  /* 0초 + 100000ns */
hrtimer_start(&timer, ktime, HRTIMER_MODE_REL);

/* 콜백 함수 */
static enum hrtimer_restart my_hrtimer_callback(struct hrtimer *timer)
{
    /* 실시간 작업 수행 */
    return HRTIMER_NORESTART;  /* or HRTIMER_RESTART */
}

/* 타이머 취소 */
hrtimer_cancel(&timer);

레이턴시 측정

cyclictest

cyclictest는 실시간 레이턴시 측정 표준 도구입니다.

# rt-tests 패키지 설치
$ sudo apt install rt-tests

# 기본 테스트 (10분)
$ sudo cyclictest -m -Sp99 -D 10m

# 상세 옵션
$ sudo cyclictest \
    -m              # mlockall() 메모리 고정
    -Sp99           # SCHED_FIFO, 우선순위 99
    -i 1000         # 1ms 간격
    -h 100          # 히스토그램 100μs 범위
    -D 1h           # 1시간 테스트
    -q              # quiet 모드
    --histfile=hist.txt

# 출력 예시
T: 0 (1234) P:99 I:1000 C: 600000 Min:      2 Act:    3 Avg:    3 Max:      12
#       T: 스레드 ID
#       Min: 최소 레이턴시 (μs)
#       Act: 현재 레이턴시
#       Avg: 평균 레이턴시
#       Max: 최대 레이턴시 (중요!)

ftrace 레이턴시 추적

# irqsoff tracer: 인터럽트 비활성화 시간 측정
# echo irqsoff > /sys/kernel/debug/tracing/current_tracer
# echo 1 > /sys/kernel/debug/tracing/tracing_on
# sleep 10
# cat /sys/kernel/debug/tracing/trace_stat/function0

# preemptoff tracer: 선점 비활성화 시간 측정
# echo preemptoff > /sys/kernel/debug/tracing/current_tracer

# wakeup tracer: 스케줄링 레이턴시 측정
# echo wakeup > /sys/kernel/debug/tracing/current_tracer
# echo 1 > /sys/kernel/debug/tracing/tracing_on

성능 비교

PREEMPT_RT 커널과 일반 커널의 레이턴시 및 처리량 비교입니다. 실제 워크로드에 따라 결과는 달라질 수 있습니다.

레이턴시 벤치마크

워크로드 일반 커널 (PREEMPT) PREEMPT_RT 개선율
Worst-case latency ~200 μs ~15 μs 93% ↓
Average latency ~8 μs ~5 μs 37% ↓
Jitter (표준편차) ±25 μs ±3 μs 88% ↓
99.9% percentile ~50 μs ~10 μs 80% ↓
측정 환경: Intel Core i7-8700K, 16GB RAM, cyclictest -m -Sp99 -D 1h, stress-ng 백그라운드 부하

워크로드별 성능 특성

워크로드 유형 일반 커널 PREEMPT_RT 권장사항
산업 제어
(PLC, 모션 컨트롤)
❌ 불안정
레이턴시 스파이크 발생
✅ 안정적
<20 μs 보장
PREEMPT_RT 필수
오디오 처리
(Jack, PipeWire)
⚠️ 가능
버퍼 크기 증가 필요
✅ 우수
64 샘플 버퍼 가능
PREEMPT_RT 권장
네트워크 (low-latency)
(금융, HFT)
⚠️ 제한적
P99 latency 높음
✅ 우수
일관된 레이턴시
PREEMPT_RT + CPU isolation
비디오 스트리밍 ✅ 충분
Soft RT로 처리 가능
✅ 더 안정적
프레임 드롭 감소
일반 커널로 충분
웹 서버
(처리량 중심)
✅ 우수
높은 처리량
⚠️ 낮은 처리량
~5-10% 감소
일반 커널 권장
데이터베이스 ✅ 우수
높은 처리량
⚠️ 낮은 처리량
트랜잭션 속도 감소
일반 커널 권장
컴파일/빌드 ✅ 빠름 ⚠️ 느림
~10-15% 증가
일반 커널 권장

Trade-offs 요약

항목 PREEMPT_RT 장점 PREEMPT_RT 단점
레이턴시 Worst-case 극적 감소 (93% ↓) Average 약간 증가 (스케줄링 오버헤드)
처리량 예측 가능성 향상 전체 처리량 5-15% 감소
전력 소비 - 더 많은 context switch → 전력 증가
복잡도 표준 Linux API 사용 튜닝 복잡 (CPU isolation, IRQ affinity 등)
디버깅 ftrace/perf 도구 사용 가능 타이밍 버그 재현 어려움
선택 기준:
  • Hard RT 필요 (산업, 로봇, 의료) → PREEMPT_RT 필수
  • Low-latency 선호 (오디오, 네트워크) → PREEMPT_RT 권장
  • 처리량 중심 (웹, DB, 빌드) → 일반 커널 권장

CPU Isolation

실시간 워크로드를 방해 없이 실행하려면 CPU를 격리해야 합니다.

isolcpus 커널 파라미터

# GRUB 설정 (/etc/default/grub)
GRUB_CMDLINE_LINUX="isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3"

# isolcpus=2,3      — CPU 2,3을 일반 스케줄러에서 제외
# nohz_full=2,3     — CPU 2,3에서 타이머 틱 비활성화 (adaptive-tick mode)
# rcu_nocbs=2,3     — CPU 2,3의 RCU 콜백을 다른 CPU로 오프로드

$ sudo update-grub
$ sudo reboot

# 격리된 CPU에 태스크 바인딩
$ taskset -c 2 ./realtime_app

IRQ Affinity 설정

# IRQ를 특정 CPU로 제한 (CPU 0,1만 사용)
# echo 3 > /proc/irq/19/smp_affinity  # 0x03 = CPU 0,1

# irqbalance 비활성화 (수동 IRQ 관리 시)
$ sudo systemctl stop irqbalance
$ sudo systemctl disable irqbalance

실시간 시스템 Best Practices

실시간 애플리케이션 체크리스트

실시간 애플리케이션 예시

#include <sched.h>
#include <sys/mman.h>
#include <string.h>

void setup_realtime(void)
{
    /* 1. 메모리 고정 (swap 방지) */
    mlockall(MCL_CURRENT | MCL_FUTURE);

    /* 2. 스택 프리페칭 (page fault 방지) */
    unsigned char dummy[8192];
    memset(dummy, 0, sizeof(dummy));

    /* 3. 실시간 스케줄링 설정 */
    struct sched_param param = { .sched_priority = 90 };
    sched_setscheduler(0, SCHED_FIFO, ¶m);

    /* 4. CPU 어피니티 (격리된 CPU 2) */
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(2, &cpuset);
    sched_setaffinity(0, sizeof(cpuset), &cpuset);
}

void realtime_loop(void)
{
    struct timespec next, interval = { .tv_nsec = 1000000 };  /* 1ms */
    clock_gettime(CLOCK_MONOTONIC, &next);

    while (1) {
        /* 실시간 작업 */
        do_critical_work();

        /* 다음 주기까지 대기 */
        next.tv_nsec += interval.tv_nsec;
        if (next.tv_nsec >= 1000000000) {
            next.tv_sec++;
            next.tv_nsec -= 1000000000;
        }
        clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
    }
}

커널 설정

# Preemption Model
CONFIG_PREEMPT_RT=y                    # Fully Preemptible Kernel (RT)
CONFIG_PREEMPT_RT_FULL=y

# High-Resolution Timers
CONFIG_HIGH_RES_TIMERS=y
CONFIG_NO_HZ_FULL=y                    # Adaptive-tick mode

# CPU Isolation
CONFIG_RCU_NOCB_CPU=y                  # RCU callback offloading
CONFIG_NO_HZ_FULL_ALL=y

# IRQ Threading
CONFIG_IRQ_FORCED_THREADING=y

# Disable for RT
# CONFIG_DEBUG_PREEMPT is not set       # 디버그 오버헤드 제거
# CONFIG_PROVE_LOCKING is not set
# CONFIG_DEBUG_LOCK_ALLOC is not set

디버깅

RT Throttling

SCHED_FIFO/RR 태스크가 CPU를 독점하지 못하도록 제한합니다.

# RT 태스크는 기본적으로 950ms/1초만 사용 가능
# cat /proc/sys/kernel/sched_rt_period_us
1000000

# cat /proc/sys/kernel/sched_rt_runtime_us
950000

# RT 제한 해제 (주의: 시스템 행 가능)
# echo -1 > /proc/sys/kernel/sched_rt_runtime_us

trace-cmd

# trace-cmd 설치
$ sudo apt install trace-cmd

# 레이턴시 추적
$ sudo trace-cmd record -p function_graph -P 1234  # PID 1234 추적
$ sudo trace-cmd report

트러블슈팅

일반적인 문제와 해결

문제 원인 해결 방법
레이턴시 스파이크
(100+ μs)
SMI (System Management Interrupt)
BIOS 레벨 인터럽트
• BIOS에서 SMI 비활성화
hwlatdetect로 SMI 감지
• 최신 펌웨어 업데이트
예상보다 높은 레이턴시 CPU 전력 관리 (C-states)
주파수 스케일링
• C-states 비활성화: processor.max_cstate=1
• 고정 주파수: intel_pstate=disable
• Performance governor 설정
RT 태스크가 실행 안 됨 RT Throttling 제한
(기본 95% CPU)
sched_rt_runtime_us 증가
• 또는 -1로 제한 해제 (주의)
시스템 응답 없음 RT 태스크 무한 루프
일반 태스크 CPU 못 받음
• Magic SysRq 사용: Alt+SysRq+k
• Watchdog 활성화
• RT throttling 유지
mlockall() 실패
(ENOMEM)
RLIMIT_MEMLOCK 제한 ulimit -l unlimited
• /etc/security/limits.conf 설정
• CAP_IPC_LOCK capability 부여
IRQ 스레드 없음 PREEMPT_RT 아닌 커널
또는 드라이버 미지원
ps aux | grep irq 확인
• RT 커널 재빌드
• 드라이버 request_threaded_irq() 사용 확인
cyclictest 높은 latency 백그라운드 프로세스
IRQ affinity 미설정
• 불필요한 서비스 중지
• IRQ affinity 설정
• CPU isolation 적용

레이턴시 스파이크 원인 분석

# 1. SMI (System Management Interrupt) 감지
$ sudo hwlatdetect --duration=60
Samples: 3600
Max latency: 127 us
SMI count: 12  ← SMI가 레이턴시 원인!

# 해결: BIOS에서 SMI 소스 비활성화 (USB legacy, thermal monitoring 등)

# 2. CPU 전력 관리 확인
$ cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
powersave  ← 문제! performance로 변경 필요

$ sudo cpupower frequency-set -g performance

# 3. C-states 확인 및 비활성화
$ cat /sys/devices/system/cpu/cpu0/cpuidle/state*/disable
0
0  ← 모든 C-states 활성화 상태 (문제)

# GRUB 파라미터로 C-states 제한
GRUB_CMDLINE_LINUX="processor.max_cstate=1 intel_idle.max_cstate=0"

# 4. ftrace로 스파이크 원인 추적
$ sudo trace-cmd record -p function_graph \
  -e irq -e sched -e timer \
  -F cyclictest -m -Sp99 -D 1m

$ sudo trace-cmd report | grep "duration > 50"  # 50μs 이상 찾기

# 5. 프로세스별 레이턴시 기여도 확인
$ ps -eLo pid,tid,class,rtprio,pri,nice,cmd | grep FF
  1234  1234 FF      99  139   - ./realtime_app  ← RT 태스크
  5678  5678 FF      50   90   - [irq/19-eth0]    ← IRQ 스레드

진단 체크리스트

# RT 시스템 상태 종합 점검 스크립트

## 1. 커널 버전 및 PREEMPT_RT 확인
$ uname -a | grep PREEMPT_RT
$ cat /sys/kernel/realtime  # 1이면 RT 커널

## 2. CPU Governor 확인
$ cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor | sort -u

## 3. IRQ Threading 확인
$ ps aux | grep "\[irq/" | wc -l  # 0이면 문제

## 4. RT Throttling 설정
$ cat /proc/sys/kernel/sched_rt_runtime_us

## 5. Isolated CPU 확인
$ cat /sys/devices/system/cpu/isolated

## 6. RCU callback offloading
$ cat /sys/devices/system/cpu/nohz_full

## 7. 메모리 잠금 한계
$ ulimit -l  # unlimited 여야 함

## 8. SMI 카운트 (Intel)
$ sudo rdmsr -a 0x34  # SMI_COUNT MSR
주의: RT 시스템에서 디버깅 도구(ftrace, perf)를 사용하면 레이턴시가 증가합니다. 최종 검증 시에는 디버깅 도구 없이 측정하세요.

참고자료

다음 학습: