[OS] 4. Mechanism - Direct Execution

Park Yeongseo·2023년 12월 24일
0

OS

목록 보기
5/54
post-thumbnail
  • 2024.06.04: 복습 및 글 다듬기

Operating Systems : Three Easy Pieces를 보고 번역 및 정리한 내용들입니다.

CPU를 가상화할 때, OS는 동시에 돌아가는 여러 작업들이 물리적인 CPU를 공유하도록 해야한다. 그 기본적인 아이디어는 한 프로세스를 잠시 실행하고 다음에는 다른 것을 실행시키는 것이다. 가상화는 이런 시분할(time sharing) 방식으로 이루어지는데, 이렇게 가상화를 구현할 때 생각해야 할 것들이 몇 가지 있다.

  1. 성능 : 어떻게 너무 많은 오버헤드를 발생시키지 않으면서 가상화를 구현할 수 있을까
  2. 제어 : 어떻게 CPU 제어권을 유지하면서 프로세스들을 효율적으로 실행할 수 있을까?
    • 제어는 특히 자원을 관리하는 OS에 있어 중요하다. 제어가 없다면 프로세스는 영원히 돌아갈 수도 있고, 접근 권한이 없는 정보들에 접근할 수도 있다.

제어를 유지하면서 높은 성능을 얻는 것이 OS를 설계하는 데 있어서의 가장 중심적인 문제가 된다.

1. 기본 테크닉 : Limited Direct Execution

프로그램을 최대한 빠르게 실행하기 위해, OS 개발자들은 제한적 직접 실행(limited direct execution)이라는 테크닉을 만들어냈다. 여기서 "직접 실행"이라는 말은 프로그램을 직접 CPU에서 실행한다는 것이다.

OS는 프로그램을 실행할 때, 프로세스 리스트에 프로세스 엔트리를 만들고, 프로그램에 어느 정도의 메모리를 할당한다. 이후 프로그램 코드를 디스크로부터 메모리로 올리고, 프로그램의 엔트리 포인트를 위치시키고 그곳으로 점프해 유저 코드를 실행한다.

OSprogram
프로세스 리스트에 엔트리를 생성
프로그램을 위한 메모리 할당
메모리에 프로그램을 적재
스택을 argc/argv을 이용해 설정
레지스터 비움
main()을 호출
 main() 실행
 main으로부터 반환 실행
프로세스의 메모리를 해제
프로세스 리스트에서 삭제

위의 방식은 간단해 보인다. 하지만 다음과 같은 의문이 있을 수 있다.

  1. 프로그램을 실행할 때, 어떻게 OS는 효율성을 잃지 않으면서도 프로그램이 우리가 원하지 않는 것을 하지 않도록 할 수 있을까?
  2. 프로세스를 실행할 때, 어떻게 OS는 이 프로세스를 멈추고 다른 프로세스를 실행할 수 있는 걸까?

위 질문들에 대한 답은 아래에 있다.

2. 문제 1 : Restricted Operations

직접 실행은 속도의 측면에서 분명한 장점을 가지고 있다. 프로그램이 하드웨어 CPU에서 직접, 충분히 빠르게 실행되기 때문이다. 하지만 CPU에서 프로그램을 실행할 때에는 다음과 같은 문제가 발생할 수 있다.

만약 프로세스가 (예컨대) 디스크에 I/O 요청을 보내거나 CPU, 메모리 등의 자원에의 접근을 더 얻고자 하는 등의 특권적 작업을 수행하고 싶어한다면 어떻게 해야할까? 어떻게 해야 해당 프로세스에 시스템에 대한 전적인 권한을 주지 않고 특권 작업들을 수행할 수 있을까?

(i) 가만히 두기

한 가지 방식은 간단히 어느 프로세스든 I/O등의 작업을 그냥 수행할 수 있게 두는 것이다. 하지만 이렇게 하면 많은 시스템에서 사용하는 기능들, 예컨대 파일에 대한 접근 권한과 같은 기능들을 포기해야 한다.

(ii) 프로세서 모드

다른 방식은 유저 모드라 불리는, 새로운 프로세서 모드를 이용하는 것이다. 유저 모드에서 돌아가는 코드들은 이 모드에서 가능한 작업들만 수행할 수 있다. 예를 들어 I/O 요청의 경우 특권이 필요한 작업이므로, 유저 모드에서 돌아가는 프로세스는 I/O 요청을 발생시킬 수 없다. 만약 해당 프로세스가 그 요청을 하면, 프로세서는 예외를 발생시키고 OS는 프로세스를 종료시킨다.

유저 모드의 반대는 커널 모드(kernel mode) 로, OS는 커널 모드에서 돌아간다. 이 모드에서 코드는 I/O 요청을 발생시키거나 특권 명령을 실행하는 등, 원하는 어떤 연산이라도 수행할 수 있다.

그렇다면 유저 모드에서 특권 명령을 수행하고 싶은 경우에는 어떻게 해야할까? 이를 위해 현대의 거의 모든 하드웨어들은 유저 프로그램이 시스템 콜을 실행할 수 있게 한다. 시스템 콜은 커널이 파일 시스템에 접근하고, 프로세스를 생성, 소멸시키고, 다른 프로세스와 상호작용하거나 메모리를 더 할당하는 등의 기능을 유저 프로그램에 제공할 수 있도록 한다.

시스템 콜을 실행하려면 프로그램은 특정한 트랩 명령을 실행해야한다. 이 명령은 커널로 점프해, 권한 레벨을 커널 모드로 올린다. 커널에 들어가고 나면 시스템은 필요한 특권 명령들을 수행할 수 있게 되고, 호출 프로세스에서 필요한 작업들을 할 수 있다. 작업이 마치면 OS는 return-from-trap 명령을 호출해 시스템 콜을 호출한 프로세스로 돌아가며, 권한 레벨을 다시 유저 모드로 바꾼다.

하드웨어는 트랩을 실행할 때, OS가 트랩이 끝나고 나서 return-from-trap 명령을 발생시켰을 때 제대로 호출 프로세스로 돌아갈 수 있도록 해야 한다. x86에서 프로세서는 PC, 플래그, 그리고 몇몇 다른 레지스터들을 프로세스 별 커널 스택에 넣는다. return-from-trap은 이 값을 스택으로부터 꺼내고 유저 모드 프로그램의 실행을 재개한다. 다른 하드웨어 시스템들은 또 다른 컨벤션들을 사용하겠지만, 기본적인 컨셉은 플랫폼에 상관없이 비슷하다.

그런데 트랩은 OS 내에서 어떤 코드를 실행할지 어떻게 알 수 있을까? 호출하는 프로세스는 프로시저 콜을 발생시킬 때와는 달리 어떤 주소로 점프할지를 모른다. 그렇다고 해서 해당 프로세스에 직접 커널의 어디로 점프할지를 알려주는 것은 좋은 선택이 아니다. 커널이 트랩에서 실행되는 코드를 주의깊게 제어해야 한다.

커널은 부트 시간에 트랩 테이블을 설정해 위와 같은 문제를 해결한다. 컴퓨터는 부팅될 때 커널 모드로 시작하기에, 필요한 하드웨어를 자유롭게 구성할 수 있다. OS가 가장 먼저 하는 것들 중 하나는, 예를 들면 하드 디스크 인터럽트가 일어났을 때, 키보드 인터럽트가 일어났을 때, 프로그램이 시스템 콜을 만들 때와 같이 하드웨어에 특정 예외 이벤트가 일어났을 때 어떤 코드를 실행해야하는지를 알려주는 것이다. OS는 하드웨어에 이러한 트랩 핸들러의 위치를 알려준다. 하드웨어는 그 정보를 받으면 기계가 다음에 재부팅 될 때까지 해당 위치를 기억하기 때문에 시스템 콜이나 여러 예외 이벤트가 일어났을 때 어떤 일을 할지 알게 된다.

시스템 콜을 정확하게 명세하기 위해, 시스템 콜에는 보통 시스템 콜 번호(system-call number) 가 할당된다. 유저 코드는 필요한 시스템 콜 번호를 레지스터나 스택에 지정된 부분에 위치시키고, OS는 트랩 핸들러 내에서 시스템 콜을 처리할 때, 해당 번호가 유효한지 검토하고, 만약 유효하다면 그에 대응하는 코드를 실행시킨다. 이 간접적인 계층은 보호의 한 형태로 작용한다. 유저 코드는 점프할 정확한 주소를 알지 못하고, 번호를 통해 특정 서비스를 요청해야 한다.

마지막으로, 트랩 테이블이 어디에 있는지 하드웨어에 알려주는 명령을 실행하는 것은 매우 강력한 기능이기 때문에, 이 작업을 위해서는 특권이 있어야 한다. 따라서 만약 유저 모드에서 이 명령어를 실행하려 하면 하드웨어는 거부한다.

3. 문제 2: Switching Between Processes

직접 실행의 다음 문제는 프로세스간 전환이다. OS는 한 프로세스를 멈추고 다른 것을 실행하면 되기 때문에 간단해 보이지만, 사실은 조금 까다롭다. 구체적으로, 만약 프로세스가 CPU에서 실행되고 있다면, 이것은 OS가 실행되고 있지는 않다는 것이다. 그런데 만약 OS가 실행되고 있지 않다면, 위와 같은 일들이 어떻게 이루어질 수 있을까?

어떻게 OS는 프로세스 전환을 하기 위해 CPU의 제어권을 다시 받을 수 있을까?

(1) Cooperative Approach : Wait For System Calls

과거의 몇몇 시스템들이 취했던 한 방식은 협력적(cooperative) 방식이라 불리는 것으로, 여기에서 OS는 시스템 내 프로세스들이 합리적으로 행동할 것이라, 즉 너무 오래 실행된 프로세스들은 주기적으로 스스로 CPU 제어권을 스스로 포기하고 OS에 넘겨줘 다른 작업을 할 수 있게 할 것이라 믿는다.

실제로 대부분의 프로세스들은 자주 시스템 콜을 통해 CPU의 제어권을 OS로 넘긴다. 이런 시스템에서는 종종 다른 프로세스를 실행하기 위해 OS로 CPU 제어권을 넘기는 yield 시스템 콜이 명시되어 있다.

유저 프로그램은 또, 0으로 나누기나 접근할 수 없는 메모리에 접근할 때와 같이 금지된 행동을 할 때에도 트랩을 발생시킴으로써 OS에 제어권을 넘긴다. 그렇기 때문에 협력적인 스케줄링 시스템에서 OS는 유저 프로그램에서 시스템 콜이나 금지된 명령이 일어나 자신에게 CPU 제어권을 다시 넘겨줄 때까지 대기한다.

하지만 정상적으로 무한 루프가 일어나는 경우에는 어떨까? OS는 아무 것도 하지 못한다. 사실 이런 협력적 접근법에서 프로세스가 무한 루프에 빠졌을 때 해결할 수 있는 방법은 재부팅을 하는 것 밖에 없다.

(2) Non-Cooperative Approach : The OS Takes Control

OS는 어떻게 프로세스가 cooperative하지 않아도 CPU 제어권을 다시 가져올 수 있을까?

추가적인 하드웨어의 도움이 없다면 OS는 프로세스가 시스템 콜이나 에러가 일어나 OS로 제어권을 돌려주지 않는 한 아무것도 할 수 없다. 이때 사용하는 하드웨어가 바로 타이머로, 타이머는 매 수 밀리세컨드마다 인터럽트를 발생시킨다. 이 인터럽트가 발생하면 현재 실행되고 있는 프로세스는 멈추고, OS에 이미 구성된 인터럽트 핸들러가 실행된다. 이 시점에서 OS는 CPU의 제어권을 다시 얻고, 실행 중이던 프로세스를 멈추고 다른 프로세스를 실행할 수 있게 된다.

시스템 콜의 경우와 마찬가지로 OS는 부팅을 할 때 인터럽트 발생 시 어떤 코드를 실행할지를 하드웨어에 미리 알려줘야 한다. 또한 부팅 과정에서 OS는 타이머도 시작해야 하는데, 이것도 특권적인 작업이다. 타이머가 한 번 시작하고 나면 OS는 결국에는 제어권이 자신에게 돌아올 것임을 알 수 있기에 사용자 프로그램을 실행할 수 있다.

하드웨어는 인터럽트가 일어났을 때 프로그램의 상태를 저장해, 이후 return-from-trap 명령이 일어났을 때 프로그램을 정상적으로 재개할 수 있도록 해야한다. 이러한 하드웨어의 동작은, 명시적으로 시스템 콜을 호출에 커널에 트랩을 발생시킬 때와 마찬가지다.

Saving and Restoring Context

어떻게든 OS가 제어권을 다시 얻으면, OS는 현재 실행 중이던 프로세스를 계속 실행할지 다른 프로세스로 전환할지를 선택해야 한다. 이 선택은 OS의 스케줄러에 의해 만들어진다.
만약 프로세스를 전환하게 된다면, OS는 문맥 전환(context-switch) 이라 불리는 로우 레벨의 코드를 실행한다. 모든 OS는 현재 실행 중인 프로세스의 레지스터 값들을 커널 스택 등에 저장하고, 다음으로 실행될 프로세스의 값들을 복원한다. 이렇게 함으로써 OS는 return-from-trap 명령어가 실행됐을 때, 실행 중이던 프로세스가 아닌 다른 프로세스의 실행을 재개할 수 있다.

지금 돌아가고 있는 프로세스의 문맥을 저장하기 위해 OS는 로우 레벨의 어셈블리 코드를 실행해 범용 레지스터, PC 커널, 스택 포인터 등을 저장하고, 다음으로 돌릴 프로세스의 그것들을 복원한다. 스택을 전환하면 커널은 한 프로세스의 문맥에서 스위치 코드를 호출하고, 다른 프로세스의 컨텍스트에서 반환한다. OS가 마지막으로 return-from-trap 명령어를 실행하면 다음으로 실행될 프로세스 실행되고 문맥 전환은 완료된다.

프로토콜이 일어날 때 두 종류의 레지스터 저장/복구가 있다는 것을 명심하자.

  1. 타이머 인터럽트가 일어났을 때.
    • 실행 중인 프로세스의 유저 레지스터들이 프로세스의 커널 스택을 사용함으로써 하드웨어에 의해 암시적으로 전환된다.
  2. OS가 프로세스를 전환하도록 결정할 때
    • 커널 레지스터가 소프트웨어, 즉 OS에 의해 명시적으로 저장된다.
    • 이때 커널 레지스터는 해당 프로세스의 프로세스 구조체에 저장된다.

4. 병행성 문제

그렇다면 시스템 콜 중에 타이머 인터럽트가 발생하거나, 인터럽트를 처리하는 중에 다른 인터럽트가 발생하면 무슨 일이 일어날까?

간단한 해법은 OS가 인터럽트 처리 중에는 다른 인터럽트를 불능화하는 것, 즉 한 인터럽트를 처리하고 있는 중에는 다른 인터럽트가 CPU에 전달되지 않게 하는 것이다. 근데 이 경우 이전에 발생한 인터럽트를 처리하는 데에 너무 오래 걸리면 이후의 인터럽트를 처리하지 못하게 될 수도 있다.

OS는 여러 락 기법을 이용해 내부 자료 구조에 동시에 접근하는 것을 막는다. 이는 커널 안에서 여러 활동들이 동시에 실행될 수 있게 하며, 특히 멀티 프로세서 구조에서 유용하다.

5. 정리

CPU 가상화를 위한 low-level 메커니즘인 제한적 직접 실행의 기본 아이디어는 프로그램을 CPU에서 실행시키되, 하드웨어를 set up해서 프로세스가 OS의 도움 없이 할 수 있는 일을 제한하는 것이다.

  • CPU는 실행에 있어 적어도 두 개의 모드를 지원해야 한다.
    + 제한적 유저 모드 & 특권적 커널 모드
  • 보통 사용자 프로그램은 유저 모드에서 돌아가고, OS 서비스를 이용하기 위해서는 시스템 콜을 사용해 커널에 트랩을 보낸다.
  • 트랩 명령은 레지스터 상태를 저장하고, 하드웨어 상태를 커널 모드로 바꾼 후, 트랩 테이블을 이용해 미리 지정된 목적지 점프한다.
  • OS가 시스템 콜에 대한 서비스를 완료하면, return-from-trap 명령어를 통해 유저 프로그램으로 이동한다. 이 명령어는 권한 레벨을 낮추고, 트랩 발생 이후의 명령어로 제어권을 돌려준다.
  • 트랩 테이블은 OS에 의해 부트 타임에 설정되며, 사용자 프로그램에 의해 수정될 수 없다. 이는 LDE 프로토콜이 프로그램을 OS의 제어권을 잃어버리지 않게 하면서도 효율적으로 실행할 수 있게 하기 위해 쓰인다.
  • OS는 사용자 프로그램이 영원히 실행되지는 않도록 타이머 인터럽트라 불리는 하드웨어 메커니즘을 사용해야한다.

0개의 댓글