본문으로 건너뛰기
yutils

system call 은 어떻게 동작할까?

user mode vs kernel mode, syscall 명령 (x86 syscall, ARM svc), strace 가 보여주는 것, syscall 최저 비용 100 ns 인 이유, vDSO (syscall 없는 clock_gettime), io_uring 의 syscall amortization.

약 9분 읽기

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 에 요청해야 함 = syscall

syscall 의 실제 명령

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 영역 send

2. 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-64syscallrdi, rsi, rdx, r10, r8, r9안정 ABI
macOS x86-64syscall같음그러나 libSystem 우회는 비추천
Windowssyscall (varies)ntdll 통해서만 안전kernel ABI 비공식
BSDsyscallLinux 와 유사

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 검토.

가이드 목록으로