Linux 시스템 콜이란: 사용자 모드·커널 모드와 strace 추적

반응형

Linux 시스템 콜(system call)은 사용자 공간 프로그램이 kernel에 파일, process, memory, network 같은 작업을 요청하는 ABI 경계다. 애플리케이션 코드의 함수 하나가 시스템 콜 하나와 반드시 일대일로 대응하는 것은 아니다. runtime·libc wrapper가 여러 시스템 콜을 조합하거나, 다른 이름의 시스템 콜을 선택하거나, vDSO로 kernel 진입을 피할 수도 있다.

운영에서 유용한 질문은 시스템 콜 이름을 많이 외우는 것이 아니라 “이 process가 어떤 kernel 요청에서 기다리거나 실패하는가”다. strace로 반환값, errno, file descriptor와 걸린 시간을 함께 보면 그 경계를 좁힐 수 있다.

사용자 함수에서 kernel까지의 흐름

애플리케이션 코드
  ↓
언어 runtime·표준 라이브러리·libc wrapper
  ↓
architecture별 system call ABI
  ↓
Linux kernel: 권한 검사와 자원 작업
  ↓
return value 또는 -1 + errno

예를 들어 C의 open()이나 Python의 open()을 추적했을 때 Linux에서는 openat()이 보일 수 있다. 고수준 함수의 이름과 실제 kernel entry point가 같을 것이라고 가정하면 trace를 잘못 읽는다.

시스템 콜 진입은 사용자 mode에서 kernel mode로 실행 권한을 전환하지만, 곧바로 다른 process로 바뀌는 scheduler context switch와 같은 말은 아니다. 같은 thread가 kernel code를 실행한 뒤 돌아올 수도 있고, I/O를 기다리며 잠들 때 scheduler가 다른 thread를 실행할 수도 있다.

자주 보는 시스템 콜을 작업별로 묶는다

질문 관련 시스템 콜 예 확인할 결과
어떤 파일을 열지 못했나 openat, newfstatat, readlink ENOENT, EACCES, 실제 path
어디에서 읽고 쓰나 read, write, pread64, pwrite64 fd, byte 수, EOF, partial I/O
memory를 어떻게 확보하나 mmap, munmap, mprotect, brk mapping 크기와 protection
process를 어떻게 만들고 바꾸나 clone, fork, execve, wait4 child PID, executable, exit status
network 어디에서 막히나 socket, bind, connect, accept4, sendto, recvfrom address, fd, ECONNREFUSED, timeout
무엇을 기다리나 futex, epoll_wait, poll, nanosleep wait 대상과 머문 시간

이름은 architecture와 kernel·libc version에 따라 다르게 보일 수 있다. 사용하는 host의 syscalls(2)와 개별 manual page를 기준으로 인자와 반환값을 확인한다.

strace 한 줄을 읽는 법

다음과 비슷한 출력이 있다고 하자.

openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 3</etc/hosts> <0.000042>
  • openat: 호출된 시스템 콜
  • AT_FDCWD, path, flags: 인자
  • = 3: 성공 반환값인 file descriptor
  • </etc/hosts>: -yy가 덧붙인 fd 경로 정보
  • <0.000042>: -T가 표시한 호출 안의 경과 시간

실패하면 -1 뒤에 errno가 나온다.

openat(AT_FDCWD, "/missing", O_RDONLY) = -1 ENOENT (No such file or directory)

애플리케이션 로그의 “설정 파일을 읽지 못했다”보다 실제로 시도한 path와 ENOENT를 보면 working directory, mount와 배포 누락을 구분하기 쉽다.

새 command를 좁혀 추적한다

파일·process·network 관련 호출만 보고 timestamp, 호출 시간과 fd 정보를 함께 표시한다.

strace -f -tt -T -yy \
  -e trace=%file,%process,%network \
  -- command arg1 arg2
  • -f: 생성된 child process·thread도 따라간다.
  • -tt: 각 event의 wall-clock timestamp를 표시한다.
  • -T: 각 system call 안에서 보낸 시간을 표시한다.
  • -yy: file descriptor와 연결된 path·socket 정보를 더 보여 준다.
  • -e trace=...: 필요한 범주만 남긴다.

호출 횟수와 시간 요약이 필요하면 별도 재현 환경에서 -c를 쓸 수 있다.

strace -f -c -- command arg1

이 수치는 tracer가 개입한 상태의 관측값이다. microbenchmark의 절대 성능이나 production latency를 그대로 대표한다고 보면 안 된다.

실행 중인 process에 attach할 때의 주의점

strace -f -p PID

straceptrace를 이용해 tracee의 system call과 signal을 관찰한다. attach는 thread 실행을 멈추고 재개하는 과정을 추가하므로 timing과 성능에 영향을 줄 수 있다. 지연에 민감하거나 부하가 큰 production process라면 짧은 시간, 좁은 filter와 안전한 중단 절차를 먼저 정한다.

container에서는 user namespace, seccomp profile, Yama ptrace_scope, capability 설정 때문에 attach가 거부될 수 있다. 이를 해결하려고 곧바로 privileged container로 바꾸기보다 필요한 권한과 데이터 노출 범위를 검토한다.

trace에는 다음과 같은 민감 정보가 나타날 수 있다.

  • command-line argument와 environment를 통해 전달된 secret
  • 파일 경로와 사용자 이름
  • read·write buffer의 일부
  • socket address와 프로토콜 payload

출력을 공유하기 전에 redaction하고, -s로 문자열 길이를 늘릴 때는 수집 범위가 커진다는 점을 인지한다.

strace에 안 보인다고 호출이 없었던 것은 아니다

Linux는 자주 쓰는 시간 조회 같은 기능을 vDSO로 사용자 공간에 제공할 수 있다. libc가 vDSO 함수를 사용하면 실제 system call trap이 발생하지 않으므로 strace에도 나타나지 않는다.

또한 buffered I/O는 애플리케이션의 여러 write()를 한 번의 kernel write로 합치거나, 한 번의 read로 가져온 데이터를 여러 고수준 호출에 나눠 줄 수 있다. JIT와 async runtime도 고수준 작업과 system call의 대응을 바꾼다.

따라서 trace가 없다는 사실은 “그 언어 함수가 실행되지 않았다”가 아니라 관찰한 kernel 경계에 해당 event가 없었다는 뜻으로 해석한다.

진단 순서

  1. 애플리케이션 로그에서 실패 시간과 process·thread를 정한다.
  2. 파일, process, network, memory 중 어느 system call 범주가 필요한지 고른다.
  3. 재현 가능한 command라면 새 process 추적부터 시작한다.
  4. 반환값과 errno, fd·path·address를 확인한다.
  5. -T가 긴 호출이 실제 대기인지 tracer overhead와 함께 판단한다.
  6. system call 아래 kernel 내부가 필요할 때 tracepoint·eBPF로 범위를 더 좁힌다.

kernel 내부 관측 지점을 고르는 기준은 eBPF Hook 선택, socket API 흐름은 TCP·UDP 소켓 프로그래밍, 시스템 콜 약어를 빠르게 찾는 표는 시스템 콜 약어 및 의미에서 이어진다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글