이번 글은 OSTEP 6장 Direct Execution을 공부하며 정리한 내용입니다. 이전 장에서 fork(), exec(), wait() 같은 프로세스 API를 봤다면, 이번 장에서는 운영체제가 CPU를 어떻게 빠르면서도 안전하게 가상화하는지 살펴봅니다.
한 문장 요약
제한적 직접 실행은 사용자 프로그램을 CPU에서 직접 실행해 성능을 얻되, 사용자 모드, 커널 모드, 트랩, 타이머 인터럽트, 문맥 교환을 통해 운영체제가 제어권을 잃지 않게 하는 CPU 가상화 메커니즘입니다.
핵심 질문
| 질문 | 핵심 답변 |
|---|---|
| 제어를 유지하면서 효과적으로 CPU를 가상화하려면 어떻게 해야 하는가? | 프로그램은 CPU에서 직접 실행시키되, 하드웨어가 제공하는 사용자/커널 모드와 트랩 메커니즘으로 위험한 연산을 제한합니다. 운영체제는 시스템 콜과 인터럽트가 발생할 때만 개입해 성능과 제어를 함께 얻습니다. |
| 제한된 연산은 어떻게 수행하는가? | 사용자 모드의 프로세스는 디스크 I/O, 메모리 관리 같은 특권 연산을 직접 수행할 수 없습니다. 대신 시스템 콜을 호출하면 하드웨어가 trap으로 커널 모드에 진입시키고, 운영체제가 요청을 검사하고 처리한 뒤 return-from-trap으로 사용자 모드에 돌려보냅니다. |
| 운영체제는 실행 중인 프로세스로부터 CPU를 어떻게 다시 얻는가? | 협조적 방식은 프로세스가 시스템 콜이나 yield로 제어권을 넘겨주기를 기대합니다. 현대적인 비협조적 방식은 타이머 인터럽트를 사용해 주기적으로 운영체제에 제어권을 강제로 되돌립니다. |
| 프로세스 간 전환은 어떻게 일어나는가? | 타이머 인터럽트나 트랩으로 커널에 진입한 뒤, 스케줄러가 다른 프로세스를 선택하면 운영체제는 현재 프로세스의 레지스터 문맥을 저장하고 다음 프로세스의 문맥을 복원합니다. |
| 하드웨어는 무엇을 제공해야 하는가? | 사용자/커널 모드, 트랩과 리턴 명령, 트랩 테이블, 커널 스택, 타이머 인터럽트, 인터럽트 시 레지스터 저장/복원 지원이 필요합니다. |
| 왜 병행성 문제가 생기는가? | 커널이 시스템 콜을 처리하는 중에도 인터럽트가 발생할 수 있고, 인터럽트 처리 중 다른 인터럽트가 들어올 수도 있습니다. 운영체제는 인터럽트 비활성화나 락 같은 기법으로 커널 내부 자료 구조를 보호해야 합니다. |
제한적 직접 실행
직접 실행만 사용하면 빠르지만 위험합니다. 프로세스가 디스크를 마음대로 읽거나 CPU를 무한히 점유할 수 있기 때문입니다. 그래서 운영체제는 다음 두 요구를 동시에 만족해야 합니다.
| 요구 | 의미 |
|---|---|
| 성능 | 사용자 프로그램은 대부분의 시간 동안 CPU에서 직접 실행되어야 합니다. |
| 제어 | 특권 연산, 예외, 타이머 인터럽트 시에는 운영체제가 반드시 CPU 제어권을 회수해야 합니다. |
제한적 직접 실행은 이 절충점입니다. 평상시에는 직접 실행하고, 위험하거나 통제가 필요한 순간에만 커널로 진입합니다.
직접 실행만 사용할 때의 문제
가장 단순한 직접 실행 모델은 다음과 같습니다.
- 운영체제가 사용자 프로그램의
main으로 분기합니다. - 사용자 프로그램이 CPU에서 직접 실행됩니다.
- 프로그램이 정상적으로 반환하면 운영체제로 돌아옵니다.
이 모델은 단순하지만 다음 문제가 있습니다.
- 프로세스가 특권 연산을 마음대로 수행할 수 있습니다.
- 프로세스가 무한 루프에 빠지면 운영체제가 CPU를 되찾을 방법이 없습니다.
- 여러 프로세스 사이에서 언제 전환할지 운영체제가 결정할 수 없습니다.
사용자 모드와 커널 모드
| 실행 모드 | 실행 주체 | 권한 |
|---|---|---|
| 사용자 모드 | 일반 응용 프로그램 | 제한된 명령만 실행 가능 |
| 커널 모드 | 운영체제 커널 | 하드웨어와 시스템 자원 전체에 접근 가능 |
사용자 모드에서 특권 명령을 직접 실행하려 하면 예외가 발생하고 운영체제가 개입합니다. 일반적으로 운영체제는 잘못된 프로세스를 종료합니다.
시스템 콜과 트랩
시스템 콜은 일반 함수 호출처럼 보이지만 내부에는 커널로 진입하는 trap 명령이 숨어 있습니다.
- 사용자 프로그램이
read(),open()같은 라이브러리 함수를 호출합니다. - C 라이브러리가 시스템 콜 번호와 인자를 정해진 위치에 배치합니다.
trap명령을 실행합니다.- 하드웨어가 사용자 레지스터를 저장하고 커널 모드로 전환합니다.
- 하드웨어는 트랩 테이블에 등록된 커널 핸들러로 분기합니다.
- 운영체제가 권한을 검사하고 요청을 처리합니다.
return-from-trap으로 사용자 모드에 복귀합니다.
중요한 점은 사용자 프로그램이 커널의 임의 주소로 직접 점프하지 않는다는 것입니다. 부팅 시 운영체제가 트랩 테이블을 설정해두고, 하드웨어는 등록된 핸들러로만 제어를 넘깁니다.
트랩 테이블
트랩 테이블은 사건별 커널 진입점을 담은 하드웨어/운영체제의 약속입니다.
| 사건 | 대표 핸들러 |
|---|---|
| 시스템 콜 | 시스템 콜 핸들러 |
| 타이머 인터럽트 | 스케줄러 진입 핸들러 |
| 페이지 폴트 | 메모리 예외 핸들러 |
| 잘못된 명령 | 예외 처리 핸들러 |
| 디스크/키보드 인터럽트 | 장치 인터럽트 핸들러 |
트랩 테이블 설정은 커널 모드에서만 가능한 특권 작업입니다. 사용자 프로세스가 이를 바꿀 수 있다면 커널 제어 흐름을 탈취할 수 있기 때문입니다.
제한적 직접 실행 프로토콜
제한적 직접 실행은 대체로 다음 흐름으로 동작합니다.

- 부팅 시 커널 모드에서 트랩 테이블을 초기화합니다.
- 타이머 인터럽트를 시작합니다.
- 프로세스를 생성하고 주소 공간을 준비합니다.
- 커널 스택에 초기 레지스터와 프로그램 카운터를 준비합니다.
return-from-trap을 실행해 사용자 모드로 이동합니다.- 사용자 모드에서 프로세스가 직접 실행됩니다.
- 시스템 콜, 타이머 인터럽트, 예외 같은 사건이 발생하면 커널 모드로 진입합니다.
- 운영체제는 요청을 처리하거나 스케줄러를 실행합니다.
- 필요하면 문맥 교환을 수행하고, 다시
return-from-trap으로 사용자 모드에 복귀합니다.
이 흐름에서 운영체제는 항상 직접 실행과 제어 회수 사이를 오갑니다.
협조적 방식과 비협조적 방식
| 방식 | 제어 회수 방법 | 장점 | 문제 |
|---|---|---|---|
| 협조적 방식 | 프로세스가 시스템 콜이나 yield를 호출 |
단순함 | 프로세스가 무한 루프에 빠지면 제어권을 회수하지 못함 |
| 비협조적 방식 | 타이머 인터럽트가 주기적으로 발생 | 악의적이거나 버그가 있는 프로세스도 선점 가능 | 인터럽트와 문맥 교환 처리 비용이 있음 |
현대 운영체제는 비협조적 방식을 기본으로 사용합니다. 타이머 인터럽트가 없다면 운영체제는 실행 중인 사용자 프로세스가 스스로 멈추기를 기다릴 수밖에 없습니다.
문맥 교환
문맥 교환은 현재 프로세스의 실행 상태를 저장하고 다음 프로세스의 상태를 복원하는 작업입니다.
- Process A가 사용자 모드에서 실행 중입니다.
- 타이머 인터럽트가 발생하고 하드웨어가 A의 사용자 레지스터를 A의 커널 스택에 저장합니다.
- 운영체제의 스케줄러가 Process B를 선택합니다.
- 운영체제는 A의 커널 문맥을 A의 PCB에 저장합니다.
- 운영체제는 B의 PCB에서 커널 문맥을 복원합니다.
return-from-trap을 실행합니다.- CPU는 B의 사용자 문맥으로 복귀합니다.
여기서 두 종류의 저장/복원을 구분해야 합니다.
| 시점 | 저장 대상 | 저장 위치 | 주체 |
|---|---|---|---|
| 트랩/인터럽트 진입 | 사용자 레지스터 | 현재 프로세스의 커널 스택 | 하드웨어 |
| 문맥 교환 | 커널 실행 문맥 | 프로세스 구조체/PCB | 운영체제 |
문맥 교환 코드의 의미
책에는 xv6의 switch() 어셈블리 코드가 나옵니다. 핵심은 다음 의사 코드로 이해할 수 있습니다.
|
1 2 3 4 5 |
void context_switch(struct context *old, struct context *new) { save_current_registers(old); load_registers(new); return_to_restored_context(); } |
실제 구현은 C 함수처럼 단순하지 않습니다. 스택 포인터와 복귀 주소까지 바꿔야 하므로 저수준 어셈블리 코드가 필요합니다. 이 조작 덕분에 커널은 A의 인터럽트 처리 도중 B의 커널 문맥으로 갈아타고, 마지막에는 B의 사용자 코드로 복귀할 수 있습니다.
비용과 측정
시스템 콜과 문맥 교환은 모두 공짜가 아닙니다.
- 시스템 콜은 모드 전환, 레지스터 저장/복원, 커널 핸들러 실행 비용이 듭니다.
- 문맥 교환은 레지스터뿐 아니라 캐시, TLB, 분기 예측 상태에도 영향을 줍니다.
- 비용을 측정하려면 같은 작업을 많이 반복하고 평균을 내야 합니다.
실행 예제:
code/ostep/direct-execution/syscall_cost.c:getpid()반복으로 시스템 콜 평균 비용 측정code/ostep/direct-execution/context_switch_pipe.c: 두 프로세스가 파이프로 왕복하며 문맥 교환 비용을 대략 측정
정리
제한적 직접 실행의 핵심은 대부분의 시간에는 직접 실행하고, 필요한 순간에는 반드시 운영체제가 제어권을 회수하는 것입니다. 사용자/커널 모드와 트랩은 제한된 연산을 안전하게 처리하고, 타이머 인터럽트와 문맥 교환은 운영체제가 비협조적인 프로세스도 선점할 수 있게 합니다.
이 장은 CPU 가상화의 저수준 메커니즘을 다룹니다. 다음 장의 CPU Scheduling은 이 메커니즘 위에서 어떤 프로세스를 실행할지 결정하는 정책을 다룹니다.