Intro


CPU 가상화를 위해서, 운영체제는 physical CPU를 여러 작업 간에 공유하여 마치 동시에 실행되는 것처럼 만들어야 한다. 기본 아이디어는 간단하다. 하나의 프로세스를 잠시 실행했다가 다른 프로세스를 실행하는 방식으로 진행한다. 이와 같은 방식으로 CPU 공유하면 가상화가 이루어지며 이를 time sharing of CPU라고 한다.

 

그러나 이러한 가상화 머신을 구축하는데는 가지 과제가 있다.

번째는 성능(performance)이다. 시스템에 과도한 오버헤드를 부가하지 않고 어떻게 가상화를 구현할 수 있을까?

두 번째는 제어(control)다. 운영체제가 CPU에 대한 제어를 유지하면서 프로세스를 효율적으로 실행하는 방법은 무엇이 있을까?

 

 

 

 

Basic Technique: Limited Direct Execution


프로그램을 기대하는 만큼 빠르게 실행하기 위해서 OS 개발자는 Limited Direct Execution을 고안해냈다.

Direct Execution은 프로그램을 CPU에서 직접 실행한다는 것이다. 그러므로, 운영체제가 프로그램을 실행하고자 할 때, Process list에 Process Entry를 생성하고, 메모리를 할당하고, 프로그램 코드를 디스크에서 메모리로 로드하고, 진입점(entry point)을 찾아서 jump하여 사용자의 코드를 실행하기 시작한다. 보다 정확한 direct execution protocol은 아래와 같다.

 

출처 : https://pages.cs.wisc.edu/~remzi/OSTEP/

 

Direct Execution 만으로 해결할 수 없는 문제들이 있다.

 

첫 번째로, 프로그램을 실행하기만 한다면 OS가 어떻게 프로그램을 효율적으로 실행하면서 동시에 원하지 않는 작업을 수행하지 않도록 할 수 있을까?

두 번째로, 프로그램을 실행할 때 OS는 어떻게 프로세스를 중지하고 다른 프로세스로 전환하는 time sharing(CPU 가상화)을 구현할 수 있을까?

 

해당 질문들을 풀어나가는 과정에서, 우리는 CPU 가상화를 위한 필요 사항들을 더 잘 이해할 수 있다.

OS는 이러한 문제들을 해결하기 위해 프로그램 실행(direct execution)을 통제 및 제한(limited)한다.

 

 

 

 

Problem #1 : Restricted Operations


direct execution 은 분명 빠르다는 장점이 있지만 만약 프로그램이 CPU에서 실행 도중에 disk I/O을 요청하거나 CPU나 Memory의 시스템 리소스에 대한 추가적인 액세스를 요청하는 등 제한된 작업(restricted operations)에 대해서 어떻게 처리해야할까?

 

한 가지 접근법은 제한된 작업들(restricted operations)에 대해서 프로세스가 원하는 것을 모두 허용하는 것이다. 물론 이는 바람직하지 못하다. 예를 들어 I/O(입출력)의 경우, 프로세스가 전체 디스크를 읽거나 쓸 수 있다면 모든 보호(protection) 기능이 손실된다.

 

따라서, 일반적으로 취하는 접근법은 user mode(사용자 모드)라고 불리는 새로운 프로세서 모드를 도입하는 것이다. 사용자 모드에서 실행하는 코드는 할 수 있는 것이 제한적이다. 예를 들어, 사용자 모드에서 실행할 때 프로세스는 I/O request를 할 수 없으며 요청 시 프로세스가 exception을 던지고 운영체제는 프로세스를 종료(kill)할 것이다.

 

사용자 모드와 달리, 운영체제가 실행되는 kernel mode(커널 모드)에서 실행하는 코드는 I/O 요청 및 restricted operations을 포함하여 권한이 필요한 모든 작업을 수행할 수 있다.

 

여전히 문제가 남아있다. 만약 유저 프로세스가 디스크 입출력과 같은 권한이 필요한 작업을 수행하고자 한다면 어떻게 할까? 이를 위해 현대의 거의 모든 최신 하드웨어는 유저 프로그램이 시스템 호출(system call)을 수행할 수 있는 기능을 제공한다. 시스템 호출은 커널이 파일 시스템(file system)에 액세스하고, 프로세스를 생성 및 파괴하고, 다른 프로세스와 통신하고, 더 많은 메모리를 할당하는 것과 같은 특정한 핵심 기능들을 사용자 프로그램(user program)에 노출한다.

 

system call을 수행하기 위해 프로그램은 special trap instruction을 실행한다. 해당 명령과 동시에 커널로 jump하고 privilege level(권한 수준)을 kernel mode로 올린다. system call의 목적이었던 권한이 필요한 작업을 수행하고 완료되면 OS에서 return-from-trap 명령을 호출하며 호출된 사용자 프로그램을 다시 user mode로 privvilege level을 낮춘다. 

 

trap 명령을 실행할 때,  확실하게 호출자의 레지스터들을 저장해야 한다. OS가 return-from-trap 명령을 실행 후 호출자의 레지스터들(caller's registers)이 올바르게 return(반환)되게 하기 위해서다. 예를 들어 x86 프로세서는 Program Counter, flags, 그리고 다른 레지스터들을 프로세스당 커널 스택에 push한다. return-from-trap 명령은 그 스택으로부터 values을 pop하여 사용자 모드 프로그램 실행을 재개한다.

 

운영체제 내부(커널 모드)에서 실행할 코드를 트랩이 알게 하기 위해서 커널은 시스템 부팅 시 트랩 테이블(trap table)을 구성한다. 시스템은 커널 모드로 부팅되므로 필요에 따라 시스템 하드웨어를 자유롭게 구성할 수 있다. OS가 가장 먼저 수행하는 작업 중 하나가 특정 예외 이벤트 발생 시 실행할 코드를 하드웨어에 알려주는 것이다. 운영체제는 이러한 트랩 핸들러(특수 명령)의 위치를 하드웨어에 알려준다. 하드웨어에 정보가 입력되면 이러한 핸들러의 위치는 다음 재부팅 전까지 기억하므로 시스템 호출 및 기타 예외 이벤트 발생 시 무엇을 해야 하는 지 알고 있다.

 

시스템 호출(system call)을 특정하기 위해 각 시스템 호출마다 번호를 할당한다. OS는 트랩 핸들러 내부에서 시스템 호출을 처리할 때 이 번호를 검사하고 유효한지 확인하고, 만약 그렇다면 해당 코드를 실행한다. 이러한 indirection(트랩 핸들러를 거쳐서 간접 실행)은 일종의 보호(protection) 기능을 한다. 사용자 코드(user code)는 jump할 exact address를 specify할 수 없다. 대신에 번호를 통해 특정 서비스를 요청해야 한다. 하드웨어에 trap table의 위치를 지정하는 명령의 실행도 강력한 기능이자 특권화된 작업이므로 사용자 모드에서 허용하지 않는다.

 

요약해보면 LDE(Limited Direct Execution) Protocol의 두 단계는 아래와 같다.

 

1 단계 - (부팅 시) 커널은 트랩 테이블을 초기화하며 CPU는 이후 사용하기 위해 위치를 기억한다. 이러한 과정은 커널의 privileged instruction을 통해 이루어진다.

 

2 단계 - (프로세스 실행 시) 커널은 프로세스를 실행하기 전 몇 가지(이전 내용 참고) 사항을 설정하고, CPU를 사용자 모드로 전환하여 프로세스를 실행하기 시작한다. 프로세스가 시스템 호출을 하면 운영체제는 trap에 걸려들어 이를 처리하고 return-from-trap을 통해 프로세스 제어 권환을 반환한다. 그 후 프로세스가 작업을 완료하면 main()에서 return(반환)된다. 이를 통해 프로그램이 정상적으로 종료(exit)되고운영체제가 정리한 후(clean up) 최종적으로 완료된다.

 

 

 

Problem #2: Switching Between Processes


Direct Execution에 있어서 다음 문제는 어떻게 OS가 CPU에 대한 제어권을 가지고 switch between processes를 수행할 수 있는 가이다. Switching Process는 겉보기에는 그저 OS가 실행 중인 프로세스를 멈추고 다음 프로세스를 실행하면 되는 간단한 문제처럼 보이지만 실제로는 그렇게 단순하지 않다. 왜냐하면, 프로세스를 수행하는 주체는 CPU이지 OS가 아니기 때문이다. 따라서 OS는 switch between process(프로세스 간 전환)을 수행하기 위해 CPU에 대한 제어권을 얻어야(regain control) 한다.

 

A Cooperative Approach: Wait For System Calls

과거에 일부 시스템에서 취했던 한 가지 접근 방식은 협력 접근 방식이다. 이와 같은 방식에서 운영체제는 시스템의 프로세스들이 합리적으로 동작할 것이라고 신뢰한다. 너무 오래 실행되는 프로세스는 주기적으로 CPU를 포기하며 OS가 다른 작업을 수행할 수 있도록 한다고 가정한다. 따라서 프로세스는 종종 시스템 호출을 통해서 CPU의 제어 권한을 OS에 전송한다. 예를 들어, 파일을 열고 읽거나, 다른 컴퓨터에 메시지를 보내거나 새로운 프로세스를 만들 때, 허용되지 않는 불법적인 작업을 할 때 OS가 CPU에 대한 제어 권한을 얻게 되며 이러한 사안에 관해 처리할 수 있게 된다.

 

따라서, 협력적 스케줄링 시스템에선 OS가 시스템 호출이나  불법적인 작동(illegal operation) 발생 시 CPU의 제어권을 얻는 수동적인 방식이다. 만약, 프로세스에 문제가 생겨 무한 루프(infinite loop) 상태에 빠지거나, 시스템 호출을 하지 않는다면 운영체제는 할 수 있는 것이 없다는 문제가 발생한다.

 

 

A Non-Cooperative Approach: The OS Takes Control

위와 같은 방식은 프로세스가 시스템 호출을 하지 않고 CPU에 대한 제어권을 운영체제에 전달하기를 거부할 때 추가적인 하드웨어의 도움 없이는 운영체제가 할 수 있는 것이 별로 없다. 프로세스가 무한 루프에 빠졌을 때 우리가 할 수 있는 유일한 방법은 재부팅이다. 따라서, 협력적 접근 방식은 우리가 해결하고자 하는 문제를 명확히 해결하긴 힘들다.

 

이를 위한 다른 해답은 timer interrupt 이다. 타이머 장치는 milliseconds 단위로 interrupt를 발생시킨다. 인터럽트가 발생하면 현재 실행 중인 프로세스가 중지되고 OS에서 미리 구성한 인터럽트 핸들러가 실행된다. 해당 시점에서 OS는 CPU를 제어할 수 있고 현재 프로세스를 중지하고 다른 프로세스를 실행할 수 있다.

 

앞서 시스템 호출에서 논의했듯이, OS는 타이머 인터럽트가 발생할 때 실행할 코드를 하드웨어에 미리 알려주어야 한다. 따라서 부팅시 OS는 하드웨어에 알려준다. 다음으로 부팅 작업 중에도 OS는 해당 타이머 장치를 실행하며 타이머가 시작되면 OS는 제어에 대한 안전성을 확보하며 자유롭게 사용자 프로그램을 실행할 수 있다. 타이머를 끌 수도 있으며 이는 동시성(concurrency)에 대한 이해와 함께 추후에 자세히 논의한다. 하드웨어는 시스템 호출 트랩 시 동작과 유사하게 인터럽트가 발생했을 때도 실행 중인 프로그램의 상태(state)를 충분히 저장하고 return-from-trap 명령이 실행될 때 실행중이었던 프로그램을 올바르게 재개할 수 있도록 해야하는 책임이 있다.

 

 

 

Saving and Restoring Context


이제 OS가 시스템 호출 또는 타이머 인터럽트를 통해서 제어권을 되찾았으므로 프로세스를 계속 실행할 지 아니면 다르 프로세스로 전환할지 결정해야 한다. 해당 결정은 OS의 일부분인 스케줄러에 의해 내려진다. 이에 대한 자세한 내용은 추후에 논의한다.

 

전환하도록 결정이 내려지면 OS는 Context Switch라고 하는 low-level code를 실행한다. Context switch 시  OS는 현재 실행중인 프로세스의 몇몇 레지스터 값들을 저장하고 전환할 프로세스의 몇몇 레지스터 값들을 복원한다. 따라서 OS는 실행중이던 프로세스로 돌아가는 대신 return-from-trap 명령이 실행될 때 시스템이 다른 프로세스를 실행하도록 보장한다.

 

현재 실행중인 프로세스의 컨텍스트(context)를 저장하기 위해서 OS는 현재 실행 중인 프로세스의 범용 레지스터, PC 및 커널 스택 포인터를 저장한 다음 실행할 프로세스의 레지스터, PC를 복원하고 실행할 프로세스의 커널 스택으로 전환한다. 스택을 전환함으로써 커널은 인터럽트된 실행중이었던 프로세스의 컨텍스트에서 switch 코드에 대한 호출을 입력하고 곧 실행 될 프로세스의 컨텍스트에서 반환한다. 그런 다음 OS가 최종적으로 Return-from-trap 명령을 실행할 때, 곧 실행될 프로세스가 현재 실행 중인 프로세스가 되며 컨텍스트 전환이 완료된다.

 

이 프로토콜 중 발생하는 레지스터 저장/복원에는 두 가지 유형이 있다.

 

첫 번째는 타이머 인터럽트가 발생할 때이다. 이 경우엔 실행중인 프로세스의 사용자 레지스터는 해당 프로세스의 커널 스택을 사용하여, 하드웨어에 의해 암시적으로(implicitly) 저장된다.

 

두 번째는 OS가 A에서 B로 Context Switch 할 때이다. 이 경우 커널 레지스터들은 소프트웨어(OS)에 의해 명시적으로(explicitly) 저장되는데, 이번에는 해당 프로세스의 프로세스 구조에서 메모리에 저장된다. 해당 동작은 시스템 커널이 A를 트랩한 상태에서 B를 트랩한 상태로 이동시킨다.

 

 

 

Worried About Concurrency?


그렇다면, 이런 의문이 생길 수도 있다. 

만약 시스템 호출중에 타이머 인터럽트가 발생한다면 어떻게 되는가?

하나의 인터럽트를 처리 중일 때 또 다른 인터럽트가 발생하면 어떻게 되는가?

 

실제로, OS는 인터럽트나 트랩 핸들링 도중에 또 다른 인터럽트가 발생할 때를 주의 깊게 고려할 필요가 있다. 해당 이슈에 대한 논의는 추후에 Concurrency 파트에서 하도록 한다.

 

 

 


 

 

출처 : pages.cs.wisc.edu/~remzi/OSTEP/ 

 

Operating Systems: Three Easy Pieces

Blog: Why Textbooks Should Be Free Quick: Free Book Chapters - Hardcover - Softcover (Lulu) - Softcover (Amazon) - Buy PDF - EU (Lulu) - Buy in India - Buy Stuff - Donate - For Teachers - Homework - Projects - News - Acknowledgements - Other Books Welcome

pages.cs.wisc.edu

 

+ Recent posts