본문 바로가기

CS/운영체제

[OSTEP] Ch2.3. Process: System Call and Trap

Operating Systems: Three Easy Pieces: Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau
https://pages.cs.wisc.edu/~remzi/OSTEP/
본 글은 위 교재을 주교재로 한 학교 '운영체제(Operating Systems)' 수업을 들으며 공부한 내용을 정리한 글입니다.

해당 글은 개인 공부 및 교육 목적으로 작성하였으며, 일부 교재에 첨부된 사진(또는 교재에서 강의노트로 첨부된 사진)들을 포함하고 있습니다.
교재 출처 사진을 최소화하고자 블로그에는 핵심 사진을 제외하곤 옮기지 않은 사진들도 있습니다. 따라서 고의로 누락한 사진들이 존재하며, 해당 사진들을 언급하는 내용이 있을 수 있습니다. 누락한 사진들은 직접 교재를 참고해 주세요.

혹시 문제가 있다면, 댓글 남겨주시면 빠른 시일 내에 확인 후 적절한 조치를 취하겠습니다.

 


How to efficiently virtualize the CPU with control?

  • 특정 정확한 순간에는, 엄밀히 1 core는 1 process(thread)만 처리 가능함.(하이퍼스레딩에선 이 개념이 깨지지만, 여기선 하이퍼스레딩을 다루지 않을 거임.)
  • 그러면, OS는 어떻게 CPU를 가상화해 여러 프로세스가 공유할 수 있게 할까? → 타임 쉐어링(Time Sharing).
  • 이런 가상화 기법을 구현하기 위해서 해결해야 할 Issue들이 있는데,
    • 성능(Performance): 어떻게 우리는 시스템에 과도한 오버헤드를 추가하지 않고 가상화를 구현할 수 있을까?
    • 통제권(Control): 어떻게 우리는 CPU에 대한 통제권을 유지하면서 프로세스를 효율적으로 실행시킬 수 있을까?
      • 이거 아까 제어권(이 또한 control)을 프로세스에 넘긴다고 말한 부분이랑 모순되는 부분인데, 여기서 말하고 있는 통제권은 위의 ‘CPU를 쓸 수 있는 제어권’이 아닌, ‘CPU의 스케쥴링 규칙에 대한 통제권과, CPU가 컴퓨터의 위험 지역에 접근할 수 없게 하는 통제권’을 말한다고 봐야함.
      • 즉 아까 나온 제어권과는 좀 다른 개념으로 저자는 설명한 듯.
      • (지금 나오는 의미의) 통제권을 상실하면 한 프로세스가 영원히 실행을 계속할 수 있고, 접근해서는 안 되는 정보에 접근할 수도 있음.

Direct Execution(직접 실행)

  • 프로그램을 CPU 상에서 그냥 naive하게 직접 실행시킨다면?? 즉, 위 표와 같은 순서로 프로그램을 실행시키는 거임.
  • 아까 배운 그 과정대로 셋업 다 해서 main() 실행하고, 프로그램 실행 다 끝나면 돌아온다는 거.
  • 이러한 Direct Execution은 1 process일 경우 빠르게 실행된다는 장점이 있긴 하나,
  • 이런 상황이면, OS는 위에서 말한 통제권(control)을 가지지 못함.
    • 프로그램이, 운영체제가 원치 않는 일을 하지 않을 거란 것을 보장할 수 없음.
    • CPU를 가상화하는 데 필요한 time sharing 기법을 구현할 수 없음.
    • 이러한 상황이면 OS는 그저 a library랑 다를 게 없어짐.
  • 이제, 이렇게 Direct Execution을 했을 때 생길 수 있는 Problem 두 가지를 알아볼 거임.

Problem 1: Restricted Operation

  • 만약 프로세스가 다음과 같은 제한된 작업들을 수행하려고 하면 어떡해야 할까? 지금까지의 Direct Execution만으로는 아래와 같은 작업들로 프로세스가 날뛰는 걸 막지 못함.
    • disk에 I/O requese를 요청하는 경우
      • 우리가 평소에 사용하던 I/O는 indirect 방식임. 운영체제한테 정중하게 부탁함. disk에서 이 영역을 읽고 싶다고? 그럼 OS가 캐시한테 그걸 저장하게 해서 메모리로 가져옴.
      • 메모리 ↔ 캐시, 캐시 ↔ 디스크의 메모리 계승 구조가 있잖음. 근데, 만약 우리가 캐시를 안거치고 디스크에 있는 데이터를 바로 가져오려 한다면 위험한 작업이다. 이게 10, 20년 전 보안 취약점의 가장 핵심적인 부분이었음.
    • CPU나 메모리와 같은 시스템 자원들에 대한 추가 접근을 획득을 요청하는 경우
  • 저런 작업들은 제한되게 수행되거나 아니면 커널한테 undercontrol되어야만 함. 그렇지 않으면, 시스템에 영향을 미치는 치명적인 오류가 발생할 수 있음.
  • 해결책: 유저 모드와 커널 모드 개념을 도입해 protected control transfer(보호된 제어 양도)를 시전하게 됨.
    • 유저 모드(User mode): 어플리케이션과 같은 유저 프로그램들이 실행되는 모드. 하드웨어 자원들에 대한 접근 권한이 일부 제한되어 있음. 예를 들어, 유저 모드 중에 실행되는 코드는 I/O 요청을 할 수 없음.
    • 커널 모드(Kernel mode): OS의 중요한 코드들이 실행되는 모드. I/O와 같은 작업들을 포함하여 원하는 모든 작업을 수행할 수 있음. disk같은 하드웨어 자원과 모든 메모리 영역에 직접 접근 가능.

System Call

  • (나무위키) 일반적인 프로그램들은 유저 모드에서 실행되므로 커널 모드에 대한 직접적인 접근이 불가능하다. 하지만 커널에 접근할 수 없으면 유저 모드의 프로세스들이 파일을 쓰거나 불러올 수 없고 그래픽 처리와 같은 거의 모든 작업을 할 수 없다. 따라서 커널에 요청하여 커널 모드에서 처리하고 그 결과를 유저 모드의 프로그램에게 전달하는 것이 바로 시스템 콜이다.
  • 유저 모드와 커널 모드 사이의 인터페이스.
  • 유저 모드에서 실행되는 프로그램이 커널 모드에서 제공하는 기능들을 이용할 수 있게 해주는 인터페이스.
  • 유저 프로세스가 커널에서 제공하는 기능들을 이용할 수 있도록 OS에서 제공하는 인터페이스.

  • 특수한 trap 명령어를 통해서 구현됨.
  • 시스템 콜은 커널이 유저 프로그램에게 특정 중요한 기능들을 노출하는 걸 신중하게 허용함. 예를 들어,
    • 파일 시스템에 접근
    • 새 프로세스를 생성하고 종료
    • 다른 프로세스들과 통신(IPC)
    • 더 많은 메모리를 할당

Interrupt vs. Exception

  • Exception = Software Interrupt
    • 문제가 되는 instruction을 실행할 때 생성됨.
      • OS의 내부 기능을 직접 요청한다든지… → trap의 예시
      • 0으로 나눈다든지 하는 소프트웨어 에러, 접근 권한이 없는 데이터에 접근한다든지, page fault가 발생했다든지… → fault의 예시
    • 즉, 현재 실행 중인 프로세스에 의해 생성됨.
    • 동기적임.
    • trap(expected, intended), fault(unexpected), abort 세 가지로 나뉨.
      • trap: trap을 유발하는 trap 명령어를 실행했을 때 발생하는 exception.
        • trap이 발생하면(trap 명령어를 실행하면) trap handler가 호출돼 해당 trap 처리를 해준 뒤에, trap이 발생한 바로 다음 코드부터 실행을 재개함.
        • 시스템 콜을 호출하는 경우임.
        • expected하며 intended한 exception임.
      • fault: 특정 exception들을 부르는 용어로, 해당 작업은 완료가 불가능함.
        • segmentation fault, page fault, division by zero fault(0으로 나누려 해서 생기는 exception) 등이 대표적인 예시.
        • 이 경우는 시스템 콜이 호출되지 않는다고 보는 게 일반적.
          • 아니면 이 책의 관점에서는 trap 명령어가 실행되니까 system call이 발생한다.
          • 그냥 발생한다고 보는 게 맞겠는데…?
        • unexpected한 exception임.
      • abort: 이 경우는 심각한 문제에 해당하는데, 이전과는 다르게 시스템이 더 이상 정상적으로 동작할 수 없는 상황이라고 판단하여 시스템이 재부팅 되거나 OS가 오동작할 수 있음.
        • abort는 exception에 포함되지 않기도 함.
    • 여기서 trap과 fault, system call에 대해서는 책마다, 사람마다 다양한 관점을 가지고 있었음.
      • 책마다 설명이 다르지만, System Call과 trap이 한 묶음이고, fault가 나머지 한 묶음으로 보는 게 일반적!! (https://monkey-engineer.tistory.com/29)
      • 사람에 따라, trap을 그냥 exception과 동의어로 보기도 했음.
        • 이 관점에서, 이 책에서는 division by zero fault 발생 시 trap이 발생한다고 서술했는데, 일반적인 관점은 아닌 듯함. 그냥 fault가 발생한다고만 보고 끝내면 될 듯함.
    • 아니면, trap 자체를 아예 ‘현재 실행중인 프로세스를 커널 모드로 전환시키는 명령어’ 로 보기도 함.
      • 이 책도 아마 이 관점을 쓰고 있는 것 같음.
      • 이 관점에 따르면, system call, fault, interrupt 이 셋은 trap 명령어에 의해 실행됨.(왜냐면 셋 다 커널 모드에 진입하긴 해야 하니까!)
  • 이 책을 읽으면 읽을수록, 그냥 예외든 인터럽트든 일어나면 트랩이 일어난다고 보는 관점이 맞는 것 같음… 그리고 트랩 명령어를 포함한 함수를 시스템 콜이라 부르니까, 시스템 콜도 일어나고.
  • Interrupt: Hardware interrupt를 그냥 Interrupt라고 부름
    • 하드웨어 장치들에 의해 생성됨.
    • 현재 실행중인 프로세스와는 관련이 없이 생성됨.
    • 비동기적임.
개념 발생 원인 복구 가능성 예시 handler
Trap 명령어 실행 중 의도적으로 발생 항상 복구 가능 시스템 콜 (syscall), int 0x80 trap handler 실행
Fault 명령어 실행 중 오류 발생 보통 복구 가능 (하지만 실패할 수도 있음) 페이지 폴트, 0으로 나누기 fault handler 실행
Abort 심각한 오류 발생 복구 불가능 하드웨어 오류, 커널 패닉 -
Interrupt 외부 장치(하드웨어)에서 발생 - 키보드 입력, 타이머 인터럽트 interrupt handler 실행

여담) 동기적 vs 비동기적

  • 동기적이란 작업이 순차적으로 또는 예상된 순서에 맞춰 실행되는 방식임.즉, 작업을 하나씩 실행하고, 이전 작업이 끝난 후에 다음 작업을 시작하는 방식임.
    • 동기적인 작업에서는 한 작업이 끝나야만 다음 작업이 실행됨. 실행 순서가 예측 가능하고 시간적으로 맞춰진 실행 흐름을 따름.
      • 그래서 exception(software interrupt)가 동기적이라는 거임. 문제가 된 instruction의 excution이 CPU에 의해 완료된 후에, 예상 가능한 시점에 exception이 발생하기 때문.
      • 또한, exception이 발생한 프로세스는 해당 exception이 처리될 때까지 대기했다가 처리가 완료된 후에 원래 프로세스가 다시 실행을 재개함. 이 역시 동기적인 특성.
    • 함수 호출에서, func1()을 호출하고 다음 line에 func2()를 호출하면, func2()는 func1()이 완료되기 전까지 실행되지 않고 기다리는데, 이러한 함수 호출 예시도 동기적이라고 볼 수 있음.
  • 비동기적이란 작업이 독립적으로 실행되고, 작업의 실행 완료 여부와 관계없이 다른 작업이 계속 실행되는 방식임. 즉, 한 작업이 다른 작업의 완료 여부와 상관없이 (이전 작업이 끝나기를 기다리지 않고 연달아) 진행됨.
    • 이러한 비동기적 작업은 병렬성을 활용하여 여러 작업을 동시에 처리할 수 있게 해줌.
      • 그래서 interrupt(hardware interrupt)가 비동기적이라는 거임. interrupt는 cpu가 뭘 하고 있는지 상관없이, 즉 CPU의 실행 흐름과 독립적으로, 예상할 수 없는 시점에 발생함.
      • 뭐 예를 들어 마우스 클릭이 일어났다 하면 바로 interrupt를 발생시키는 거임. CPU가 지금 뭘 하고있냐는 신경쓰지 않음.
    • 함수 호출에서, 파이썬의 경우 asyncio 모듈을 import해 asyncio.run()으로 함수를 비동기적으로 호출하는 경우, func2()는 func1()이 완료되기를 기다리지 않고 실행되는데, 이러한 함수 호출 예시도 비동기적이라고 할 수 있음.

trap instruction & return-from-trap instruction

  • 시스템 콜을 실행하기 위해 프로그램은 trap이라는 특수 명령어를 실행함.
  • 즉, 위에서도 말했듯 trap 명령어를 실행해서 시스템 콜을 실행(호출)하는 것임.
    • trap 명령어는 커널 안으로 프로세스를 분기하는 동시에, 특권 수준을 커널 모드로 상향 조정함.
    • 이 때, trap 명령어가 실행되면 OS는 현재 실행 중인 프로세스를 중단하고, trap handler라는, 해당 trap을 처리하는 코드 뭉치를 호출함.
    • 커널 모드로 진입하면 OS는 모든 명령어를 실행할 수 있고, 이를 통하여 프로세스가 요청한 작업을 처리할 수 있음.
    • 필요한 작업이 완료되면 OS는 return-from-trap이라는 특수 명령어를 실행함. 해당 명령어는 특권 수준을 유저 모드로 다시 하향 조정하면서 시스템 콜을 호출한 유저 프로그램으로 return함.
  • trap 명령어를 실행할 때, 내가 현재 실행중인 프로세스의 레지스터들을 따로 저장해둬야 하지 않겠음? 이는 OS가 return-from-trap 명령어 실행 시 유저 프로세스로 다시 리턴하기 위함임.
    • trap 명령어가 실행되면, 시스템 콜을 호출한 프로세스의 레지스터들을 각 프로세스의 커널 스택(kernel stack)에 저장해 둠.
    • 이후 OS가 return-from-trap 명령어를 실행하면, 이 커널 스택에서 레지스터 정보들을 다시 pop하여 원래의 프로세스로 return해 계속 프로세스를 실행할 수 있게 함.
  • 커널(OS)은 부팅 시에 trap table을 만들고 이를 이용해 시스템을 통제함.
    • trap table에는 각 trap 명령어를 실행하기 위한 trap handler들이 매핑되어 있다.
    • OS는 exception이 일어났을 때, 하드웨어에게 어떤 코드를 실행해야 하는지 알려줘야 하는데,
    • 그 때 저 trap table을 보고, ‘이 trap handler(=코드 뭉치)를 실행해라!’하고 trap handler의 위치를 알려주는 거임.
    • 아래 실제 x86 예시에서, mov eax, 5 부분이 시스템 콜 번호 5번인 open에 대한 trap handler을 실행하라고 하드웨어한테 알려주는 거임.
  • x86 등 대부분의 ISA에서는 exception(, trap), interrupt는 모두 interrupt라는 단일 하드웨어 매커니즘으로 설계됨.
    • 그래서 아래 x86의 예시를 보면, trap table이 아닌 interrupt descripter table이라는 테이블을 사용하고 있는 걸 볼 수 있음.
// int 0x80 (x86 아키텍처에서의 예시):
mov eax, 5           ; 시스템 콜 번호 (5는 open)
mov ebx, filename    ; 파일 경로
mov ecx, flags       ; 플래그 (읽기/쓰기 모드 등)
int 0x80             ; 트랩 명령어 -> 시스템 콜 호출 (커널 모드로 전환)
// 여기서, int 0x80이 trap 명령어에 해당.

// 0x80은 시스템 콜의 인터럽트 번호로, OS는 이벤트마다 각각 
// 번호 - interrupt handler address 매핑을
// IDT(Interrupt Descriptor Table)라는 테이블에 저장해
// 두는데, 여기에 0x80에 매핑되어있는 메모리 주소를 찾아서 
// 해당 interrupt handler(즉, 여기선 trap handler)를 실행함.
// 이 handler은 이제 eax 레지스터에서 시스템 콜 번호를 가져와,
// 해당 시스템 콜을 처리함.

여담) 시스템 콜과 함수(프로시저 콜)은 형태가 같음.

  • 시스템 콜은 그냥 함수랑 비슷하게 생겼음. write(), read() 등. 저 함수들이 실은 시스템 콜들인데, 우린 그걸 잘 모르고 있다.
  • 그래서, 왜 일반 함수들은 일반 함수고, 시스템 콜은 시스템 콜일까? 생긴 건 같은데?
  • 해답) 저 시스템 콜들은 진짜 함수(프로시저 콜)이 맞음. 다만, 그 내부 구현에 ‘trap 명령어’가 포함되어 있으면 시스템 콜이라고 부르는 것.
    • ex. open()을 실행할 때, C 라이브러리의 해당 함수를 호출하는 건데, 그 안에 trap 명령어가 포함되어 있음. 해당 trap 명령어를 실행해 시스템 콜을 호출함. 이 때 시스템 콜을 호출하는 부분은 어셈블리어로 작성되어 있음.

Limited Direction Execution Protocol

  • 제한적 직접 실행 프로토콜(Restricted Direct Execution Protocol): 운영체제에서 프로세스가 시스템 자원들을 직접 제어할 수 있는 메커니즘을 제한적으로 제공하는 프로코톨(방식)임.
    • 위는 제한적 직접 실행 프로토콜에 따라 시스템 콜이 실행되는 메커니즘을 요약한 타임라인임. 위에서부터 아래로 진행됨.
  • 여기서 중요한 포인트들은,
    • 기본적으로 trap 명령어가 실행되면, 시스템 콜을 호출한 프로세스의 레지스터를 커널 스택에 저장하고, return-from-trap 명령어가 실행되면 시스템 콜을 호출한 프로세스의 레지스터를 커널 스택으로부터 복원한다는 기본 동작.
    • 처음 시작 시 커널에 스택에 처음에 레지스터들을 저장해놓고, 그걸 main 프로세스로 가져오기 위해 return-from-trap 명령어가 실행됨. 바로 main() 프로세스로 레지스터를 넣는 게 아님.
    • main() 프로세스를 종료하는 것 또한 exit()시스템 콜로 이뤄지기에, 즉 종료 시에도 trap 명령어가 실행됨.
  • 참고로, 여기 보이는 Hardware는, 우리가 이런 커널 모드와 유저 모드를 왔다갔다하는 instruction을 실행하는 Chip을 말하는 거지, 우리가 기존에 다룬 disk, memory, I/O devices를 포함한 hardware resources를 말하는 게 아님