printf 안에서 결국 write(1, ...) 가 호출되고, 그게 kernel 로 진입한다. user mode 와 kernel mode 의 경계를 넘는 그 순간 — syscall — 의 비용이 100 ns. 왜 그런지, 어떻게 줄이는지 (vDSO, io_uring) 이 가이드는 정리한다.
User mode vs Kernel mode
CPU 의 protection ring:
Ring 0 — kernel (모든 명령 사용 가능, 모든 메모리 접근)
Ring 1, 2 — 잘 안 씀
Ring 3 — user application (제한적)
User mode 에서는 못 함:
- I/O port 직접 접근 (disk, network card)
- 임의 물리 메모리 read/write
- interrupt mask 변경
- 다른 process 의 메모리 접근
→ kernel 에 요청해야 함 = syscallsyscall 의 실제 명령
write(1, "hi", 2);
내부 (x86-64 Linux):
mov rax, 1 ; syscall number (1 = write)
mov rdi, 1 ; arg1 = fd (1 = stdout)
mov rsi, msg ; arg2 = buffer pointer
mov rdx, 2 ; arg3 = size
syscall ; ← CPU 명령 한 줄
"syscall" 명령이 하는 일:
1. user mode → kernel mode 전환 (privilege escalation)
2. PC 를 kernel 의 syscall handler 주소로 jump
3. user 의 rsp, rip, flags 등 저장
4. kernel stack 으로 전환
kernel handler:
- rax 의 syscall number 로 dispatch table lookup
- sys_write() 함수 호출
- 결과 rax 에 박음
- "sysret" 명령으로 user 로 복귀왜 100 ns 인가
함수 호출은 1 ns 인데 syscall 은 100 ns. 100 배 차이 출처:
- privilege 전환 — CR3 register (page table base) 갱신 가능
- stack 전환 — user → kernel stack
- register save/restore — user 의 모든 register 백업
- cache · TLB pollution — kernel 코드 fetch → I-cache miss, kernel 데이터 access → D-cache miss
- Meltdown / Spectre 대응 (KPTI) — 2018 이후 page table 전체 교체. syscall cost 거의 2× ↑
strace — 어떤 syscall 이 나가나
$ strace -c ls
% time seconds usecs/call calls errors syscall
------ -------- ---------- ----- ------ ----------
60.32 0.000045 0 89 read
19.46 0.000015 0 37 write
9.32 0.000007 0 10 openat
...
→ 가장 빈번한 syscall 보고. hot path 의 syscall 수 확인.
특정 syscall 만:
$ strace -e trace=open,read ./program
attach 도 가능:
$ strace -p $(pidof program)vDSO — syscall 없는 syscall
gettimeofday(), clock_gettime() 같은 "kernel state 만 read" 함수는 매번 syscall 비싸다. 해결: kernel 이 user-space 에 read-only 페이지를 mmap 해줌.
$ cat /proc/self/maps | grep vdso
7ffd...000-7ffd...000 r-xp 00000000 00:00 0 [vdso]
vDSO 에 박힌 함수:
- clock_gettime
- gettimeofday
- getcpu
- time
glibc 의 clock_gettime 구현:
if (vdso_clock_gettime != NULL)
return vdso_clock_gettime(clk, tp); ← 일반 함수 호출, ~5 ns
else
return syscall(SYS_clock_gettime, clk, tp); ← fallback, ~100 ns
→ 같은 함수 호출인데 100× 빠름.Syscall 비용 줄이기 — 디자인 패턴
1. Batch / vectored I/O
100 회 write(fd, &b, 1) → 100 syscall = 10 μs
1 회 write(fd, buf, 100) → 1 syscall = 100 ns
→ 100 배 빠름. stdio (printf) 의 buffering 이 이거.
writev() / readv() — 여러 buffer 한 syscall:
struct iovec iov[3] = {
{hdr, hdr_len},
{body, body_len},
{footer, footer_len}
};
writev(fd, iov, 3); // 한 syscall 로 3 영역 send2. mmap — read 대신 메모리 매핑
전통:
for (...) read(fd, buf, size); // 매 read = syscall
mmap:
char* p = mmap(NULL, file_size, PROT_READ, MAP_SHARED, fd, 0);
// 한 syscall 로 파일 전체를 메모리에 매핑
// 이후 p[i] access = 그냥 메모리 접근 (page fault 시만 kernel)
→ 대용량 파일 random access 에 유리.3. io_uring (Linux 5.1+) — async syscall 묶음
전통:
read(fd1, b1, n); // 100 ns syscall
read(fd2, b2, n); // 100 ns syscall
read(fd3, b3, n); // 100 ns syscall
→ 300 ns + 3 context switch
io_uring:
submission queue 에 3 read 박음 (memory write, syscall 없음)
io_uring_enter() 한 번 (single syscall)
kernel 이 async 로 3 처리 → completion queue 에 결과
→ 100 ns + 0 추가 context switch
→ disk-heavy / network-heavy 워크로드에서 큰 성능.Modern 측정 — eBPF + bpftrace
# 현재 시스템에서 가장 느린 syscall 들 top 10
sudo bpftrace -e '
tracepoint:raw_syscalls:sys_enter { @start[tid] = nsecs; }
tracepoint:raw_syscalls:sys_exit /@start[tid]/ {
@ns[probe] = hist(nsecs - @start[tid]);
delete(@start[tid]);
}'
→ kernel 변경 없이 production 에서 실시간 측정.OS 별 syscall ABI
| OS | 명령 | arg passing | 비고 |
|---|---|---|---|
| Linux x86-64 | syscall | rdi, rsi, rdx, r10, r8, r9 | 안정 ABI |
| macOS x86-64 | syscall | 같음 | 그러나 libSystem 우회는 비추천 |
| Windows | syscall (varies) | ntdll 통해서만 안전 | kernel ABI 비공식 |
| BSD | syscall | Linux 와 유사 |
WebAssembly + WASI
WebAssembly 의 file I/O / network 는 host 의 syscall 권한 필요. WASI 가 sandbox 안 syscall 추상. Cloudflare Workers, Wasmtime 등이 WASI 구현.
흔한 함정
- syscall in tight loop — 1M 회 가장 단순 syscall = 100 ms. profile 하면 의외의 hot spot.
- fork() 의 비용 — 부모 page table copy. 큰 process fork 가 GB 단위 메모리 copy 트리거 (CoW 라도 metadata).
- signal handler 에서 가능한 syscall 제한 — async-signal-safe syscall 만 안전 (printf 안 됨).
- strace 의 overhead — strace 자체가 ptrace syscall — production 측정에는 perf / eBPF 가 적합.
- EINTR 처리 누락 — signal 도착 시 read 가 -1 + EINTR 로 끝남. retry loop 필요.
마무리
Syscall 은 user 와 kernel 의 유일한 contract. 100 ns 의 cost 가 쌓이면 성능의 큰 부분이 거기로 간다. batch · mmap · vDSO · io_uring 의 발전은 모두 "syscall 줄이기" 의 다른 형태.
성능 분석 시작점: strace -c. syscall 수 / 종류 보고 과도하면 batching 검토.