ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [OS] User mode & Kernel mode
    ComputerScience/OS 2024. 3. 12. 12:47
    728x90
    이 글은 'OSTEP: Operating Systems Three Easy Pieces'를 참고하여 쓴 글로, 학습 기록용으로 작성된 글입니다.
    따라서, 정확하지 않은 내용이 있을 수 있습니다.

    옳지 않은 정보가 있다면 지나치지 마시고 수정 요청 댓글 달아주시면 감사하겠습니다. 🙂

     

    OS 과정을 진행해 오면서, 이해가 되지 않지만 그냥 슥~ 하고 지나간 부분이 여러 개 있었다. 그 중 하나가 바로 이 User mode와 Kernel mode이다. 이제는 키워드로도 등장해서, 더 이상 피할 수 없다! 바로 살펴보자.

     

    6. Mechanism: Limited Direct Execution

    우리가 가상화를 하는 이유는 무엇인가? 바로 하드웨어와 소프트웨어 기술이 발달하면서, 컴퓨터가 처리해야 할 일이 많아졌기 때문이다. 가상화를 통해서 다양한 프로세스들의 동시적 처리를 이행할 수 있어야 했다.

     

    Personal Computer의 대중화로, 많은 양의 작업을 동시에 처리해야하는 상황이 많아지기 시작한다. 역시 기술의 발전은 소비자들의 수요가 있을 때 이뤄진다는 말이 다시금 증명되는 것 같다.

     

    우리는 가상화를 위해서, Time Sharing(시분할 시스템)가 필요하다는 사실을 이전에 배웠다(아마도).

    시분할 시스템을 위해 고려해야 할 두 가지 포인트는 다음과 같다.

    • Performance
      시스템의 퍼포먼스에 저하가 없어야 한다. 즉, 오버헤드가 적어야 한다.
    • Control
      CPU 제어를 할 수 있어야 한다. 즉, 접근 불가능한 리소스에 프로세스들이 접근을 하지 못하도록 막아야 한다.

    이러한 점들을 고려하여, 가상화를 구현한 것이 다음의 방법들이다.

    6.1 Basic Technique: Limited Direct Execution

    LDE(Limited Direct Execution)을 설명하기 이전에 Direct Execution이란 무엇인지 알아야 한다.

    "Direct Execution"이란 CPU의 PC가 프로그램의 특정 부분을 가리켜 직접 프로그램을 수행하고, 한 번 수행 되면 종료될 때까지 수행하는 것을 의미한다.

     

    그렇다면 Limited한 Direct Execution은 무엇일까?

    말 그대로 Direct execution을 하는데, limited하게 하는 것을 말한다.

    제한이 없는 direct execution

     

    위 예제에 따르면, 제한이 없는 direct execution은 다음과 같은 프로토콜로 실행된다.

    1. OS가 프로세스 리스트에 entry를 생성한다.
      entry는 무엇인가?
    2. 프로그램을 위한 메모리를 할당한다.
    3. 프로그램 코드를 디스크에서 메모리로 올린다.
    4. 스택 메모리를 argc와 argv로 설정한다.
      argc와 argv는 무엇인가?
    5. 레지스터를 초기화한다.
    6. main 함수와 같은 entry point를 실행한다.
    7. 프로그램이 main()을 실행한다.
    8. main()에서 return을 실행한다.
    9. OS가 프로세스의 메모리를 free하여 할당을 해제한다.
    10. 프로세스 리스트에서 제거한다.

    위와 같은 과정을 거치는 프로토콜은, 앞서 말했던 가상화의 두 가지 포인트들 중에서 Performance 측면에서는 문제가 없다.

    왜냐하면 프로세스의 실행 중간에 OS가 어떠한 개입도 할 수 없고 프로세스가 끝나야만 다른 프로세스가 수행될 수 있기 때문이다.

    즉, context switching이 아예 발생하질 않기 때문이다.

     

    이는 좋은 방법이 아니다. 만약 프로세스가 계속해서 CPU를 점유하고 있다면 문제가 되는데, OS가 이를 통제할 방법이 없다. 따라서 이는 Control 측면에서 문제가 발생한다.

     

    따라서 control을 얻기 위해 프로세스에 제한을 두기 시작한 것이 Limited Direct Execution이다.

    6.2 Problem #1: Restricted Operations

    앞서 Direct Execution에서 발생한 문제를 되짚어보면, OS가 프로세스 중간에 개입할 수 없기 때문에 문제가 발생한다고 하였다.

    하지만 프로세스가 정상적으로 실행하고 꽤 이상적인 시간 안에 종료된다면, CPU는 다른 프로세스를 받아들일 수 있다.

     

    지금 내가 쓴 문장을 읽고 무언가 이상함을 느끼지 않았는가?

    프로세스에게 '이상적인 시간'이라는 것은 없다. 프로세스가 얼마나 CPU를 차지할 것이며 어느 정도 차지하는 것이 좋은 지는 예측하기 매우 어려운 일이기 때문이다. 다음과 같은 예시를 보자.

    • 프로세스가 실행 도중에 추가적인 메모리 요청을 한다
      -> OS는 막을 수 없다. 이에 시스템의 한도만큼 요청한다면 메모리가 터질 수 있다.
    • 프로세스가 I/O작업을 요청한다
      -> 파일 권한을 검사하는 과정을 추가하여 이를 막아보려 하더라도, 권한 검사와 같은 일은 소프트웨어적인 측면이기 때문에 이미 CPU(하드웨어)에서 '직접 실행'되고 있는 프로세스는 디스크와 같은 I/O 장치에 직접 접근 할 수 있다.
      한마디로, 권한 검사는 이 상황에서 의미 없는 일일 뿐인 것이 된다.
    • 프로세스가 무한히 진행된다
      -> 또한 막을 수 없다

    이러한 예외적인 상황이 있기 때문에, 우리는 프로세스가 아닌 OS를 통해 프로세스들을 관리해야만 한다.

    이러한 프로세스의 직접 실행이 주는 위험성 때문에 우리의 키워드인 kernel mode와 user mode를 통해 이 문제를 해결한다.

    User mode

    디스크 I/O 요청, 자원 추가 요청 같은 작업을 수행할 수 없는 달리 말하자면 제한된 기능만 수행할 수 있는 모드를 의미한다.

    만약 User mode에서 I/O 요청을 시도하고, 자원을 추가 요청하는 월권을 시도하면 OS는 그냥 프로세스를 일방적으로 kill 할 수 있다.

    Kernel mode

    User mode에서 실행될 수 없는, 하드웨어와 직접적인 연관이 있는 '위험한' 작업들을 수행할 수 있는 모드를 의미한다.

    하드웨어 관리, 메모리 관리, 프로세스 스케줄링, 시스템 호출 처리, 보안 및 접근 제어 등 시스템의 기본적인 운영과 관련된 핵심 작업을 수행할 수 있다.

     

    좋다. 유저 모드와 커널 모드를 나눠서 OS가 CPU 통제를 할 수 있도록 해준 것은 알았다. 그런데 유저가 실제로 메모리 추가 요청을 필요로 하고, 입출력을 원한다면 어떻게 해야할까?

     

    유저 모드와 커널 모드를 나눈 것은 데이터 보호와 보안을 위해서이지만, 이는 기본적으로 유저로 하여금 하드웨어를 이용할 수 있게 해야한다는 것을 전제로 하는 것이다.

     

    따라서, 유저 모드에서 커널 모드로의 전환을 통해 유저로 하여금 하드웨어 접근을 할 수 있게 해주어야 하고 이 방법이 비로소 Limited Direct Execution의 첫 번째 해결 방법이 된다. 

     

    그렇다면 user mode에서 kernel mode로의 전환은 어떻게 이뤄질까?

     

    GPT가 알려준 우리가 자주 사용하는 'MS Word'에서의 User mode와 Kernel mode의 전환 예시를 자세하게 봐보자.

    유저 모드와 커널 모드 전환의 예시

    1. 워드 실행과 문서 작성
    사용자가 운영 체제를 통해 마이크로소프트 워드를 실행한다.
    운영 체제는 워드 프로그램을 메모리에 로드하고 프로세스를 생성하여 실행한다. 이 때, 프로그램은 user mode에서 실행된다.

    2. 문서 저장 요청
    사용자가 문서를 저장하려고 할 때, 워드는 디스크에 파일을 쓰기 위해 운영 체제의 도움이 필요하다. user mode에서는 입출력 작업을 할 수 없기 때문이다.
    따라서 프로그램은  `write()` 라는 system call을 호출한다.

    3. 시스템 호출과 트랩 발생
    `write()` 함수는 내부적으로 `trap` 명령을 실행하여 프로세스를 user mode에서 kernel mode로 전환한다.
    `trap`은 시스템 호출이 필요함을 운영 체제에 알린다.
    커널은 트랩 테이블(trap table)을 참조하여 해당 시스템 호출(`write()`)을 처리하는 커널 내부 함수로 점프한다.

    4. 커널 스택
    운영 체제는 현재 프로세스의 상태(레지스터, 프로그램 카운터 등)를 커널 스택에 push하면서 상태를 저장한다.이는 나중에 프로세스로 복귀할 때 이전의 실행 흐름의 상태로 돌아가기 위함이다.

    5. 파일 시스템 접근과 문서 저장
    커널은 파일 시스템을 통해 디스크에 접근하고, 사용자가 작성한 문서 데이터를 디스크에 쓴다.
    이 과정에서 파일에 대한 접근 권한도 확인한다.

    작업 완료 후, 운영 체제는 커널은 작업 결과를 워드에 반환하고, 커널 스택을 pop 하면서 프로세스의 상태를 복원한다.
    최종적으로 return-from-trap 명령을 사용하여 사용자 모드로 전환한다.

     

    처음 보는 개념이 나왔다. 바로 system call이다.

    흐름을 통해 보자면, 사용자는 시스템에 메모리에 접근하려는 작업(저장)을 하고 싶어하고 이를 위해 커널 모드로의 전환이 필요했다. 이를 위해 수행하는 것이 system call인 것이다.

     

    이 system call 이전에 수행해야할 특수한 명령이 있는데, 바로 `trap`이다.

    `trap()`을 통해 커널에 jump in할 수 있게 되고, 커널에서 사용할 수 있는 명령어들을 사용할 수 있게 된다.

     

    예시에서는 프로그램이 호출한 write()함수의 안에 trap이 있다고 설명한다. 트랩에 대해 좀 더 자세히 알아보자.

    trap, syscall, trap table

    트랩은 더 넓은 의미에서 프로그램 실행 중에 발생하는 모든 종류의 예외 상황(예: 하드웨어 인터럽트, 오류 등)을 처리하기 위해 사용되는 메커니즘을 지칭한다.

     

    system call을 실행할 때, 응용 프로그램은 특정 트랩 명령(예: x86 아키텍처에서 int 0x80 또는 syscall 명령)을 사용하여 커널 모드로의 전환을 요청한다.

     

    -> 실제로 트랩이 trap()으로 쓰인다기 보다는 아키텍쳐 별로 trap을 위한 명령어를 지정해 놓는 것처럼 보인다. 이는 pintOS project2를 보면서 실제로 어떻게 사용되는지 좀 더 확인해 봐야 할 것 같다.

     

    이 과정에서 트랩이 발생하고, 운영 체제의 커널은 트랩 테이블을 참조하여 해당 시스템 호출을 처리한다.

    -> 트랩 테이블이라는 것을 참조하여 커널이 system call을 처리한다는데, 트랩 테이블은 도대체 무엇인가?

    • trap table
      트랩 테이블은 운영 체제의 커널이 시스템 호출, 인터럽트, 예외 등을 처리할 때 참조하는 데이터 구조이다.
      컴퓨터 시스템에서 발생하는 다양한 이벤트를 처리하기 위해 사전에 정의된 핸들러 함수(또는 루틴)의 주소를 저장하고 있다.
      트랩 테이블은 운영 체제가 부팅될 때 설정되며, 프로세스 실행 중에 발생할 수 있는 예상되는 모든 종류의 트랩을 매핑한다.

    요약하자면, OS가 부팅 될때 커널이 유저모드에서 커널모드로 전환하기 위한 명령어인 system call들을 매핑해 놓은 트랩 테이블이라는 것을 초기화한다.

     

    이후 프로그램 수행 중에 유저 모드에서 특정 system call이 수행될 때 OS의 커널은 해당 명령이 무엇인지 트랩 테이블에서 찾아서 핸들러 함수를 수행하여 유저의 요청을 처리하게 되는 것이다.

     

    이를 책에서 다음과 같은 예시로 설명한다.

     

     

    좋다. 이제 여태까지 유저 모드와 커널 모드를 어떻게 넘나드는지까지는 알았다.

    쉽게 생각해보면 가상화의 목표를 모두 달성한 것 같지만 여전히 OS는 가상화의 두 번째 목표인 CPU의 완전한 Control을 하지 못한다.

     

    만약 프로그램이 system call을 요하지 않는 작업을 계속한다면 어떨까? 그 프로세스는 CPU를 계속해서 독점한다.

    이 문제는 어떻게 해결할 수 있을까?

    6.3 Problem #2: Switching Between Processes

    우리는 이 문제를 여태 하나의 CPU에 하나의 프로세스만을 가정한 상황을 고려하였고 이를 단계적으로 해결해 나가는 과정에 있었다.

    허나 CPU가 처리해야 할 프로세스가 많고, 다양해진다면 결국 'Context Switching'을 통해 프로세스간 빠른 전환이 필요하다.

    A cooperative Approach: Wait for System calls

    이 접근은 프로세스가 일정 시간이 지나면 저절로 CPU를 양보하겠지.. 라는 아이디어에서 온 접근 방법으로 system call의 yield()를 사용하게끔 한 것이다. 이 yield는 OS에게 CPU 제어권을 바로 넘기는 system call이다.

     

    허나 이는 너무나 어리석은 생각이다. 모든 프로그래머들에게 '프로그램 짤때 일정 시간 지나면 yield() 호출하는 로직 꼭 코드에 넣으세요~' 할 수도 없거니와 그렇게 한다면 내가 생각하는 프로그래밍의 자율성는 거리가 멀게 느껴진다.

     

    A Non-cooperative Approach: The OS Takes Control

    따라서 우리는 인터럽트를 사용하고 인터럽트 중에서도 Timer Interrupt를 활용한다.

    Interrupt는 하드웨어적 사건을 OS에게 알리는 메커니즘이고 이에 반해 trap은 소프트웨어적 사건을 OS에 알리는 메커니즘이라고 볼 수 있다.

     

    Cooperative Approach에서는 System call 말고는 HW의 도움 없이 소프트웨어적으로 할 수 있는 일이 거의 없다.

    그래서 이걸 타이머 장치를 사용한 Timer interrupt를 통해 해결하는 방법이 무려 59년 전에 고안되었다(McC+63)고 한다...

     

    타이머 핸들러는 딱 두 가지 기능을 수행하는데,

    • 시간을 재고,
    • 일정 주기로 interrupt를 발생시킨다.

    이 timer interrupt를 프로세서가 감지하면 현재 실행중이던 프로세스는 중단되고, OS의 interrupt handler가 실행되어 OS가 다시 CPU의 제어권을 얻게 된다.

     

    이 timer interrupt에 의한 interrupt handler 역시 커널이 부팅시 HW에 Trap handler로써 등록해 두었기 때문에 가능하다.

    CPU 점유권을 가져왔다 한다고 해도 현재까지 실행되던 프로그램의 실행 흐름을 완전히 무시할 없다. 따라서 trap 마찬가지로, 커널 스택에 General purpose register, PC, kernel stack pointer 등의 수행 정보를 위한 값들을 저장한다.

     

    다음은 타이머 인터럽트를 통한 direct execution의 과정이다.

    타이머 인터럽트를 사용한 Direct Execution

     

    여기서 중요하게 봐야할 점은, timer interrupt는 OS가 시작한다는 것이다. 다른 프로세스에 의해서 수행되는 것이 아니다.

     

    또 하나 의아한 점은, save regs(A) -> k-stack(A)의 과정이 타이머 인터럽트와 switch() 루틴을 할 때 두 번 이뤄지고,

    restore regs(B) <- proc_t(B) 하는 과정 또한 두 번 이뤄진다는 것이다.

     

    이는 단지, switch()를 하는 과정 안에서 수행되는 작업일 뿐이다. 즉 Process A에서 Process B로 Context switch가 발생하기 때문에 수행되는 작업이고, context switch가 발생하지 않는다면 이러한 과정은 한 번씩만 이뤄질 것이다.

     

    6.4 Worried About Concurrency?

    우리는 다음의 예외사항을 또 생각해 볼 수 있다.

    Timer Interrupt를 처리하는 와중에 또 다른 인터럽트가 발생하면 어떻게 될까?

     

    이 문제를 해결하기 위해서, 공유 자원에 대한 접근을 제한하는 방식으로 동시성을 지켜 가상화를 이뤄낸다고 한다.

    우리가 pintOS project 1에서 배웠던 타이머 인터럽트, 락, 세마포어 등으로 이러한 동시성을 지켜나가는 것이다.

     

    추후에 정리를 통해 락, 세마포어, 모니터 등의 동시성을 지키는 여러 기법들에 대해 알아보도록 하겠다.

     


    Reference

    https://velog.io/@qkrdbqls1001/OS-6.-Mechanism-Limited-Direct-Execution

Designed by Tistory.