[OS] 가상화의 세계 - 제한적 실행 원리

아주 쉬운 세가지 이야기 - 가상화의 세계

제한적 직접 실행 원리

운영체제는 여러 프로세스가 동시에 실행되는 것처럼 보이게 만들기 위해 CPU를 가상화한다. 실제 CPU는 한 순간에 하나의 작업만 실행할 수 있지만, 운영체제는 한 프로세스를 잠깐 실행하고, 다른 프로세스를 또 잠깐 실행하는 방식으로 CPU 시간을 나누어 사용한다. 이를 통해 사용자는 여러 프로그램이 동시에 실행되는 것처럼 느끼게 된다.

하지만 CPU를 가상화할 때는 두 가지 문제가 생긴다.

첫 번째는 성능 문제이다. 운영체제가 프로세스 실행에 너무 자주 개입하면 오버헤드가 커져 프로그램 실행 속도가 느려질 수 있다.

두 번째는 제어 문제이다. 프로세스를 CPU에서 직접 실행시키면 빠르지만, 운영체제가 CPU에 대한 제어권을 잃을 수 있다. 예를 들어 어떤 프로세스가 무한 루프에 빠지거나, 접근하면 안 되는 시스템 자원에 접근하려고 할 수도 있다.

따라서 운영체제는 다음과 같은 문제를 해결해야 한다.

어떻게 하면 CPU를 효율적으로 가상화하면서도 운영체제가 시스템에 대한 제어권을 유지할 수 있을까?

이 문제를 해결하기 위해 사용되는 핵심 기법이 제한적 직접 실행이다.

기본 원리: 제한적 직접 실행

제한적 직접 실행은 프로그램을 CPU에서 직접 실행시키되, 아무 제한 없이 실행시키는 것이 아니라 운영체제와 하드웨어가 정한 제한 안에서 실행시키는 방식이다.

프로그램을 빠르게 실행하려면 CPU 위에서 직접 실행시키는 것이 가장 좋다. 운영체제가 프로그램의 모든 명령어를 대신 실행한다면 너무 느려진다. 그래서 운영체제는 프로세스를 생성하고, 메모리를 할당하고, 프로그램 코드를 메모리에 올린 뒤, CPU가 사용자 프로그램을 직접 실행하도록 넘긴다.

운영체제가 프로세스 생성
→ 프로그램을 위한 메모리 할당
→ 프로그램 코드를 메모리에 적재
→ 실행 시작 지점 설정
→ CPU가 사용자 프로그램 직접 실행

이 방식의 장점은 빠르다는 것이다. 프로그램이 실제 CPU 위에서 직접 실행되기 때문이다.

하지만 단순히 직접 실행만 허용하면 문제가 생긴다. 사용자 프로그램이 디스크에 마음대로 접근하거나, 메모리를 함부터 건드리거나, CPU를 계속 독점할 수 있기 때문이다.

그래서 직접 실행에는 반드시 제한이 필요하다. 운영체제는 프로그램을 직접 실행시키면서도, 프로그램이 해서는 안 되는 일은 하지 못하도록 막아야 한다.

직접 실행

“CPU 위에서 직접 실행한다”는 말은 프로그램의 명령어를 운영체제가 하나하나 대신 해석해서 실행하는 것이 아니라, CPU가 직접 가져와서 실행한다는 뜻이다.

보통 운영체제에서는 “이 프로그램 실행 준비”, “메모리에 할당” 등 하나하나 검사해서 대신 실행한다.

문제점 1: 제한된 연산

프로세스는 실행 중에 파일을 읽거나, 디스크에 접근하거나, 메모리를 추가로 요청해야 할 수 있다. 하지만 이런 작업은 시스템 전체에 영향을 줄 수 있기 때문에 사용자 프로그램이 마음대로 수행하면 안 된다.

예를 들어 어떤 프로그램이 디스크를 직접 읽고 쓸 수 있다면, 파일 권한 검사는 의미가 없어진다. 따라서 운영체제는 사용자 프로그램이 중요한 자원에 직접 접근하지 못하도록 제한해야 한다.

이 때문에 사용자 모드(user mode) 라고 알려진 새로운 모드가 도입되었다. 사용자 모드에서 실행되는 코드는 할 수 있는 일이 제한된다. 프로세스가 사용자 모드에서 실행 중이면 입출력 요청을 할 수 없도록 설정한다.

커널 모드(kernel mode) 는 사용자 모드와 대비되는 모드로 운영체제의 중요한 코드들이 실행된다. 이 모드에서 실행되는 코드는 모든 특수한 명령어를 포함하여 원하는 모든 작업을 수행할 수 있다.

사용자 프로세스가 디스크를 읽기와 같은 특권 명령어를 실행해야 할 때는 어떻게 해야 하는가? 이런 제한 작업의 실행을 허용하기 위하여 거의 모든 현대 하드웨어는 사용자 프로세스에게 시스템 콜을 제공한다.

사용자 프로그램은 직접 하드웨어를 제어하지 않고, 운영체제에게 요청한다. 이 요청이 시스템 콜이다.

시스템 콜이 호출되면 사용자 프로그램은 trap 명령을 통해 운영체제로 진입한다. 이때 CPU는 사용자 모드에서 커널 모드로 전환된다. 운영체제가 요청을 처리한 뒤에는 return-from-trap 명령을 통해 다시 사용자 모드로 돌아간다.

커널은 부팅 시에 **트랩 테이블(trap table)**을 만들고 이를 이용하여 시스템을 통제한다. 컴퓨터가 부트될 때는 커널 모드에서 동작하기 때문에 하드웨어를 원하는 대로 제어할 수 있다.

사용자 프로그램 실행
→ 시스템 콜 호출
→ trap 발생
→ 커널 모드로 전환
→ 운영체제가 시스템 콜 처리
→ return-from-trap
→ 사용자 모드로 복귀

시스템 콜이 일반 함수와 비슷하게 보이는 이유

open(), read() 같은 시스템 콜은 일반적인 C 함수 호출처럼 보인다. 하지만 내부적으로는 C 라이브러리가 시스템 콜 번호와 인자를 정해진 위치에 저장한 뒤 trap 명령을 실행한다. 즉, 겉은로는 함수 호출처럼 보이지만, 실제로는 운영체제로 진입하는 특별한 과정이 숨어 있다.

문제점 2: 프로세스 간 전환

두 번째 문제점은 프로세스 간 전환을 할 수 있어야 한다는 점이다. 운영체제는 실행 중인 프로세르르 계속 실행할 것인지, 멈추고 다른 프로세스를 실행할 것인지를 결정해야 한다.(까다로운 문제이다.)

CPU에서 프로세스가 실행 중이라는 것은 운영체제가 실행 중이지 않다는 것을 의미한다. CPU에서 실행하고 있지 않다면 운영체제는 어떠한 조치도 취할 수 없다.

CPU에서 프로세스가 실행 중이라는 것은 운영체제가 실행 중이지 않다

CPU는 한 순간에 하나의 코드만 실행할 수 있다. 따라서 CPU가 사용자 프로세스의 명령어를 실행하는 동안에는 운영체제 코드가 실행되고 있지 않다. 하지만 운영체제가 사라진 것은 아니고, 메모리에 있다가 시스템 콜이나 인터럽트가 발생하면 다시 CPU 제어권을 가져와 실행된다.

협조적 방식 (Cooperative Approach)

초기 운영체제(Early Mac OS, Windows 3.x 등)가 채택했던 방식이다. 이 방식에서 운영체제는 프로세스들이 공정하게 CPU를 양보할 것이라고 가정한다. 만약 어떤 프로세스가 너무 오랫동안 CPU를 독점할 것 같으면, 그 프로세스는 정기적으로 CPU 사용권을 운영체제에게 넘겨줘서 다른 프로세스들이 실행될 수 있게 해야 한다.

이상적인 환경에서 우호적인 프로세스는 어떻게 CPU를 포기할 수 있을까?

대부분의 프로세스는 파일 입출력, 네트워크 통신 등의 작업을 위해 시스템 콜을 호출하여 CPU의 제어권을 운영체제에게 넘겨준다. 프로세스가 시스템 콜을 호출하여 자연스럽게 CPU 제어권이 운영체제로 넘어가게 된다. 어떤 운영체제들은 yield 라는 시스템 콜을 제공하기도 하는데, 이 호출은 자발적으로 CPU를 양보하는 역할을 한다.

응용 프로그램의 오작동 처리하기

운영체제는 종종 오작동 프로세스를 처리해야 한다. 현대 시스템에서는 오작동을 처리하는 방법은 단순히 행위자를 종료시키는 것이다. 불법 메모리 접근이나 잘못된 명령어 실행 같은 상황에서 운영체제가 할 수 있는 일이 그리 많지는 않다.

만약 프로세스가 잘못된 행동을 하면