본 포스팅부터 Remzi 교수님의 Operating Systems: Three Easy Pieces로 교재를 갈아탑니다.
Limited Direct Execution
cpu를 가상화 하기 위해, 우리는 시분할 (time-sharing) 아이디어를 도입했다. 한 프로세스를 잠시 동안 실행하고, 다른 프로세스를 또 잠깐 실행하고, 이런식으로 계속해서 잠깐씩 실행시키면 된다. 그런데 여기에는 두가지 문제점이 있다.
- 성능 저하 : 계속 process를 바꾸는 것은 시스템에 과중한 overhead를 줄 수 있어, 이것은 성능 저하로 이어진다.
- 제어 문제 : cpu에 대한 제어를 유지하면서 process를 효율적으로 실행해야 한다. 그렇지 않으면 한 프로세스가 영원히 실행되거나, 접근해서는 안되는 정보에 접근할 수 있다.
어떻게 하면 cpu 가상화를 성능과 제어를 유지하면서 구현할 수 있을까? 본 포스트에서는 위의 두 문제를 해결하는 기법인 제한적 직접 실행 (Limited Direct Execution)에 대해서 알아볼 것이다.
기본 원리
Limited 라는 단어를 제외한 Direct Execution은 말 그대로 프로그램을 cpu상에서 직접 실행하는 것을 의미한다.
운영체제는 PCB에 해당하는 메모리에 정보들을 탑재하고, main()을 실행한다. 이 때, 프로그램은 한번 수행되면 종료될 때 까지 수행된다. 그러므로, 앞서 언급한 cpu 가상화를 구현할 수가 없다..!! 한번 실행된 프로세스에 대해서 제어권이 없으며, 다른 프로세스를 번갈아가면서 수행하는 time-sharing도 할 수 없다 ㅠㅠ
그러므로, 프로그램 실행에 제한을 두지 않으면 (limited) 안된다는 결론에 도달했다. 어떤 식으로 제한을 두어서 위의 문제점들을 해결할 수 있을지 알아보자.
문제점 1: 제한된 연산
직접 실행의 장점은 빠르게 실행된다는 것이다. 그러나 프로그램 실행 도중에 disk에 I/O 요청을 한다거나 Memory를 더 할당받고 싶을 때 이를 해결할 수 없다. 또한, 프로세스가 악의적인 코드를 실행하더라도 운영체제는 해당 프로세스에 cpu 제어권을 넘겨줬기 때문에 이를 막을 방법이 없다.. 이를 어떻게 해결하면 좋을까?
그냥 프로세스가 하고싶은 대로 하라고 방치하면, 위의 문제 상황 중 일부는 해결할 수 있을 것이다.. 그러나 프로세스의 권한을 제한할 수 없어 disk에 대한 보호 기능이 완전히 상실되게 된다.
이러한 문제 때문에 실제로 도입된 방법은 위의 방법이 아니다. 바로 사용자 모드 (user mode)와 커널 모드 (kernel mode)로 실행 모드를 구별하는 것이다. 사용자 모드에서는 할 수 있는 일이 제한되어 있다. 예를 들어 응용 프로그램은 하드웨어 자원에 대한 접근 권한이 제한되어 있고, 이를 넘어서는 코드가 실행된다면 프로세스는 곧바로 예외를 발생시켜 운영체제에게 해당 사실을 알려준다. 운영체제는 해당 프로세스를 제거해버린다.
커널 모드에서는 운영체제의 중요한 코드들이 실행된다. 컴퓨터의 모든 자원에 대한 접근 권한을 가지고 있다. 그래서 해당 모드에서는 원하는 모든 작업을 수행할 수 있다.
만약 사용자 프로세스가 disk I/O와 같은 특권 명령어를 실행하고 싶을때는 어떻게 하면 될까? 이러한 제한 작업의 실행을 허용하기 위해 시스템 콜 (System call)이 도입되었다. 시스템 콜은 유저 모드 프로세스에게 커널 모드에서 수행가능한 작업을 수행할 수 있도록 OS가 제공하는 API라고 이해하면 된다. 시스템 콜이 실행되면, trap
이라는 특수 명령어가 실행 되고 이는 모드를 커널 모드로 전환한다. 그리고 커널 모드에서 유저 프로세스가 요청한 특정한 작업을 처리한다. 이후 운영체제는 return-from-trap
이라는 특수 명령어를 호출하여 다시 모드를 원래 모드인 유저 모드로 전환한다.
trap
명령어가 실행될때를 좀 더 자세하게 알아보자. 해당 명령이 수행되면, 프로세스는 program counter, flags, register 정보 등을 각 프로세스의 커널 스택에 저장한다. 나중에 다시 유저 모드로 돌아왔을 때 프로세스를 제대로 리턴해야 하기 때문이다. 커널 모드에서의 작업이 마무리되면 return-from-trap
가 호출되는데, 이때 아까 커널 스택에 저장해둔 정보들을 pop 하여 모두 제거한다.
Q. trap 명령어가 실행되면, 운영체제의 어떤 코드를 실행할지 어떻게 알까?
운영체제가 trap을 처리하기 위해서 trap table을 사용한다. 이 테이블은 부팅 시에 초기화가 이루어지며, 소프트웨어적 사건들을 처리하기 위한 함수 (trap handler) 들이 들어 있다. 각 함수에는 시스템 콜 넘버 (system call number)라는 번호가 정의되어 있고, 운영체제만 해당 위치를 기억 하고 있다. 이제 유저 프로세스가 trap 명령어를 호출하면, 전달 받은 시스템 콜 번호를 확인한 후 유효한 경우 이를 테이블에서 검색 후 실행한다. (사용자 프로그램은 시스템 콜의 위치를 모른다! 일종의 보안 기법이다.)
문제점 2: 프로세스 간 전환
직접 실행의 두번째 문제점은 프로세스 간 전환 방식에 있다. 프로세스의 전환이란 실행 중인 프로세스를 멈추고 다른 프로세스를 실행하는 것이다. 우리는 이러한 전환을 통해 time-sharing을 구현할 수 있는데, 운영체제는 어떻게 cpu를 확득하여 프로세스를 전환할 수 있을까?
협조 방식
첫 번째 방법은 서로 협력하는 방식 (cooperative)으로, 착한 프로세스만 있다고 가정한다. 각 프로세스가 비정상적인 행동은 하지 않고, 오래 연산할 프로세스는 다른 프로세스들이 cpu를 사용할 수 있도록 주기적으로 cpu를 반납할 것이라고 믿는다. 프로세스가 cpu를 반납할 때에는 시스템 콜을 활용한다. 프로세스 실행 도중 시스템 콜을 적절히 활용하여 서로 협조적으로 cpu를 사용하는 방식인 것이다. 만약 프로세스 내에서 0으로 나눈다던지 하는 비정상적인 연산이 발생하면 trap이 일어나 운영체제로 주도권이 넘어온다.
그런데, 잘 생각해보면 위 방식은 매우 수동적이다. 운영체제는 프로세스가 시스템 콜을 호출하거나 에러가 발생할때 까지 계속 기다릴 수 밖에 없다. 프로세스가 무한루프에 빠져서 시스템 콜을 하지 못하는 상황이면.. 망한다 ㅠ
비협조 방식
프로세스가 시스템 콜을 하지 않는 경우, 하드웨어를 강제로 재부팅 하는 방법이 있다. 좋은 방법이긴 한데 프로세스가 너무 길게 cpu를 점유하고 있다고 재부팅을 할 수는 없지 않은가? 다른 방법은 무엇이 있을까?
시스템 콜의 호출이 없어도 운영체제가 제어권을 할당하는 방법은 바로 특정 시간마다 강제로 interrupt를 걸어버리는 것이다. (이러한 방법을 timer interrupt 라고 한다.) interrupt가 발생하면 운영체제는 수행 중인 프로세스를 중단하고 interrupt handler을 실행한다. 이 핸들러는 운영체제의 일부분이기 때문에 interrupt를 처리하는 과정에서 자연스럽게 제어권이 운영체제로 넘어가게 된다. 제어권을 획득한 운영체제는 그대로 기존의 프로세스를 실행할 수도 있고, 다른 프로세스를 실행할 수도 있을 것이다. 타이머가 interrupt를 주기적으로 발생시키기 때문에, 혹시나 비정상적인 프로그램이 동작하더라도 끌 수 있는 기회를 매 주기마다 가지게 된다.
문맥의 저장과 복원
시스템 콜을 하던 반강제적으로 타이머 인터럽트를 통해 제어권을 가져오던 운영체제가 제어권을 획득하면 이제 중요한 결정을 내려야 한다. 현재 실행중이었던 프로세스를 계속 실행할 것인지, 아니면 다른 프로세스로 전환할 것인지를 말이다. 이 결정은 운영 체제의 스케쥴러 (scheduler)가 내리는데 이후에 자세히 공부할 것이다.
일단 현재 프로세스를 중단하고 다른 프로세스로 전환하기로 결정했다고 하자. 이 때, 운영체제는 문맥 교환 (context switch)이라 불리는 코드를 실행한다. 현재 실행 중인 프로세스의 레지스터 값 (문맥) 들을 커널 스택에 저장하고, 새로 실행될 프로세스의 커널 스택으로부터 레지스터 값을 복원하는 것이다. 그렇게 되면 return-from-trap
명령어가 실행될 때 현재 사용중인 프로세스로 리턴이 아니라, 레지스터 값이 복원된 다른 프로세스로 리턴된다.
Q. 문맥 교환 시 레지스터의 저장/복원에 대해서 조금 더 자세하게 알고 싶어!
문맥 교환 시 서로 다른 두 가지 종류의 레지스터의 저장 및 복원이 발생함. 첫 번째는 타이머 인터럽트가 발생했을 때인데, 사용 중인 프로세스의 레지스터가 하드웨어에 의해 저장되고, 저장 장소로는 해당 프로세스의 커널 스택이 사용됨. 두 번째는 운영체제가 문맥 교환을 결정했을 때 발생하는데, 이 경우 커널 레지스터는 운영체제에 의해 해당 프로세스의 PCB에 저장됨.
복원 시에도 먼저 PCB에 저장되어 있는 레지스터를 복원하고, 이후 전환되는 프로세스의 커널 스택에 해당 레지스터가 복원됨.
요약
- 제한적 직접 실행 방식은 마치 아기 보호 장치와 같음. cpu 사용에 대한 안전 장치를 제공하는 것임
- 사용자 모드와 커널 모드로 구분하여, 커널 모드에서만 모든 명령어가 실행될 수 있도록 함
- 읿반적인 응용 프로그램은 사용자 모드에서 실행되며, 시스템 콜을 사용해여 커널로 trap해서 운영체제의 서비스를 요청함
- 프로세스 간 전환을 비협조적 방식으로 수행할 경우, 이를 위한 방법 중 하나가 주기적으로 타이머 인터럽트를 발생시키는 것임
- 문맥 전환은 현재 프로세스에서 다른 프로세스로 전환하는 것을 의미함