락 경합(lock contention)은 멀티코어 시스템에서 흔한 성능 병목 원인이다. CPU 사용률은 높은데 실제 작업 처리량은 그대로거나 오히려 떨어진다면, 스레드들이 실제 일을 하는 대신 서로 락을 기다리며 시간을 보내고 있을 가능성이 크다. 이런 상황을 가장 빠르게 확인할 수 있는 도구가 perf top이다. 커널과 유저 공간을 통틀어 지금 이 순간 CPU 사이클을 가장 많이 쓰는 함수를 실시간으로 보여주는 샘플링 기반 프로파일러라서, 락 관련 함수가 상위권에 계속 보인다면 그 자체가 경합 신호다. 이 글에서는 perf top으로 스핀락/뮤텍스/퓨텍스 관련 함수를 실시간으로 관찰하는 방법과, 실제로 락 경합을 재현해서 어떤 함수가 상위에 잡히는지 확인한 결과를 정리한다.
준비 사항
커널 설정과 perf 설치
perf를 쓰려면 커널이 CONFIG_PERF_EVENTS=y로 빌드되어 있어야 한다. 대부분의 배포판 커널은 기본으로 켜져 있으므로, 안 되면 그때 확인하면 된다.
$ grep CONFIG_PERF_EVENTS /boot/config-$(uname -r)
CONFIG_PERF_EVENTS=y
$ sudo apt install linux-tools-common linux-tools-$(uname -r) linux-tools-generic
$ perf --version
perf version 6.17.13
접근 권한 설정
일반 사용자가 커널 이벤트를 샘플링하려면 perf_event_paranoid 값을 낮춰야 한다. 최근 배포판은 강화된 기본값(예: 4)이 설정되어 있는 경우가 있으므로, 값을 확인부터 하는 게 좋다.
$ cat /proc/sys/kernel/perf_event_paranoid
4
$ echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
-1이 가장 느슨한 값(커널/유저 이벤트 모두 제한 없이 접근)이고, 값이 클수록 더 제한적이다. 재부팅하면 원래 값으로 돌아가므로 영구 적용하려면 /etc/sysctl.d/에 설정을 남겨야 한다. sudo로 perf를 실행할 거라면 이 단계는 건너뛰어도 된다.
기본 사용법
$ sudo perf top
실행하면 상단에 다음과 같은 헤더가 뜨고, 그 아래로 Overhead가 높은 함수 순으로 목록이 실시간 갱신된다.
PerfTop: 1520 irqs/sec kernel: 42.3% exact: 100.0% lost: 0/0 drop: 0/0
[4000Hz cpu-clock:ppp], (all, 6 CPUs)
-------------------------------------------------------------------------------
Overhead Shared Object Symbol
12.34% [kernel.kallsyms] _raw_spin_unlock_irqrestore
9.87% [kernel.kallsyms] finish_task_switch.isra.0
5.21% [kernel.kallsyms] __schedule
perf top은 화면을 계속 다시 그리는 인터랙티브 TUI라서 실제 터미널에서 직접 실행해야 한다. SSH로 파이프에 연결하거나 | grep으로 걸러보려 하면 화면 갱신 대신 단축키 안내만 한 번 찍고 멈춘다 — 스크립트로 값을 뽑아내거나 로그로 남기고 싶다면 뒤에서 다룰 perf record + perf report --stdio 조합을 쓰는 게 맞다.
특정 함수·DSO로 좁혀보기
화면에 뜨는 함수가 너무 많다면 --symbols로 관심 있는 심볼만 남기거나, --dsos로 특정 바이너리/커널만 남길 수 있다(둘 다 콤마로 여러 개를 나열한다).
$ sudo perf top --symbols=_raw_spin_unlock_irqrestore,futex_wake
$ sudo perf top --dsos=[kernel.kallsyms]
커널 자체의 뮤텍스·rwsem 경합을 들여다보고 싶다면 아래 함수들이 자주 상위권에 등장하는 이름이다. 다만 정확한 심볼명은 커널 버전마다 조금씩 바뀔 수 있으니, --symbols에 넣기 전에 perf top 화면에서 실제 존재하는 이름인지 먼저 확인하는 게 안전하다.
__mutex_lock_slowpath— 커널mutex가 경합 상태일 때mutex_spin_on_owner— 뮤텍스 소유자를 스핀하며 기다릴 때rwsem_down_write_slowpath— 읽기/쓰기 세마포어 쓰기 락 경합_raw_spin_lock/_raw_spin_unlock_irqrestore— 스핀락 계열futex_wake/futex_wait— 유저스페이스 락(pthread_mutex등)이 커널로 넘어오는 지점
락 경합 재현해서 확인하기
스레드 여러 개가 같은 뮤텍스를 계속 붙잡고 노는 간단한 프로그램으로 경합을 인위적으로 만들어본다.
#include <pthread.h>
#include <unistd.h>
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static void *worker(void *arg) {
while (1) {
pthread_mutex_lock(&lock);
usleep(500);
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main(void) {
pthread_t t[4];
for (int i = 0; i < 4; i++)
pthread_create(&t[i], NULL, worker, NULL);
for (int i = 0; i < 4; i++)
pthread_join(t[i], NULL);
return 0;
}스레드 4개가 usleep(500) 동안 락을 붙잡고 있으니 나머지 스레드는 계속 대기하게 된다. 컴파일해서 백그라운드로 띄운다.
$ gcc -O2 -o lock_contend lock_contend.c -lpthread
$ ./lock_contend &
perf top은 화면 캡처가 안 되니, 스크립트로도 재현 가능하도록 perf record로 3초간 샘플을 뜬 뒤 perf report --stdio로 정적 결과를 뽑았다. 아래는 실제로 실행해서 받은 결과다.
$ sudo perf record -p $(pidof lock_contend) -o perf.data -- sleep 3
$ sudo perf report -i perf.data --stdio --sort overhead,symbol
73.75% [k] _raw_spin_unlock_irqrestore
21.48% [k] finish_task_switch.isra.0
1.67% [k] handle_softirqs
0.48% [.] __GI___lll_lock_wait
0.24% [k] __hrtimer_setup
0.24% [k] __schedule
1위가 __mutex_lock_slowpath가 아니라 _raw_spin_unlock_irqrestore인 게 눈에 띈다. __mutex_lock_slowpath는 커널 내부 코드가 커널 자체의 mutex를 잡을 때 경합하는 함수고, 지금 데모처럼 유저스페이스 pthread_mutex가 경합하면 glibc가 futex 시스템 콜로 넘어가고, 그 안에서 대기 큐를 조작하는 스핀락(_raw_spin_unlock_irqrestore)과 스케줄러 전환(finish_task_switch)이 실제 오버헤드로 잡힌다. __GI___lll_lock_wait는 glibc 쪽에서 보이는 락 대기 함수로, 유저스페이스 관점의 경합 신호다. 어떤 락 계층(커널 mutex인지, 유저스페이스 pthread_mutex인지)에서 경합이 일어나는지에 따라 상위에 잡히는 함수가 달라진다는 걸 실제로 확인할 수 있다.
고급 옵션
| 옵션 | 설명 |
|---|---|
-F <freq> | 샘플링 주파수 지정 (예: -F 999) |
-p <PID> | 특정 프로세스만 추적 |
-g | 호출 그래프(콜스택) 수집 |
--call-graph dwarf | DWARF 기반 호출 스택 추적 (디버그 심볼 필요) |
-K / -U | 커널 심볼 숨김 / 유저 심볼 숨김 |
--stdio는 perf top이 아니라 perf report/perf annotate 쪽 옵션이다. perf top은 애초에 라이브 TUI 전용이라 이 옵션이 없으므로, 스크립트에서 쓰려면 앞서처럼 perf record + perf report --stdio 조합으로 대체해야 한다.
함께 쓰면 좋은 도구
| 도구 | 목적 |
|---|---|
perf lock | 커널 락 경합 통계를 락 종류별로 집계 |
ftrace | 함수 단위 트레이싱 |
bpftrace | 동적 BPF 스크립트로 락 대기 시간까지 직접 추적 |
vmstat / mpstat | 런큐 길이·CPU 사용 패턴으로 경합 여부 1차 판단 |
주의사항
perf top은 인터랙티브 TUI다. SSH 세션에 tty가 없거나 출력을 파이프로 연결하면 실시간 갱신 대신 단축키 안내 화면만 찍히고 멈춘다 — 자동화가 필요하면perf record+perf report --stdio를 쓴다.--functions같은 옵션은 존재하지 않는다(실행하면Error: unknown option). 함수 이름으로 필터링할 때는--symbols를 쓴다.- 샘플링 주파수를 과도하게 높이면(
-F값을 크게) 그 자체가 오버헤드가 되고, NMI watchdog과 성능 카운터를 두고 경쟁할 수 있다. - 커널 심볼 이름은 버전마다 바뀔 수 있다(예: 어떤 커널에서는
rwsem_down_write_slowpath가 다른 이름으로 리팩터링되어 있기도 하다). 목록에 없다고 단정하지 말고perf top화면에서 실제 이름을 먼저 확인한다.
마무리
perf top은 락 병목을 찾는 가장 빠른 진입점이다. 다만 실제로 돌려보면 유저스페이스 락과 커널 락이 서로 다른 함수로 잡힌다는 걸 알 수 있다 — __mutex_lock_slowpath만 찾아보다가는 pthread_mutex 경합에서 나오는 futex/스핀락 계열 오버헤드를 놓칠 수 있다. 상위 함수가 어느 락 계층에 속하는지까지 확인한 뒤, 더 깊은 원인 분석이 필요하면 perf lock이나 bpftrace로 넘어가는 흐름이 실전에 가깝다.