OSTEP Limited Direct Execution: 프로세스는 직접 실행되는데 OS는 어떻게 제어할까

반응형

OSTEP의 「Mechanism: Limited Direct Execution」을 읽으며 가장 먼저 생긴 질문은 이것이었다. 사용자 프로그램을 CPU에서 직접 실행해야 빠르다면, 무한 루프에 빠지거나 특권 명령을 시도하는 프로그램으로부터 운영체제는 어떻게 제어권을 되찾을까?

답은 평소에는 직접 실행하되, 위험하거나 제어가 필요한 경계에서는 하드웨어가 커널로 전환하도록 제한하는 것이다. 이 결합을 Limited Direct Execution(LDE)이라고 한다.

왜 ‘Limited’가 필요한가

모든 명령을 kernel이 대신 해석하면 통제하기는 쉽지만 느리다. 반대로 user program에 CPU를 완전히 맡기면 다음 문제가 생긴다.

  • device I/O나 page table 같은 보호 자원을 임의로 바꿀 수 있다.
  • 다른 process의 memory에 접근할 수 있다.
  • 무한 루프에서 system call을 하지 않으면 OS가 다시 scheduling할 기회를 얻지 못한다.

CPU는 privilege level을 나누고 user mode에서 privileged instruction을 제한한다. program이 OS 기능을 요청할 때는 system call, 잘못된 동작이 생기면 exception, 외부 장치나 timer가 알릴 때는 interrupt를 통해 kernel mode로 들어간다.

세 가지 kernel 진입 경로

경로 발생 주체 대표 예 성격
system call 실행 중인 program read, write, mmap 의도한 동기적 요청
exception 현재 instruction 실행 page fault, illegal instruction 동기적이지만 정상 복구 또는 종료가 갈린다
hardware interrupt timer·device timer tick, network completion 현재 instruction 흐름과 비동기적으로 도착한다

모두 kernel code로 전환될 수 있지만 원인과 복귀 조건은 다르다. 특히 page fault는 언제나 오류가 아니다. demand paging처럼 OS가 page를 준비한 뒤 같은 instruction을 다시 실행하는 정상 경로일 수도 있다.

System call은 함수 호출보다 경계가 두껍다

application이 read() 같은 library function을 호출하면 wrapper가 architecture에 맞는 system call instruction과 번호·인자를 준비한다. CPU는 미리 정해진 kernel entry point로 전환하고 privilege level을 바꾼다.

user code
  → libc wrapper
  → system call instruction
  → kernel entry code
  → syscall handler
  → return-to-user sequence
  → user code

“하드웨어가 모든 register를 PCB에 자동 저장한다”라고 외우면 정확하지 않다. CPU가 자동으로 보존하는 state는 architecture가 정하고, kernel entry code가 나머지 register와 실행 문맥을 추가로 저장한다. trap table, interrupt vector, IDT 같은 이름도 architecture마다 다르지만 핵심은 user가 임의의 kernel 주소로 뛰지 못하고 승인된 entry를 통과한다는 점이다.

Timer interrupt가 선점을 가능하게 한다

협력형 scheduling에서는 process가 system call을 하거나 exception을 일으켜야 OS가 CPU를 돌려받는다. process가 계산만 계속하면 이 방식만으로는 선점할 수 없다.

timer interrupt는 일정한 시간 기반 사건으로 kernel에 제어권을 돌려준다. kernel은 현재 실행 상태를 확인하고 scheduler가 필요하다고 판단하면 다른 runnable process를 고른다. timer interrupt가 발생할 때마다 반드시 process가 바뀌는 것은 아니다.

process A 실행
  → timer interrupt
  → kernel 진입
  → scheduler 판단
      ├─ A 계속 실행
      └─ A 저장 → B 복원 → B 실행

Mode switch와 context switch는 다르다

system call로 kernel mode에 들어갔다가 같은 process로 돌아오면 mode switch는 있었지만 process context switch는 없을 수 있다.

다른 process로 전환할 때는 scheduler와 architecture code가 실행에 필요한 state를 저장·복원한다. 일반적으로 register, stack pointer, program counter, scheduling state, address-space 관련 정보가 포함된다. 다만 TLB를 언제나 전부 비운다고 단정할 수는 없다. 현대 CPU와 OS는 ASID·PCID 같은 식별자를 활용해 일부 translation을 유지할 수 있다.

context switch의 비용도 “register 몇 개를 복사하는 시간”만이 아니다. 바뀐 workload가 cache와 TLB의 locality를 잃으면서 뒤늦게 나타나는 간접 비용이 클 수 있다.

운영에서 볼 때의 관찰 순서

이 장을 DevOps 관점으로 가져올 때 곧바로 scheduler timeslice를 조정하는 것은 좋은 출발이 아니다. 먼저 증상과 workload를 확인한다.

## process별 voluntary / involuntary context switch 추이
pidstat -w -p <pid> 1

## workload 실행 동안 주요 사건 계수
perf stat -e context-switches,cpu-migrations,minor-faults,major-faults \
  -- <command>
  • voluntary switch가 많으면 I/O wait, lock, sleep처럼 스스로 CPU를 놓는 경로를 본다.
  • involuntary switch가 많으면 runnable task 수, CPU quota, 우선순위와 scheduling pressure를 함께 본다.
  • run queue가 긴데 CPU가 포화라면 process·thread 수와 CPU limit을 확인한다.
  • system call 수가 많다는 이유만으로 batching이 정답은 아니다. latency와 buffering trade-off를 측정한다.
  • container는 process isolation을 구성하지만 kernel을 공유한다. user/kernel privilege 경계와 container boundary를 같은 것으로 보지 않는다.

수치 하나만으로 context switch가 문제라고 단정하지 않는다. 기준선, latency, CPU 사용률, run queue, application profile을 함께 봐야 한다.

다시 확인할 질문

  1. 현재 kernel 진입은 system call, exception, interrupt 중 무엇 때문인가?
  2. kernel에 들어간 뒤 같은 process로 돌아왔는가, 다른 process로 바뀌었는가?
  3. 직접 비용과 cache·TLB locality 손실을 구분했는가?
  4. scheduler tuning 전에 runnable task와 blocking 원인을 측정했는가?

LDE는 “process가 CPU를 직접 쓴다”와 “OS가 시스템을 통제한다”가 모순처럼 보이는 지점을 풀어 준다. 성능은 direct execution에서 얻고, 보호와 scheduling은 제한된 kernel entry에서 확보한다.

참고 자료

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

댓글