CPU 가상화를 위해서는 여러 개의 작업이 동시에 실행되는 것처럼 묶여있는 작업을 어떻게 공유할지 결정해야 한다. 이를 위해 CPU를 시간 분할하여 한 작업을 잠시 실행한 다음 다른 작업을 실행하고 이를 반복하여 CPU를 공유한다. 그러면 가상화가 이루어진다.
하지만, 이러한 가상화 기능을 구현하는 데는 몇 가지 문제점이 있다. 첫 번째는 성능 문제이다. 즉, 시스템에 과도한 부하를 추가하지 않으면서 가상화를 구현하는 방법은 무엇일까? 두 번째는 제어 문제이다. 즉, CPU를 효율적으로 실행하면서도 OS가 제어를 유지할 수 있는 방법은 무엇일까? 특히, OS는 리소스를 관리하기 때문에 제어는 매우 중요하다. 제어를 잃으면 작업이 무한히 실행되어 시스템을 점유하거나 액세스해서는 안 되는 정보에 액세스할 수도 있다. 따라서 고성능을 유지하면서 제어를 유지하는 것은 운영 체제를 구축하는 중요한 과제 중 하나이다.
1. Basic Technique: Limited Direct Execution
"제한된 직접 실행"은 OS 개발자들이 프로그램을 기대한 대로 빠르게 실행하기 위해 고안한 기술이다. "직접 실행"이란 아이디어는 간단하다. CPU에서 프로그램을 직접 실행하는 것이다. 따라서, OS가 프로그램을 실행하려고 할 때, 프로세스 목록에 프로세스 항목을 만들고, 프로그램 코드를 디스크에서 메모리로 로드하고, entry point(즉, main() 함수나 비슷한 것)를 찾아서 해당 지점으로 이동시키고, 사용자의 코드를 실행시킨다. 그림 6.1은 이러한 기본 직접 실행 프로토콜(아직은 어떠한 제한도 없는 상태)을 나타낸다. 프로그램의 main() 함수로 점프하고 나중에 커널로 다시 돌아가기 위해 일반적인 호출과 반환을 사용한다.
하지만 이러한 접근 방식은 CPU를 가상화하기 위한 우리의 노력에서 몇 가지 문제가 발생한다. 첫 번째는 간단하다. 우리가 프로그램을 실행하면서 효율적으로 실행되는 것을 보장하면서도 우리가 원하지 않는 동작을 수행하지 않도록 어떻게 OS가 제어할 수 있는가? 두 번째는 프로세스를 실행하는 동안, 우리가 다른 프로세스로 전환해서 CPU 가상화를 구현하는 데 필요한 시간 공유를 어떻게 할 수 있는가?
이러한 질문에 대답함으로써, 우리는 CPU를 가상화하기 위해 필요한 것에 대해 더욱 잘 이해할 수 있다. 이러한 기술을 개발하면서 "제한된"이라는 이름이 생겨났는데, 프로그램 실행에 대한 제한이 없으면 OS는 아무것도 제어하지 못하므로 "그저 라이브러리"가 된다는 안타까운 결과를 가져온다.
2. Problem #1: Restricted Operations
직접 실행은 빠른 실행 속도라는 분명한 장점을 가지고 있다. 프로그램은 하드웨어 CPU에서 네이티브로 실행되어 빠르게 동작한다. 그러나 CPU에서 실행되는 프로세스가 디스크에 대한 I/O 요청과 같은 제한된 작업을 수행하려면 문제가 발생할 수 있다. 또한, 시스템 자원인 CPU나 메모리에 더 많은 접근을 허용해야 하는 경우도 있다.
이러한 문제를 해결하기 위해 사용자 모드라는 새로운 프로세서 모드를 도입한다. 사용자 모드에서 실행되는 코드는 수행할 수 있는 작업이 제한된다. 예를 들어 사용자 모드에서 실행되는 프로세스는 I/O 요청을 할 수 없으며, 이를 시도하면 프로세서에서 예외가 발생하고 OS에서 프로세스를 종료할 수 있다.
반면, 커널 모드는 운영 체제(또는 커널)에서 실행되며 이 모드에서 실행되는 코드는 모든 종류의 제한된 명령을 포함하여 원하는 작업을 수행할 수 있다.
그러나 여전히 사용자 프로세스가 디스크 읽기와 같은 특권 작업을 수행하려면 어떻게 해야 할까? 이를 가능하게 하기 위해 대부분의 현대 하드웨어는 사용자 프로그램이 시스템 호출을 수행할 수 있도록 한다. 시스템 호출은 파일 시스템에 액세스하거나 프로세스를 생성 및 제거하고 다른 프로세스와 통신하며 메모리를 할당하는 등의 기능을 운영 체제에게 부여한다.
시스템 호출을 실행하려면 프로그램은 특수한 트랩 명령을 실행해야 한다. 이 명령은 커널로 이동하고 동시에 특권 수준을 커널 모드로 올리는 작업을 수행한다. 커널에서는 필요한 작업을 수행한 후 특수한 리턴-프롬-트랩 명령을 호출하여 사용자 프로그램으로 돌아간다.
하드웨어는 트랩을 실행할 때 주의해야 할 점이 있다. 트랩을 실행할 때 하드웨어는 충분한 레지스터를 저장하여 OS가 리턴-프롬-트랩 명령을 실행할 때 올바르게 되돌릴 수 있도록 해야 한다.
운영체제는 부팅 시 트랩 테이블을 설정하여 이 문제를 해결한다. 기계가 부팅되면 특권(커널) 모드에서 실행되어 하드웨어를 필요한 대로 구성할 수 있다. 따라서 OS는 첫 번째로 특정 예외 이벤트가 발생했을 때 실행할 코드를 하드웨어에 알려야 한다. 예를 들어 하드 디스크 인터럽트가 발생하거나 키보드 인터럽트가 발생하거나 프로그램이 시스템 호출을 할 때 어떤 코드를 실행해야 할까? OS는 트랩 핸들러의 위치를 하드웨어에 알리고, 하드웨어는 다음 기계 재부팅 시까지 핸들러의 위치를 기억한다. 따라서 하드웨어는 시스템 호출 및 기타 예외 이벤트가 발생할 때 어떤 작업을 수행해야 하는지 알 수 있다.
시스템 호출을 정확히 지정하기 위해 각 시스템 호출에 시스템 호출 번호가 할당된다. 사용자 코드는 원하는 시스템 호출 번호를 레지스터에 배치하거나 스택의 지정된 위치에 두는 책임이 있다. OS는 트랩 핸들러 내에서 이 번호를 검사하여 유효한지 확인하고, 유효한 경우 해당 코드를 실행한다. 이 간접적인 수준은 보호 형태로 작용한다. 사용자 코드는 정확한 주소를 지정할 수 없으며 시스템 호출 번호를 통해 특정 서비스를 요청해야 한다.
한 가지 중요한 세부사항이 누락되었다: 트랩이 OS 내에서 어떤 코드를 실행할지 어떻게 알 수 있을까? 호출 프로세스가 점프할 주소를 지정할 수 없기 때문에(이렇게 하면 커널로 점프하는 것이 분명히 매우 나쁜 아이디어가 된다) 커널은 트랩이 발생할 때 실행될 코드를 주의 깊게 제어해야 한다.
제한된 직접 실행(LDE) 프로토콜에는 두 단계가 있다. 첫 번째 단계(부팅 시)에서 커널은 트랩 테이블을 초기화하고 CPU는 이후 사용을 위해 위치를 기억한다. 커널은 특권 명령(모든 특권 명령은 굵게 표시 된다)를 사용하여 이 작업을 수행한다. 두 번째 단계(프로세스 실행 시)에서 커널은 프로세스를 시작하기 전 몇 가지 작업을 설정한다(예: 프로세스 목록에 노드 할당, 메모리 할당). 그런 다음 리턴-프롬-트랩 명령을 사용하여 프로세스의 실행을 시작한다. 이렇게 하면 CPU가 사용자 모드로 전환되고 프로세스가 실행된다.
프로세스가 시스템 호출을 요청하려면 OS로 트랩을 다시 걸어야 한다. 그러면 OS가 이를 처리하고 리턴-프롬-트랩을 통해 다시 프로세스에 제어권을 돌려준다. 프로세스는 작업을 완료한 후 메인 함수에서 반환하고, 이는 일반적으로 프로그램을 올바르게 종료하는 데 필요한 스텁 코드로 반환된다(예: exit() 시스템 호출을 호출하여 프로그램을 종료한다). 이 시점에서 OS가 정리 작업을 수행하고 종료된다.
결과적으로, 직접 실행에 대한 제한은 프로세스가 커널 내에서 실행되는 코드를 제어하고, 시스템 호출을 통해 필요한 작업을 수행하는 데 필요한 수단을 제공한다. 이 프로토콜은 시스템의 안전성과 보호를 보장하는 데 도움이 되며, 사용자 코드가 특권 명령을 실행하려고 시도할 때 하드웨어가 올바르게 처리할 수 있도록 한다. 이와 같은 구조 덕분에 다양한 시스템이 원활하게 작동하며 사용자 프로그램과 운영 체제 간에 적절한 권한 관리가 가능하다.
3. Problem #2: Switching Between Processes
직접 실행하는 방법의 다음 문제는 프로세스 간 전환이다. 우리는 OS가 하나의 프로세스를 중단하고 다른 프로세스를 시작하기만 하면 된다고 생각할 수 있다. 그러나 이것은 조금 까다로운 문제이다. 특히, 프로세스가 CPU에서 실행 중인 경우, 이는 OS가 실행 중이지 않다는 것을 의미한다. OS가 실행 중이지 않으면 어떤 일도 할 수 없다는 것이다. 이는 철학적인 이야기처럼 들릴 수 있지만, 실제로는 문제이다. CPU를 제어하기 위해서는 OS가 실행 중이어야 하기 때문이다. 따라서 우리는 문제의 핵심에 도달했다.
일부 시스템에서 사용하는 접근 방식은 협력적인 방식이다. 이 방식은 예전 맥킨토시 운영체제나 오래된 제록스 알토 시스템에서 사용되었다. 이 방식에서 OS는 시스템의 프로세스가 합리적으로 행동할 것을 믿는다. 너무 오래 실행되는 프로세스는 주기적으로 CPU를 포기해서 OS가 다른 작업을 실행할 수 있도록 하는 것으로 가정한다.
그렇다면 우호적인 프로세스는 어떻게 CPU를 포기할까? 대부분의 프로세스는 시스템 콜을 수행함으로써 CPU의 제어를 OS에게 전달한다. 예를 들어 파일을 열고 읽거나 다른 기계에 메시지를 보내거나 새로운 프로세스를 만들기 위해 시스템 콜을 사용한다. 이러한 시스템에서는 종종 명시적으로 CPU 제어를 OS에게 전달하는 yield 시스템 콜이 포함된다.
어플리케이션도 시스템 콜을 수행하거나 잘못된 작업을 수행할 때 OS에게 CPU 제어를 전달한다. 예를 들어 어플리케이션이 0으로 나누거나 액세스할 수 없는 메모리에 액세스하려고 시도하면, OS는 트랩을 생성하여 CPU 제어를 다시 획득하게 된다(그리고 아마도 문제가 있는 프로세스를 종료한다).
따라서 협력적인 스케줄링 시스템에서는 OS는 시스템 콜이나 어떤 종류의 잘못된 작업이 일어날 때까지 CPU 제어를 회수한다.