개발 공부

손승열(Son Seungyeol)

[운영체제][OSTEP] 제한적 직접 실행 원리

제어를 유지하면서 효과적으로 CPU를 가상화하는 방법에 대해 공부해봅니다.

손승열(Son Seungyeol)
[운영체제][OSTEP] 제한적 직접 실행 원리

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

  • "직접 실행": 프로그램을 CPU상에서 그냥 직접 실행시키는 것

(아무 제한이 없는) 직접 실행 프로토콜의 순서

  1. 프로세스 목록의 항목을 생성

  2. 프로그램 메모리 할당

  3. 메모리에 프로그램 탑재

  4. argc/argv를 위한 스택 셋업

  5. 레지스터 내용 삭제

  6. call main( ) 실행

    1. main( ) 실행

    2. main에서 return 명령어 실행

  7. 프로세스 메모리 반환

  8. 프로세스 목록에서 항목 제거

그러나!

  • 이 접근법은 CPU를 가상화함에 있어 몇 가지 문제를 일으킴

    • 프로그램을 직접 실행시킬 때, 운영체제가 원치 않는 일을 하지 않는다는 것을 어떻게 보장하는 가?

    • CPU를 가상화하는 데 필요한 시분할(time sharing) 기법을 어떻게 구현할 수 있느냐?

  • 이러한 질문에 답하면서 CPU 가상화에 대한 기법을 발전시키는 과정 중에 "제한적"이라는 이름이 어디서 비롯되었는지 살펴보자

    • 프로그램 실행에 제한을 두지 않으면 운영체제는 어떠한 것도 제어할 수 없음 ➡️ 단순한 라이브러리


2️⃣ 문제점 1: 제한된 연산

  • 직접 실행의 장점은 빠르게 실행된다는 것

    • 기본적으로 프로그램이 하드웨어 CPU에서 실행되기 때문

  • 그러나 CPU에서 직접 실행시키면 새로운 문제가 발생

    • 프로세스가 디스크 입출력 요청, 시스템 자원 추가 할당 요청과 같은 특수한 종류의 연산 수행을 원하는 경우

  • 프로세스가 원하는 대로 할 수 있게 방치하는 방안 ➡️ 바람직한 시스템을 구축하는 데에는 방해 요인

  • 프로세스가 제한된 연산(입출력 연산 등)을 수행하기 위해 운영체제와 하드웨어가 해야 할 일은 무엇인가?

사용자 모드(user mode)

  • 사용자 모드에서 실행되는 코드는 할 수 있는 일이 제한

  • 만약 프로세스가 실행 중에 입출력 요청을 할 수 없도록 설정하면 입출력 요청을 했을 때 프로세스가 예외를 발생시키고 운영체제는 해당 프로세스를 제거

커널 모드(kernel mode)

  • 사용자 모드와 대비되는 모드로서 운영체제의 중요한 코드들이 실행

  • 커널 모드에서 실행되는 코드는 모든 특수한 명령어를 포함하여 원하는 모든 작업을 수행

시스템 콜의 사용

  • 사용자 프로세스가 디스크 읽기와 같은 특권 명령어를 실행해야 하는 경우 ➡️ 시스템 콜 사용

    • 파일 시스템 접근, 프로세스 생성 및 제거, 다른 프로세스와의 통신 및 메모리 할당 등

    • 대부분의 운영체제는 수백 개의 시스템 콜을 제공

    • 초기 Unix 시스템은 약 20개 정도의 시스템 콜을 제공

trap과 return-from-trap 특수 명령어

  • 시스템 콜을 실행하기 위해 프로그램은 trap 특수 명령어를 실행해야 함

    • 이 명령어는 커널 안으로 분기하는 동시에 특권 수준을 커널 모드로 상향 조정

    • 커널 모드로 진입 ➡️ 운영체제는 모든 명령어 실행 가능 ➡️ 프로세스가 요청한 작업 처리 가능

  • 완료되면 운영체제는 return-from-trap 특수 명령어를 호출

    • 이 명령어는 특권 수준을 사용자 모드로 다시 하향 조정하면서 호출한 사용자 프로그램으로 리턴

  • 하드웨어는 trap 명령어를 수행할 때 주의가 필요

    • 호출한 프로세스의 필요한 레지스터들을 저장 ➡️ 운영체제가 return-from-trap 명령어 실행 시 사용자 프로세스로 제대로 리턴할 수 있기 위함

    • x86에서는 프로그램 카운터, 플래그와 다른 몇 개의 레지스터를 각 프로세스의 커널 스택(kernel stack)에 저장

    • return-from-trap 명령어가 이 값들을 스택에서 pop하여 사용자 모드 프로그램의 실행을 다시 시작

  • trap이 운영체제 코드의 어디를 실행할지 어떻게 아는가?

    • 호출한 프로세서는 분기할 주소를 명시할 수 없음

    • 주소를 명시한다는 것은 커널 내부의 원하는 지점을 접근할 수 있다는 것 ➡️ 위험 ⚠️

    • 커널이 임의의 코드를 실행하기 위해서는 접근 권한 검사가 끝난 후 분기

    • 따라서 커널은 trap 발생 시 어떤 코드를 실행할지 신중하게 통제

트랩 테이블(trap table)

  • 커널이 부팅 시에 만들고 이를 이용하여 시스템을 통제

  • 컴퓨터가 부트될 때는 커널 모드에서 동작 ➡️ 하드웨어를 원하는 대로 제어 가능

  • 운영체제의 초기 작업 중 하나: 하드웨어에게 예외 사건이 일어났을 때 어떤 코드를 실행해야 하는지 알려줌

  • 운영체제는 특정 명령어를 사용하여 하드웨어에게 트랩 핸들러(trap handler)의 위치를 알려줌

    • 시스템 콜 등 예외적인 사건이 발생했을 때 하드웨어는 어느 코드로 분기하여 실행할지 알 수 있음

  • 정확한 시스템 콜을 특정하기 위해 각각의 시스템 콜에 시스템 콜 번호를 할당함

    • 사용자 코드는 원하는 시스템 콜 번호를 레지스터나 스택의 명시된 위치에 배치하는 일을 담당

    • 운영체제는 트랩 핸들러 내에서 시스템 콜을 처리할 때 이 번호들을 검사하여 유효한 지 확인하고 유효하다면 해당 코드를 실행

    • 사용자 코드는 이동할 정확한 주소를 지정할 수 없고 번호를 통해 특정 서비스를 요청 ➡️ 일종의 보호 역할 🛡️

  • 하드웨어에게 트랩 테이블의 위치를 알려주는 것은 매우 강력한 기능

    • 따라서 이는 특권 명령어이며 사용자 모드에서 사용 불가능

제한적 직접 실행 프로토콜의 순서

  1. [운영체제: 부트 (커널 모드)] 트랩 테이블 초기화

    1. [하드웨어] syscall 핸들러의 주소 기억

  2. [운영체제: 부트 (실행 모드)] 프로세스 목록에 항목 추가

  3. [운영체제: 부트 (실행 모드)] 프로그램을 위한 메모리 할당

  4. [운영체제: 부트 (실행 모드)] 프로그램을 메모리에 탑재

  5. [운영체제: 부트 (실행 모드)] argv를 사용자 스택에 저장

  6. [운영체제: 부트 (실행 모드)] 레지스터와 PC를 커널 스택에 저장

  7. [운영체제: 부트 (실행 모드)] return-from-trap

    1. [하드웨어] 커널 스택으로부터 레지스터 복원

    2. [하드웨어] 사용자 모드로 이동

    3. [하드웨어] main으로 분기

      1. [프로그램 (사용자 모드)] main( )을 실행

      2. [프로그램 (사용자 모드)] 시스템 콜 호출

      3. [프로그램 (사용자 모드)] 운영체제로 trap

    4. [하드웨어] 레지스터를 커널 스택에 저장

    5. [하드웨어] 커널 모드로 이동

    6. [하드웨어] 트랩 핸들러로 분기

  8. [운영체제: 실행 (커널 모드)] 트랩 처리

  9. [운영체제: 실행 (커널 모드)] syscall의 임무 수행

  10. [운영체제: 실행 (커널 모드)] return-from-trap

    1. [하드웨어] 커널 스택으로부터 레지스터 복원

    2. [하드웨어] 사용자 모드로 이동

    3. [하드웨어] 트랩 이후의 PC로 분기

      1. [프로그램 (사용자 모드)] main에서 리턴

      2. [프로그램 (사용자 모드)] trap(exit( )를 통하여)

  11. [운영체제: 실행 (커널 모드)] 프로세스의 메모리를 반환

  12. [운영체제: 실행 (커널 모드)] 프로세스 목록에서 제거

  • 프로세스는 커널 스택을 각자 가지고 있음

    • 커널 모드로 진입/진출 시 하드웨어에 의해 프로그램 카운터와 범용 레지스터 등의 레지스터가 저장되고 복원되는 용도로 사용

  • LDE(제한적 직접 실행) 프로토콜은 두 단계로 진행

    • 전반부(부팅 시)

      • 커널은 트랩 테이블을 초기화

      • CPU는 나중에 사용하기 위해 테이블의 위치 기억

      • 커널은 이러한 작업을 커널 모드에서만 사용할 수 있는 명령어를 이용하여 수행(위의 순서에 밑줄로 표시)

    • 후반부(프로세스 실행 시)

      • return-from-trap을 이용하여 사용자 프로세스를 시작할 때 몇 가지 작업을 수행

        • 새로운 프로세스를 위한 노드를 할당하여 프로세스 리스트에 삽입, 메모리 할당 작업 등 포함

      • return-from-trap 명령어는 CPU를 사용자 모드로 전환하고 프로세스 실행을 시작

      • 프로세스가 시스템 콜을 호출하면 운영체제로 다시 trap

      • 운영체제는 시스템 콜 처리 후 return-from-trap 명령어를 사용하여 다시 제어를 프로세스에게 넘김

      • 프로세스는 작업을 마친 후 main( )에서 리턴 ➡️ 일반적으로 스텁 코드로 리턴하고 스텁 코드가 프로그램을 종료시킴

        • 종료시킬 때 exit( ) 시스템 콜을 호출하고 다시 운영체제로 trap

      • 운영체제는 정리 작업을 진행하고 모든 일이 완료됨


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

  • 운영체제는 실행 중인 프로세스를 계속 실행할 것인지, 멈추고 다른 프로세스를 실행할 것인지를 결정

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

    • 운영체제는 어떻게 CPU를 다시 획득하여 프로세스를 전환할 수 있는가?

협조 방식: 시스템 콜 기다리기

  • 과거의 몇몇 시스템에서 채택되었던 방식

    • Mac11, Alt79 등

  • 운영체제가 프로세스들이 합리적으로 행동할 것이라고 신뢰

  • 너무 오래 실행할 가능성이 있는 프로세스 ➡️ 운영체제의 다른 작업 실행 결정을 위해 주기적으로 CPU를 포기할 것이라고 가정

  • 대부분의 프로세스는 자주 시스템 콜을 호출 ➡️ CPU의 제어권을 운영체제에게 넘겨줌

  • 이런 유형의 운영체제는 yield 시스템 콜을 제공 ➡️ 운영체제에게 제어를 넘겨 운영체제가 다른 프로세스를 실행할 수 있게함

  • 응용 프로그램이 비정상적인 행위를 하는 경우 ➡️ 운영체제에게 제어가 넘어감

    • 0으로 나누기, 접근할 수 없는 메모리에 접근 등

    • 운영체제로의 trap이 일어남 ➡️ 운영체제는 다시 CPU를 획득하여 해당 프로세스를 종료

  • 즉, 협조 방식의 스케줄링 시스템에서 운영체제가 CPU의 제어권을 다시 획득하는 방법

    • 시스템 콜이 호출되기를 기다림

    • 불법적인 연산이 일어나기를 기다림

  • 단, 프로세스가 무한 루프에 빠져 시스템 콜을 호출할 수 없다면? ➡️ 컴퓨터 재부팅(협조 방식의 한계)

비협조 방식: 운영체제가 전권을 행사

  • 타이머 인터럽트(timer interrupt)를 이용

    • 수 밀리 초마다 인터럽트를 발생시키도록 프로그램 가능

  • 인터럽트 발생 ➡️ 현재 수행 중인 프로세스는 중단되고 미리 구성된 운영 체제의 인터럽트 핸들러(interrupt handler)가 실행 ➡️ 운영체제는 CPU 제어권을 다시 얻음

  • 운영체제는 하드웨어에게 타이머 인터럽트가 발생했을 때 실행해야 할 코드를 알려주어야 함

    • 부팅 시 운영체제가 이러한 작업을 진행

  • 부팅 과정 진행 중 운영체제가 타이머를 시작

  • 타이머는 특정 명령어를 수행하여 끌 수도 있음

  • 인터럽트 발생 시 하드웨어의 작업

    • 실행 중이던 프로그램의 상태를 저장(다양한 레지스터가 커널 스택에 저장) ➡️ return-from-trap 명령어가 프로그램을 다시 시작할 수 있도록 해야 함

    • 시스템 콜이 호출되었을 때 하드웨어가 하는 동작과 매우 유사

문맥의 저장과 복원

  • 스케줄러(scheduler): 현재 실행 중인 프로세스의 계속 실행 vs. 다른 프로세스로 전환을 결정

  • 후자의 경우 운영체제는 문맥 교환(context switch) 코드를 실행

    • 프로세스 전환을 위해 운영체제는 저수준 어셈블리 코드를 사용하여 다음을 수행

      • 현재 실행 중인 프로세스의 범용 레지스터, PC, 현재 커널 스택 포인터를 저장

      • 곧 실행될 프로세스의 범용 레지스터, PC를 복원하고 커널 스택을 이 프로세스의 커널 스택으로 전환

      • 운영체제가 return-from-trap 명령어를 실행하면 프로세스가 전환

    • switch( ) 루틴: 하드웨어의 타이머 인터럽트 발생 후 운영체제(커널 모드)에서 수행

      • A 프로세스 ➡️ B 프로세스로 전환되는 경우

      • A의 레지스터의 현재 값을 A의 프로세스 구조체에 저장

      • B의 프로세스 구조체에서 B의 레지스터를 복원

      • B의 커널 스택을 사용하도록 스택 포인터를 바꿈

    • 두 번의 레지스터의 저장/복원이 일어남

      • [1] 타이머 인터럽트 발생 시: 실행 중이던 프로세스의 커널 스택에 실행 중인 프로세스의 사용자 레지스터가 하드웨어에 의해 암묵적으로 저장

      • [2] 운영체제가 프로세스를 전환하기로 결정 시: 커널 레지스터는 운영체제에 의하여 해당 프로세스의 프로세스 구조체에 저장


4️⃣ 병행성에 대한 우려

  • 운영체제는 인터럽트 또는 trap을 처리하는 도중에 다른 인터럽트가 발생할 떄 어떤 일이 생기는 가에 대해 신중하게 고려할 필요가 있음

  • 운영체제가 할 수 있는 간단한 해법은 인터럽트를 처리하는 동안 인터럽트를 불능화 시키는 것

    • 인터럽트를 너무 오랫동안 불능화시키면 인터럽트를 놓치게 되고 기술적으로도 좋지 않음

  • 운영체제는 내부 자료 구조에 동시에 접근하는 것을 방지하기 위해 많은 정교한 락(lock) 기법을 개발해 옴

    • 커널 안에서 동시에 다수의 활동이 진행될 수 있게 허용


📚 참고 문헌

Operating Systems: Three Easy Pieces ― 6: Mechanism: Limited Direct Execution

운영체제 아주 쉬운 세 가지 이야기 ― 9: 제한적 직접 실행 원리

관련있는 게시물

[운영체제][OSTEP] 프로세스 API
개발 공부

[운영체제][OSTEP] 프로세스 API

프로세스 API에 대해 공부해봅니다.

손승열(Son Seungyeol)
손승열(Son Seungyeol)
[운영체제][OSTEP] CPU 스케줄링
개발 공부

[운영체제][OSTEP] CPU 스케줄링

운영체제의 다양한 스케줄링 정책에 대해 공부해봅니다.

손승열(Son Seungyeol)
손승열(Son Seungyeol)

Made with React, Gatsby and DatoCMS by @smastrom

Contribute or star on GitHub

© 2022-2023 손승열 for 🅒🅞🅝🅣🅔🅝🅣🅢

모두 좋은 하루 보내세요! 😊