본문 바로가기
CS/운영 체제🖥️

[OS] CPU 가상화 메커니즘 (Limited Direct Execution) - 1부

by Mintta 2023. 10. 31.

이전 글

https://codingmon.tistory.com/37

 

[OS] Processes: Process, Process States, Process API

Program 소스프로그램에서 컴파일에서 얻은 실행 파일을 말하고, 이것은 HDD, SDD 저장장치에 저장됩니다. 저장장치에 저장되어있던 프로그램이 실행되려면 메인 메모리로 loading되어야합니다. 어째

codingmon.tistory.com


오늘 다룰 주제는 CPU의 가상화입니다.

 

운영체제는 여러 프로세스들이 동시에 실행되는 것처럼 보이도록 하기 위해 물리적인 CPU를 공유하도록 지원합니다. 바로 Time Sharing(시분할) 방법을 통해서 말이죠.

 

Time sharing이란 간단하게 말해서 일정 시간 동안 어떤 프로세스한테 CPU를 사용하게끔 운영체제가 CPU를 사용할 수 있는 권리를 줬다가 주어진 시간이 지나면 권리를 뺏어서 다른 프로세스에게 넘겨주고, 이걸 반복하는 것을 말합니다.

 

이렇게 프로세스, 혹은 스레드간 넘어다니는 것을 Context Switching(문맥 교환)이라고 하는데요.

앞에서도 살짝 언급했듯이 Context switch를 할 때 하던 작업들을 저장하기 위해 context를 저장하고 다시 복원하는 일을 반복해야하는데 이 일이 그렇게 간단하지 않습니다. 그렇기 때문에 CPU의 Performance, 즉 성능(속도)를 위해서 이 오버헤드를 최소화해야합니다 🤔

 

이것이 CPU를 가상화하는데 있어서 해결해야할 첫번째 챌린징 요소 바로 Performance입니다.

 

CPU 가상화의 두번째 챌린징 요소는 이제 Control입니다. 운영체제는 여러 자원들을 관리하는 관리자 역할을 수행하기 때문에 이 Control에 대한 제어는 확실하게 가져가야만 합니다. 만약 예를 들어, 한 프로세스가 Control을 가져가서 운영체제 말을 듣지 않고 계속해서 끝날 때까지 주구장창 본인만 CPU를 사용하고 있다라면 문제가 되겠죠 ? 🤔

 

그렇기 때문에 Performance, Control을 모두 갖춘 메커니즘을 가지고 우리는 CPU를 가상화해야합니다.


6.1 Basic Technique: Limited Direct Execution

가장 기본적인 테크닉은 바로 Limited Direct Execution, LDE입니다.

Limited, Directed Execution인데 우선 Directed에 대해서부터 먼저 살펴봅시다.

 

Direct execution에 대한 아이디어는 간단합니다.

Just run the program directly on the CPU.

CPU에서 프로그램을 직접 돌리는 겁니다. 그냥 PC가 가르키는 특정 부분을 CPU가 바로 실행하는 것 입니다.

 

위의 그림은 Limit가 아직 없는 상태에서의 Direct Execution Protocol입니다.

  1. 프로그램을 실행하고자 할 때 OS가 프로세스 리스트에 프로세스를 생성하고
  2. 프로그램을 위해 메모리를 할당해주고
  3. 프로그램 코드를 메모리에 loading해주고
  4. main()과 같은 entry point를 locate시켜주고
  5. 그곳으로 점프해서 user코드를 실행시킵니다.

그리고 이렇게 끝날 때까지 쭉 실행이 됩니다. 

 

하지만 여기서 문제가 있습니다..!

 

프로그램을 실행하기만 한다면, 운영체제가 원치 않는 동작을 방지하고자 할 때 어떻게 알리고 방지할 수 있을까요..? 🤔 (Problem #1)

 

그리고 프로세스를 실행하는 동안 어떻게 운영체제가 그것을 멈추고 다른 프로세스로 context switching을 시켜서 Time sharing을 통해 CPU를 가상화시킬 수 있을까요 ? 🤔  (Problem #2)

 

no limit..

즉, OS에게 이런 CPU를 제어할 수 있는 Control(제어권)이 없으면 CPU의 가상화는 불가능하게 됩니다.

 

이 문제를 해결하고자 한 방법들을 보면 LDE의 Limited에 대해 알 수 있습니다 !

Problem #1: Restricted Operations (Limited의 뜻)

프로세스가 모든 권한을 가져가서 생기는 월권 문제

위의 상황들을 미루어 보아 프로세스는 I/O 및 일부 다른 제한된 작업을 수행할 수 있어야 하지만, 프로세스에 시스템을 완전히 통제할 수 있게 해서는 안된다는 점을 알았습니다.

 

만약 저기서 프로세스가 디스크로 I/O 요청을 보내고 나면 프로세스가 디스크 전체를 읽거나 쓸 수 있게되거나, 그로 인해 문제가 생길 수도 있습니다..!

 

그렇다면 어떻게 할 수 있을까요 ? 🤔

 

바로 CPU에 두가지 모드를 추가하는 것입니다. 

모드는 Interface같은 느낌으로 받아들이셔도 괜찮을 것 같습니다 !

  • User mode: 유저프로그램과 관련된 systemcall만 수행할 수 있는 제한된 기능만을 수행할 수 있는 모드입니다. 그래서 만약 User mode에서 월권을 해서 Kernel mode에 있는 기능을 수행할려고 한다면...운영체제에서 그 프로세스를 kill해버릴 수 있습니다😱
  • Kernel mode: privileged, 특권 모드로써, I/O 요청과 같이 user mode에선 제한된 모든 작업(privileged)들을 수행할 수 있습니다.

이렇게 실행 권한을 나눠놓고 권한에 맞는 명령어셋만 노출시킴으로써 프로세스를 통제할 수 있습니다.

 

하지만 이렇게 나눠버리면 우리의 프로그램은 당연히 User mode일텐데, 진짜 I/O 요청이 필요할 때는 어떻게 접근해야하지..?

특권 명령 중에 진짜 그 명령이 필요한 상황일 때 방법이 없다는게 문제입니다.

 

그래서 이 때 필요한 것이 바로 system call입니다. 운영체제가 kernel기능 중 운영체제 관리하에 쓸 수 있게끔 허가된 kernel 기능들인 셈입니다.

 

systemcall을 실행시키기 위해서는 무조건 Trap이라는 특별한 명령어를 통해서 systemcall을 실행시켜여만 합니다.

Trap 명령어가 실행되는 순간 ! PC가 kernel code쪽으로 갑자기 이동하게 되고, CPU의 모드를 kernel mode로 권한을 바꿉니다.

 

kernel 내부로 진입하면 시스템은 필요한 특권 작업을 수행할 수 있으며, 호출하는 프로세스의 필요한 작업을 수행합니다.

작업을 완료하면 운영체제가 retrun-from-trap 명령어를 호출하게 되는데, 이 때 CPU 모드가 다시 User mode로 돌아가면서 user program, 우리의 프로그램으로 돌아가게 됩니다.

 

kernel code를 호출하는 것을 조금 더 자세히 살펴보겠습니다.

kernel code

kernel 코드에는 Trap handler에 대한 indirect jump table이 있습니다. 즉, jump table의 element들은 모두 포인터들이고, 저장되어있는 값을 통해서 메모리주소를 얻고, 그 주소로 jump를 하게 되면 필요한 작업을 수행하는 코드가 있는 곳으로 갈 수 있습니다.

 

이 jump table에서 interrupt와 exception에 대한 handling을 맡고 있습니다.

여기서 exception은 internal(내부의) interrupt라고도 합니다. 여기서 말하는 internal, 내부란 CPU와 메모리 그 안쪽을 internal이라고 하고, 그 바깥쪽을 external(Second storge, network card, 키보드, 마우스등의 I/O)이라고 합니다.

internal interrupt == exception

 

아래에는 systemcall을 위한 indirect jump table이 있고, 배열의 크기는 systemcall의 종류 개수입니다.

 

trap handler로 진입하게 되면 우선 어디서 불린 것이지를 먼저 판단합니다. (exception ? , interrupt ? , systemcall(Software trap) ?)

 

이 때 systemcall을 기준으로 테이블의 크기가 크기 때문에 통로는 한군데로 통일되어야합니다.

 

이 때 함수의 인자값을 넘기는 것을 register를 통해서 했듯이, 여기서도 마찬가지로 어떤 systemcall을 표현하는 수를 레지스터 혹은 정해져있는 스택값에 넣어놓고 kernel code로 jump하면, kernel code에서는 값이 저장되어있는 register나 스택을 보고 그 값을 offset으로 배열에서 인덱싱해서 함수를 호출합니다.

 

그리고 이 과정은 배열에서 arr[1] 이런식으로 접근하는 것과 똑같기 때문에 어떤 것이든 호출하는 시간은 O(1)에 그칩니다.


실행예시를 보면서 다시 정리해봅시다.

<<부팅시>>

  1. 컴퓨터를 부팅하면 kernel mode로 시작되고, Trap handler의 jump table을 초기화합니다. 이 때, 처음으로 운영체제가 하는 일은 어떤 exception event가 발생할 때 어떤 코드를 실행해야 하는지 1:1로 매핑, 하드웨어에게 알려주는 것 입니다. 예를 들어, 하드 디스크 인터럽트가 발생했을 때, 키보드 인터럽트가 발생했을 때 또는 프로그램이 시스템 호출을 수행했을 때 어떤 코드가 실행되어야 하는지를 정의합니다.

<< 프로그램 실행 시>>

  1. Kernel mode OS: 프로세스를 만들어놓고 main()과 같은 entry point에 대한 정보(명령어의 메모리주소)를 kernel stack에 넣어놓고 return-from-trap 명령어 호출
  2. HW: return-from-trap이 불리면 kernel stack에 저장되어있던 레지스터 값들을 메모리에 복원시키는 일을 수행하고, CPU의 권한을 trap으로 오기전의 권한으로 돌려놓고 복원된 레지스터값에 있는 PC값을 통해서 돌아간다. (main()으로 여기서는)
  3. User Program: main() 실행 후 프로그램 동작하다 systemcall 호출 !
  4. HW: Trap이 불리면 메모리에 있던 register들의 값들을 kernel stack에 저장하고, CPU를 kernel mode로 바꾸고 trap handler의 jump table로 간다. 이 때 main에서 trap handler로 넘어올 때 어떤 trap종류인지를 User program과 OS간에 약속된 위치(레지스터 혹은 스택 값)에 저장해놓으면 !
  5. Kernel mode OS: Trap handler는 그 값을 offset으로 jump table을 인덱싱해서 User program에서 요청한 작업을 수행한다. 작업이 완료되면 return-from-trap systemcall을 호출한다.
  6. HW: return-from-trap이 불리면 kernel stack에 저장되어있던 레지스터 값들을 메모리에 복원시키는 일을 수행하고, CPU의 권한을 trap으로 오기전의 권한으로 돌려놓고 복원된 레지스터값에 있는 PC값을 통해서 돌아간다. (main()에서 systemcall을 호출하고 . 난 다음 명령어로)
  7. User program: 프로그램 작업이 끝나면 exit() systemcall을 호출하면서 프로그램 종료.
  8. Kernel mode OS: 프로세스 메모리에서 할당해제 시키고 프로세스 리스트에서 지우는 등 종료 작업 뒷처리 마무리.

 

이렇게 Limited Directed Execution이 어떻게 실행되는지까지 살펴보았습니다.


글이 너무 길어지게 될 것 같아서 여기서 1부는 마무리하겠습니다. 나머지 내용은 2부에서 이어서 정리해보겠습니다 !

 

2부에서 다룰 내용은..

  • 아직 해결되지 않은 프로세스 간 전환 방법과 그 해결책입니다 !

댓글