" Mechanism: Limited Direct Execution "
운영체제가 CPU를 가상화하기 위해서는 물리적인 CPU를 여러 작업이 동시에 실행되는 것처럼 공유해야 합니다. 기본 아이디어는 CPU를 시간을 나눠가며 여러 프로세스에 할당하여 가상화하는 것입니다 (time sharing).
여기에는 두 개의 도전과제가 있습니다. 첫번째로, 가상화를 구현할 때 성능을 최적화해야 합니다. 시스템에 오버헤드를 최소화하는 효율적인 가상화 방법을 고려해야 합니다. 두번째로, CPU를 제어하면서 효율적으로 프로세스를 실행해야 합니다. 운영체제는 프로세스가 영원히 실행되거나 보안에 위협이 되지 않도록 효과적으로 제어해야 합니다.
이를 위해서는 하드웨어와 운영체제의 협력과 지원이 필요합니다.
운영체제 개발자들은 프로그램을 빠르게 실행하기 위해서 "limited direct execution" 기술을 개발했습니다.
이 아이디어의 핵심은 CPU에서 프로그램을 직접 실행하는 것입니다.
direct execution은 OS의 관여와 제한 없이 프로그램을 실행합니다. 위의 그림(6.1) 을 보면 프로세스가 한 번 실행하면 종료될 때까지 프로세스는 멈추지 않습니다. 운영체제가 프로그램을 실행할 때, 프로세스를 생성하고 메모리를 할당하며 프로그램 코드를 메모리에 로드한 다음, 프로그램의 진입점으로 이동하여 사용자 코드를 실행합니다.
하지만 이렇다면 운영체제는 아무 것도 제어할 수 없어 부적절합니다. 다시 말해, 이러한 기본 직접 실행 접근 방식은 CPU 가상화 구현에 몇 가지 문제를 야기합니다. 첫 번째 문제는 어떻게 프로그램이 우리가 원하지 않는 작업을 중지 할지(효율적으로 수행할지), 두 번째 문제는 프로세스를 실행한 다음, 어떻게 프로세스를 중지하고 CPU 시간을 효율적으로 공유하기 위해 다른 프로세스로 전환할 수 있는지입니다.
직접 실행은 하드웨어 CPU에서 프로그램을 네이티브로 실행하여 빠른 실행이 가능하다는 이점이 있지만, 이로 인해 몇 가지 문제가 발생합니다. 예를 들어, 프로세스가 디스크에 I/O 요청을 하거나 CPU와 메모리 같은 시스템 자원에 접근해야 할 때 어떻게 제한할 수 있을까요?즉, 제한된 작업을 어떻게 수행할까요?
프로세스가 I/O 및 다른 제한된 작업을 수행할 수 있어야 하지만, 시스템에 완전한 제어를 부여해서는 안 됩니다. 이러한 상황에서 운영체제와 하드웨어는 어떻게 함께 작동하여 이 문제를 해결할 수 있을까요?
하나의 방법은 모든 프로세스가 I/O와 관련된 작업을 원하는 대로 할 수 있게 하는 것입니다. 하지만, 만약 모든 프로세스가 제한 없이 디스크에 I/O 요청을 할 수 있다면, 프로세스가 디스크 전체를 읽거나 쓸 수 있게 되어 모든 보호 조치가 무용지물이 됩니다. 예를 들어, 무단으로 중요한 시스템 파일을 변경하거나, 다른 사용자의 개인 데이터를 읽는 등의 행위가 가능해집니다.
이러한 이유로, 운영체제는 프로세스가 수행할 수 있는 I/O 작업을 엄격히 제한하고, 파일 시스템 같은 중요한 자원에 대한 접근을 통제합니다. 이는 시스템의 안정성과 보안을 보장하기 위한 필수적인 조치입니다.
따라서 우리가 취하는 접근 방식은 새로운 프로세서 모드, 즉 사용자 모드(user mode)를 도입하는 것입니다. 사용자 모드에서 실행되는 코드는 할 수 있는 작업에 제한이 있습니다. 예를 들어, 사용자 모드에서 실행되는 프로세스는 I/O 요청을 할 수 없습니다. 그렇게 하려고 하면 프로세서가 예외를 발생시키고, 운영체제는 그 프로세스를 종료시킬 가능성이 높습니다.
사용자 모드와 대조적으로, 커널 모드가 있습니다. 이 모드에서는 운영체제(또는 커널)가 실행됩니다. 이 모드에서 실행되는 코드는 I/O 요청 발행 및 제한된 지시사항을 포함하여 원하는 모든 작업을 수행할 수 있습니다.
대부분의 운영체제는 수백 개의 시스템 호출을 제공합니다. 시스템호출은 사용자 권한을 가진 프로그램이 커널 권한 명령이 필요할때 이를 제한적으로 가능케합니다. 시스템 호출을 실행하기 위해, 프로그램은 특별한 '트랩' 명령어를 실행해야 합니다. 이 명령어는 커널로 동시에 점프하며 권한 레벨을 커널 모드로 상승시킵니다. 커널 내부에 들어가면, 시스템은 필요한 특권 작업을 수행할 수 있게 되고, 따라서 호출 프로세스를 위한 필요한 작업을 수행할 수 있습니다. 작업이 끝나면, 운영체제는 '트랩에서 복귀'라는 특별한 명령어를 호출합니다. 이 명령어는 호출한 사용자 프로그램으로 돌아가면서 동시에 권한 레벨을 다시 사용자 모드로 감소시킵니다.
하드웨어는 다양한 실행 모드를 제공함으로써 운영체제를 지원합니다. 사용자 모드에서는 응용 프로그램이 하드웨어 자원에 완전히 접근할 수 없습니다. 커널 모드에서는 운영체제가 기계의 모든 자원에 접근할 수 있습니다. 또한, 사용자 모드 프로그램엔 커널로 트랩(trap)하는 특별한 명령어와 트랩에서 돌아와 사용자 모드 프로그램으로 돌아가는 return-from-trap 명령어도 제공됩니다. 전환은 '트랩'과 '트랩에서 복귀' 명령어를 통해 안전하게 수행됩니다.
트랩을 실행할 때, 하드웨어는 호출자의 레지스터를 저장합니다. 예를 들어, 프로세서가 프로그램 카운터, 플래그 및 기타 몇 가지 레지스터를 각 프로세스별 커널 스택에 푸시합니다. '트랩에서 복귀'는 이러한 값을 스택에서 팝하여 사용자 모드 프로그램의 실행을 재개합니다.
그럼 트랩이 운영체제 내부에서 어떤 코드를 실행해야 할지 어떻게 알까요? 호출 프로세스는 절차 호출을 할 때처럼 점프할 주소를 지정할 수 없습니다. 그렇게 하면 프로그램이 커널 내부 어디로든 점프할 수 있게 되기 때문입니다.
커널은 부팅 시 트랩 테이블을 설정함으로써 이를 수행합니다. 기계가 부팅될 때 커널 모드에서 부팅되며, 운영체제는 하드웨어에게 특정 예외적인 사건( hard disk interrupt, keyboard interrupt, system call 등)이 발생했을 때 어떤 코드를 실행해야 할지를 알려줍니다. 여기서 '트랩 핸들러'는 특정 상황에서 운영체제가 수행해야 할 작업을 정의합니다. 예를 들어, 프로그램이 파일을 열거나 네트워크 요청을 하는 것과 같은 시스템 호출을 할 때 이를 처리하고 대응하는 코드가 각각 있습니다.
운영체제는 시스템이 부팅될 때 이러한 트랩 핸들러의 위치를 하드웨어에 알려줍니다. 즉 OS가 특정 예외 상황이 발생했을 때 실행할 코드들을 하드웨어에게 미리 알려줌으로써 trap을 처리할 명령어들의 위치를 외부(사용자 프로세스)에 노출시키지 않을 수 있는 것입니다.
즉 프로그램이 운영체제에게 trap을 통해 특정 작업을 요청하면, 하드웨어가 트랩핸들러를 보고 작업에 대응하는 운영체제의 올바른 부분으로 제어를 넘겨줌으로써 운영체제가 그 요청을 처리할 수 있게 해주는 방식입니다.
정확한 시스템 호출을 지정하기 위해, 일반적으로 각 시스템 호출에는 시스템 호출 번호가 할당됩니다. 사용자 코드는 원하는 시스템 호출 번호를 레지스터에 넣고, 트랩 핸들러 내에서 시스템 호출을 처리하는 OS는 이 번호를 검사하고, 유효한지 확인한 후, 그렇다면 해당 코드를 실행합니다. 이러한 간접성은 보호의 한 형태로 작용합니다. 사용자 코드는 정확한 주소로 점프할 수 없으며, 대신 번호를 통해 특정 서비스를 요청해야 합니다.
제한된 직접 실행(LDE) 은 두 단계입니다. 첫 번째 단계(부팅 시)에서 커널은 트랩 테이블을 초기화하고 CPU는 이후 사용을 위해 그 위치를 기억합니다. 커널은 privilieged 명령어로 이 작업을 수행합니다. 두 번째 단계(프로세스 실행 시)에서 커널은 몇 가지를 설정합니다(예: 프로세스 목록에 노드 할당, 메모리 할당) 그 후 트랩에서 복귀해 CPU를 사용자 모드로 전환하고 프로세스의 실행을 시작합니다.
프로세스가 시스템 호출을 하면, OS로 다시 트랩하여 이를 처리하고, 다시 트랩에서 복귀하여 프로세스에 제어를 반환합니다. 그런 다음 프로세스는 작업을 완료하고 main()에서 반환합니다. 이것은 일반적으로 프로그램을 적절하게 종료하는 일부 스텁 코드로 반환됩니다(예를 들어, OS로 트랩하는 exit() 시스템 호출 사용). 이 시점에서 OS는 정리 작업을 수행 및 완료합니다.
정리 :
컴퓨터 부팅시 OS가 trap table 만들어 하드웨어에게 예외별 실행코드 알려준다.
예외발생 -> HW가 Trap handler에 접근 -> OS 실행코드 실행.
Trap handling을 할 때에는 kernel stack push/pop.
.
Direct exectuion에는 한 프로세스 후 다른 프로세스가 실행됩니다.여기서, 현재 CPU에서 프로세스가 실행 중인 경우에는 운영체제가 실행되지 않는다는 문제가 발생합니다. 이로 인해 OS가 CPU 제어권을 다시 얻는 방법이 필요합니다.
CPU 제어권을 얻기 위해서는 운영체제가 CPU에서 실행되어야 합니다. 그러나 현재 실행 중인 프로세스가 있을 때 이를 어떻게 해야할까요?
일부 운영체제는 협력적인 접근 방식을 사용합니다. 이 방식에서는 프로세스가 CPU를 양보할 것이라고 가정하고, 이를 통해 운영체제가 CPU 제어권을 얻습니다. 대부분의 프로세스는 시스템 호출을 통해 CPU 제어권을 운영체제에게 양도하며, 명시적인 yield 시스템 호출을 사용하여 CPU 양보를 수행합니다.
협력적 스케줄링 시스템에서 운영체제는 시스템 호출 또는 불법적인 작업 발생을 기다리며 CPU 제어권을 다시 얻습니다. 예를 들어, 프로세스가 0으로 나누기나 접근할 수 없는 메모리에 접근하는 경우, 운영체제는 트랩을 생성하고 CPU 제어권을 회수합니다. 이러한 접근 방식은 수동적인 방식으로 CPU 제어권을 얻습니다.
하지만 만약 악의적이거나 버그가 많은 프로세스가 무한 루프에 빠져 시스템 호출을 전혀 하지 않는다면 어떻게 될까요? 그럴 때 운영체제는 무엇을 할 수 있을까요?
하드웨어로부터 추가적인 도움이 없다면, 프로세스가 시스템 호출을 거부하거나 실수로 운영체제에 제어권을 반환하지 않는 경우, 운영체제가 할 수 있는 일은 거의 없습니다. 사실, 협력적 접근 방식(A Cooperative Approach: Wait For System Calls)에서는 프로세스가 무한 루프에 빠졌을 때 유일한 해결책은 컴퓨터 시스템의 모든 문제에 대한 고전적인 해결책, 즉 기계를 재부팅하는 것입니다.
그렇다면 운영체제는 어떻게 협력하지 않는 프로세스들에게서도 CPU의 제어를 얻을 수 있을까요?
해답은 타이머 인터럽트에 있습니다 (A Non-Cooperative Approach: The OS Takes Control). 타이머 장치는 몇 밀리초마다 인터럽트를 발생시킵니다. 인터럽트가 발생하면 현재 실행 중인 프로세스가 중단되고, 운영체제의 사전 구성된 인터럽트 핸들러가 실행됩니다. 이 시점에서 운영체제는 CPU의 제어를 다시 얻었으므로, 현재 프로세스를 중지하고 다른 프로세스를 시작할 수 있습니다.
운영체제는 타이머 인터럽트가 발생할 때 어떤 코드를 실행할지 하드웨어에 알려야 합니다. 인터럽트가 발생할 때 하드웨어에는 특정 책임이 있습니다. 특히 인터럽트가 발생했을 때 실행 중이던 프로그램의 상태를 충분히 저장하여 이후에 '트랩에서 복귀' 명령어가 실행 중이던 프로그램을 올바르게 재개할 수 있도록 해야 합니다. 추가로, 특정 privileged 명령어를 통해 잠깐 이 타이머 기능을 꺼둘 수도 있다고 합니다 (Concurrency 장에서 배움).
협력적으로 시스템 호출을 통해 되찾았든, 타이머 인터럽트를 통해 더 강력하게 되찾았든, 이제 운영체제가 제어권을 되찾았을때, 현재 실행 중인 프로세스를 계속 실행할지, 다른 프로세스로 전환할지 결정해야 합니다. 이 결정은 운영체제의 일부인 스케줄러에 의해 이루어집니다. 우리는 다음 몇 장에서 스케줄링 정책에 대해 자세히 논의할 것입니다.
전환하기로 결정되면, 운영체제는 컨텍스트 스위치라고 부르는 저수준 코드를 실행합니다. 컨텍스트 스위치는 개념적으로 간단합니다. 운영체제가 해야 할 일은 현재 실행 중인 프로세스의 몇 가지 레지스터 값을 저장하고(예를 들어, 커널 스택에), 곧 실행될 프로세스의 몇 가지를 복원하는 것입니다(커널 스택에서). 이렇게 함으로써, 운영체제는 '트랩에서 복귀' 명령어가 최종적으로 실행될 때, 실행 중이던 프로세스로 돌아가는 대신, 시스템이 다른 프로세스의 실행을 재개하도록 합니다.
현재 실행 중인 프로세스의 컨텍스트를 저장하기 위해, 운영체제는 레지스터(일반 목적 레지스터, PC, 현재 실행 중인 프로세스의 커널 스택 포인터 등)를 저장하기 위한 저수준 어셈블리 코드를 실행하고, 그 다음 곧 실행될 프로세스의 레지스터, PC를 복원하고 커널 스택으로 전환합니다. 스택을 전환함으로써, 커널은 하나의 프로세스(인터럽트 당한 것)의 컨텍스트에서 스위치 코드를 호출하고 다른 프로세스(곧 실행될 것)의 컨텍스트에서 반환합니다.
전체 프로세스의 타임라인은 그림 6.3에 나와 있습니다.
이 프로토콜 중에 일어나는 레지스터 저장/복원에는 두 가지 유형이 있습니다. 첫 번째는 타이머 인터럽트가 발생할 때입니다; 이 경우, 실행 중인 프로세스의 사용자 레지스터는 하드웨어에 의해 암시적으로 저장되며, 해당 프로세스의 커널 스택을 사용합니다.
두 번째는 운영체제가 A에서 B로 전환하기로 결정할 때입니다; 이 경우, 커널 레지스터는 소프트웨어(즉, 운영체제)에 의해 명시적으로 저장되지만, 이번에는 프로세스의 프로세스 구조체에 있는 메모리로 저장됩니다.
🔍 헷갈려서 찾아본 PCB저장과 레지스터 값 저장 차이
PCB에 레지스터 값 저장 및 복구:
- Timer interrupt나 다른 이벤트에 의해 현재 실행 중인 프로세스 A가 중단되면, A의 레지스터 값을 PCB에 저장합니다. 이것은 A의 현재 상태를 보존하는 역할을 합니다.
- 그런 다음, 스케줄러가 다음으로 실행될 프로세스 B를 선택하고, B의 PCB로부터 레지스터 값을 읽어와 B의 레지스터를 복구합니다. 이렇게 하면 B가 이어서 실행될 수 있습니다.
이 과정은 레지스터 값을 커널 스택과 PCB 간에 복사하는 것이며, PCB는 프로세스의 중요한 정보를 저장하는데 사용됩니다
Trap Handling:
- Trap(예를 들어, 시스템 호출)이 발생하면 현재 실행 중인 프로세스의 레지스터 값은 현재 상태를 나타냅니다. 이러한 레지스터 값은 trap handling을 위해 커널 스택에 저장됩니다.
- Trap handling이 완료되면, 커널 스택으로부터 레지스터 값이 다시 복원되어 현재 프로세스가 중단되기 이전의 상태로 돌아갑니다.
결론적으로, PCB에 값을 저장하고 복구하는 것은 컨텍스트 스위칭에서 프로세스 간 전환 및 상태 보존을 위한 것이며, 커널 스택과 레지스터 값의 저장 및 복구는 trap handling에서 예외 상황을 처리하고 프로세스 상태를 관리하기 위한 것입니다.
그렇다면 시스템 호출 중에 타이머 인터럽트가 발생하면 어떻게 되나요? 또한, 하나의 인터럽트를 처리하는 도중에 다른 인터럽트가 발생하면 어떻게 되나요?
실제로 운영체제는 인터럽트나 트랩 처리 중에 다른 인터럽트가 발생하는 경우에 대해 신경 써야 합니다. 실제로 이 문제는 이 책의 두 번째 부분인 동시성에 대한 전체 주제입니다.
운영체제가 할 수 있는 간단한 방법 중 하나는 인터럽트 처리 중에 인터럽트를 비활성화하는 것입니다; 이렇게 하면 하나의 인터럽트가 처리되는 동안 다른 인터럽트가 CPU로 전달되지 않도록 보장합니다. 물론, 운영체제는 이를 수행하는 데 있어 주의를 기울여야 합니다; 인터럽트를 너무 오랫동안 비활성화하면 인터럽트가 손실될 수 있으며, 이는 좋지 않습니다.
운영체제는 또한 내부 데이터 구조에 대한 동시 접근을 보호하기 위한 여러 가지 정교한 잠금 체계를 개발했습니다. 이를 통해 커널 내에서 여러 활동이 동시에 진행될 수 있으며, 특히 멀티프로세서에서 유용합니다.
우리는 CPU 가상화를 구현하기 위한 몇 가지 주요 저수준 메커니즘을 설명했습니다. 이러한 기술들을 우리는 '제한된 직접 실행'이라고 통칭합니다. CPU에서 실행하고자 하는 프로그램을 실행하지만, 먼저 하드웨어를 설정하여 프로세스가 운영체제의 도움 없이 할 수 있는 것을 제한합니다.
운영체제는 먼저 (부팅 시) 트랩 핸들러를 설정하고 인터럽트 타이머를 시작하고, 그 다음에는 프로세스를 제한된 모드에서만 실행합니다. 이를 통해 운영체제는 프로세스가 효율적으로 실행될 수 있으며, 특권적인 작업을 수행하거나 CPU를 너무 오랫동안 독점한 경우에만 운영체제의 개입이 필요하다는 것을 확신할 수 있습니다.
따라서 우리는 CPU를 가상화하기 위한 기본 메커니즘을 갖추었습니다. 하지만 중요한 질문 하나가 남아 있습니다: 그렇다면 주어진 시간에 어떤 프로세스를 실행해야 할까요? 이 질문에는 스케줄러가 답해야 하며, 다음 주제에서 다룹니다.
정리
• CPU는 적어도 두 가지 실행 모드를 지원 : 사용자 모드 , 커널 모드
• 일반적인 코드는 사용자 모드에서 실행되며 운영 체제 서비스를 요청하기 위해 시스템 호출을 사용하여 커널로 트랩됩니다.
• 트랩 명령은 레지스터 상태를 저장, 하드웨어 상태를 커널 모드로 변경하며 OS로 이동하여 사전 지정된 대상인 트랩 테이블로 이동합니다.
• OS가 시스템 호출을 서비스하는 작업을 완료하면, 다른 특별한 리턴-프롬-트랩 명령을 통해 사용자 프로그램으로 돌아가며, 이 명령은 트랩을 통해 OS로 진입한 후 트랩 이후의 명령으로 권한을 낮추고 제어를 반환합니다.
• 트랩 테이블은 OS가 부팅 시에 설정됩니다.
• 프로그램이 실행 중인 경우 OS는 사용자 프로그램이 영원히 실행되지 않도록 하기 위해 타이머 인터럽트와 같은 하드웨어 메커니즘을 사용합니다. (이 접근 방식은 CPU 스케줄링에 대한 비협력적인 접근 방식)
• 때로는 OS가 타이머 인터럽트나 시스템 호출 중에 현재 프로세스를 실행 중인 프로세스에서 다른 프로세스로 전환하려고 할 수 있으며, 이것을 컨텍스트 스위치라고 합니다.
📔 부록 지식 모음
📝 시스템 호출이 프로시저 호출처럼 보이는 이유
시스템 호출(예: open() 또는 read())을 호출할 때, C 언어의 일반적인 프로시저 호출과 유사하게 보입니다. 이것이 프로시저 호출처럼 보이는 이유는 단순합니다. 시스템 호출은 실제로 프로시저 호출이지만, 그 내부에는 "trap 명령어"가 숨어 있기 때문입니다.
더 자세히 말하면, 시스템 호출을 호출할 때, 당신은 C 라이브러리를 통해 일반적인 프로시저 호출을 하고 있습니다. 라이브러리는 시스템 호출의 인수를 정확한 위치에 놓으며(예: 스택 또는 특정 레지스터), 시스템 호출 번호도 알려진 위치에 저장합니다. 그런 다음, 라이브러리는 trap 명령어를 실행하여 운영체제에 시스템 호출을 알립니다. trap 이후 라이브러리는 반환 값을 처리하고 프로그램에 제어를 반환합니다.
C 라이브러리에서 시스템 호출을 만드는 부분은 어셈블리로 작성되어 있으며,하드웨어에서 특정 trap 명령어를 실행하기 위한 규칙을 따릅니다. 이로써 어셈블리 코드를 직접 작성하지 않고도 운영체제에 트랩하기 위한 코드를 사용할 수 있는 이유를 이해할 수 있습니다. 이미 누군가가 이 작업을 대신 해주었기 때문입니다.
📝 재부팅은 유용합니다
재부팅은 또한 오래되거나 누출된 자원(예: 메모리)을 회수하는 데 도움이 됩니다. 이러한 자원은 그렇지 않으면 처리하기 어려울 수 있습니다. 마지막으로, 재부팅은 자동화하기 쉽습니다. 이러한 이유들로 인해, 대규모 클러스터 인터넷 서비스에서 시스템 관리 소프트웨어가 주기적으로 기계들을 재부팅하여 이러한 이점들을 얻는 것은 드문 일이 아닙니다.
컨텍스트 스위치가 걸리는 시간
여러분이 궁금해할 수 있는 자연스러운 질문은 컨텍스트 스위치가 얼마나 걸리는지, 혹은 심지어 시스템 호출이 얼마나 걸리는지입니다. 이는 시간이 지남에 따라 상당히 개선되었으며, 대략적으로 프로세서 성능을 따라갑니다.
다만, 모든 운영체제 작업이 CPU 성능을 따라가는 것은 아니라는 점을 지적할 필요가 있습니다. 많은 운영체제 작업은 메모리 집약적이며, 메모리 대역폭은 시간이 지남에 따라 프로세서 속도만큼 극적으로 개선되지 않았습니다. 따라서 작업 부하에 따라, 최신 최고의 프로세서를 구입한다고 해서 운영체제의 속도가 여러분이 기대하는 만큼 빨라지지 않을 수도 있습니다.
'운영체제' 카테고리의 다른 글
[Operating System] Scheduling: Proportional Share (1) | 2024.01.02 |
---|---|
[Operating System] Virtualization-Processes (0) | 2023.12.19 |