Computer Architecture/CS APP

[CS] 예외 상황 및 프로세스 (Exceptions and Processes)

  • -

프로그램 카운터는 프로세서에 전원을 처음 공급하는 시점부터 전원을 끌 때까지 연속된 값들을 가정한다.

 

인스트럭션 Ik 에 대응되는 주소가 ak일 때 ak+1로의 전환을 제어이동이라고 부르고,

이러한 제어이동의 배열제어흐름이라고 한다.

 

점진적인 순서의 제어흐름은 Ik 와 Ik+1 이 메모리에서 서로 나란히 있는 경우다.

Ik 와 Ik+1 이 인접해있지 않은 경우는 jumpcall 인스트럭션이 발생한다.

 

시스템들은 내부 프로그램 변수에 의해 표시되지 않으며,

프로그램의 실행과는 관련 없는 시스템 상태의 변화에도 반응할 수 있어야 한다.

  1. 하드웨어 타이머는 규칙적인 간격으로 꺼지며, 시스템은 이것을 처리해야 한다.
  2. 패킷들은 네트워크 어댑터에 도착하고 메모리에 저장되어야 한다.
  3. 프로그램은 디스크로부터 데이터를 요청하며, 완료될 때까지 잠든 상태에 들어간다.
  4. 부모 프로세스는 자식 프로세스가 종료될 때 통지를 받아야 한다.

시스템들은 이러한 상황에 제어 흐름의 갑작스런 변화를 만드는 방법으로 반응한다.

이와 같은 급격한 변화를 예외적인 제어 흐름(Exceptional control flow, ECF)이라고 부른다.

 

이는 컴퓨터 시스템의 모든 수준에서 발생한다.

예를 들어, 하드웨어 수준에서 하드웨어의 의해 검출되는 이벤트들은 예외 핸들러로 갑작스 런 제어이동을 발생시킨다.

또한 커널 수준의 문맥전환(Context switching)을 통해서 유저 프로세스에서 다른 프로세스로 제어가 이동한다.

 

프로세스는 시그널 핸들러로 제어를 급격히 이동하는 다른 프로세스로 시그널을 보낼 수 있다.

개별 프로그램은 일반적인 스택 운영을 회피하고 다른 함수 내 임의의 위치로 비지역성 점프를 하는 방법으로 에러에 대응할 수 있다.

 

응용이 운영체제와 하는 상호작용은 모두 ECF 를 중심으로 돌아간다.

 

예외 상황

예외 상황의 자세한 내용은 시스템에 따라 다르지만, 기본적인 아이디어는 모든 시스템에서 동일하다.

 

 

어떤 인스트럭션 Ik 를 실행하고 있을 때 프로세서 상태에 중요한 변화가 일어나고 있다.

이 상태는 프로세서 내 다양한 비트들과 신호들로 인코드되고, 상태 변화는 이벤트라고 한다.

 

이 이벤트가 현재 인스트럭션의 실행에 직접적으로 관련될 수 있는 경우는 다음과 같다.

(가상메모리 페이지 오류, 산술 오버플로우, divide by zero 시도 등)

 

이벤트가 현재 인스트럭션의 실행과 관련이 없는 경우는 다음과 같다.

(시스템 타이머 정지, I/O 요청이 완료되는 경우)

 

어느 경우이든지 프로세서가 이벤트 발생을 감지하면,

예외 테이블이라고 하는 점프 테이블을 통해서 이 특정 종류의 이벤트를 처리하기 위해

특별히 설계된 운영체제 서브루틴인 예외처리 핸들러 로 간접 프로시저 콜을 하게 된다.

 

예외처리 핸들러가 처리를 끝마치면, 예외상황을 발생시킨 이벤트의 종류에 따라서 다음의 일 중 하나가 발생한다.

  1. 핸들러 는 제어를 현재 인스트럭션으로 돌려준다. (이벤트가 발생했을 때 실행되고 있던 인스트럭션)
  2. 핸들러 는 제어를 다음 인스트럭션으로 돌려준다. (예외상황이 발생하지 않았더라면 다음에 실행되었을 인스트럭션)
  3. 핸들러 는 중단된 프로그램을 종료한다.

 

예외 처리

한 시스템 내에서 가능한 예외 상황의 종류마다 중복되지 않은 양의 정수를 예외번호 로 할당한다.

- 프로세서 설계자가 할당한 번호는 divide by zero, 페이지 오류, 메모리 접근 위 반, break point, 산술연산 오버플로우 등 포함

- 커널 설계자가 할당한 번호는 시스템 콜, 외부 I/O 디바이스로부터의 시그널 등 포함

 

시스템 부팅 시, 운영체제는 예외 테이블이라고 하는 점프 테이블을 할당하고 초기화 해서

엔트리 k 가 예외 상황 k 에 대한 핸들러의 주소를 갖는다.

 

예외 테이블 포맷

 

런타임에 프로세서는 이벤트가 발생했다는 것을 감지하고 대응되는 예외번호 k 를 결정한다.

프로세서는 그 후에 예외 테이블의 엔트리 k 를 통해서 간접 프로시저 콜을 하는 방법 으로 예외 상황을 발생시킨다.

 

프로세서가 예외 테이블을 이용해서 해당 예외 핸들러의 주소를 어떻게 만드는지 보여줌

 

예외 번호는 예외 테이블의 index 이며, 이 테이블의 시작 주소는 예외 테이블 베이스 레지스터 라는 특별한 CPU 레지스터에 저장되어 있다.

 

예외 상황은 프로시저 콜과 유사하지만, 일부 중요한 차이점이 있다.

  1. 프로세서는 프로시저 콜을 사용해서 핸들러분기하기 전에 스택에 리턴주소를 push한다.
    하지만 예외 종류에 따라 리턴주소는 현재 인스트럭션이거나 다음 인스트럭션이 된다.
  2. 프로세서는 핸들러 가 리턴할 때, 중단된 프로그램을 다시 시작하기 위해 필요할 스택에 추가적인 프로세서 상태를 push한다. ( EFLAGS 레지스터 )
  3. 제어가 사용자 프로그램에서 커널로 전환할 때, 이 모든 아이템들은 커널의 스택 상에 push된다.
  4. 예외 핸들러커널 모드에서 돌아가는데, 이것은 이들이 모든 시스템 자원에 완전 히 접근할 수 있다는 것을 의미한다.

핸들러는 예외처리로 인해 발생된 이벤트를 처리한 후,

경우에 따라서는 인터럽트에서 원래 위치로 복귀라는 특별한 인스트럭션을 실행시켜서 인터럽트에 의해 중단된 프로그램으로 돌아간다.

 

이 인스트럭션은 원래 프로세서의 제어 상태와 데이터 레지스터 상태를 스택으로 터 pop하여 돌려주고,

만일 예외가 프로그램을 종료했다면 상태를 사용자 모드로 되돌리고, 제어를 중단되 었던 프로그램으로 리턴해준다.

 

예외의 종류

예외 상황은 네 가지 종류로 구분할 수 있다.

 

  1. 인터럽트 (Interrupts)
    프로세스 외부에 있는 입출력 디바이스로부터의 시그널 결과다.
    하드웨어 인터럽트는 비동기적 (특정 인스트럭션을 실행해서 발생한 것이 아니 라는 의미)이다.
    (하드웨어 인터럽트를 위한 예외 핸들러 는 종종 인터럽트 핸들러 라고 부른다.)

    네트워크 어댑터, 디스크 컨트롤러, 타이머 칩 같은 입출력 디바이스들은 프로세서 칩의 핀에 시그널을 보내서 인터럽트를 발생시키고, 인터럽트를 발생시킨 디바이스를 식별하는 예외번호를 시스템 버스에 보낸다.
    현재 인스트럭션이 실행을 완료한 후에, 프로세서는 인터럽트 핀이 high로 올라갔다는 것을 발견하고 시스템 버스에서 예외 번호를 읽으며, 적절한 인터럽트 핸들러를 호출한다.

    핸들러가 리턴할 때, 제어를 다음 인스트럭션으로 돌려준다. (인터럽트가 발생하 지 않았던 것처럼 프로그램이 계속해서 실행)

    나머지 예외 종류들은 현재 인스트럭션을 실행한 결과로 동기적으로 일어난다.
    이것을 오류 인스트럭션(faulting instruction) 이라고 부른다.

  2. 트랩과 시스템 콜 (Traps and System Calls)
    트랩은 의도적인 예외상황으로, 어떤 인스트럭션을 실행한 결과로 발생한다.
    인터럽트 핸들러 와 마찬가지로 트랩 핸들러 는 제어를 다음 인스트럭션으로 리턴한다.

    트랩의 가장 중요한 사용처는 시스템 콜이다.
    커널에게 특정 서비스를 요청하기 위해서 프로세서는 특별한 n 인스트럭션 을 제공 하며,
    이들은 사용자 프로그램이 서비스 n 을 요청하고자 할 때 사용할 수 있는 인스트럭션이다.
    syscall 인스트럭션을 실행하면 트랩이 인자들을 해독하고 적절한 커널 루틴을 호출하는 예외 핸들러 로 가게 한다.

    시스템 콜은 프로그래머 입장에서 함수 호출과 동일하지만, 실제 구현은 매우 다르다.
    이는 커널 모드에서 돌아가며, 이로 인해 커널 내에서 정의된 스택에 접근하고 특권을 가진 인스트럭션을 실행할 수 있도록 해준다.

  3. 오류 (Faults)
    fault는 error가 아니다.
    fault는 핸들러정정할 수 있을 가능성이 있는 에러 조건으로부터 발생한다.
    fault가 발생하면 프로세서는 제어를 fault 핸들러 로 이동해준다.

    만일 핸들러가 에러 조건을 정정할 수 있다면, fault를 발생시킨 인스트럭션으로 제어 를 돌려주어 거기서부터 재실행한다.
    그렇지 않다면, 핸들러는 커널 내부의 abort 루틴으로 리턴해서 fault를 발생시킨 응용프로그램을 종료한다.

    fault의 고전적인 예는 페이지 오류 예외로,
    인스트럭션이 가상메모리 테이블을 참조했을 때 대응되는 실제 메모리 page가 존재 하지 않는 상황이며,
    따라서 디스크에서 가져와야할 때 발생한다. (페이지 : 가상메모리의 연속적인 블록 (대개 4KB))
    페이지 fault 핸들러 는 디스크에서 적절한 페이지를 로드해서 fault를 발생시킨 인스트럭션으로 제어를 넘겨준다.
    이 인스트럭션이 다시 실행될 때 적절한 페이지는 메모리에 있게 되므로 인스트럭션 은 오류를 발생시키지 않고 완료된다.

  4. 중단 (Aborts)
    중단은 대개 DRAM 이나 SRAM 이 고장날 때 발생하는 패리티 에러와
    하드웨어같은 복구할 수 없는 치명적인 에러에서 발생한다.
    중단 핸들러는 절대로 응용프로그램으로 제어를 리턴하지 않는다.

 

리눅스에서의 예외상황

보다 구체적인 설명을 돕기 위해 x86-64 시스템에서 정의된 일부 예외를 살펴보자.

 

 

모두 256개의 서로 다른 예외 종류들이 있다.

0에서 31까지의 숫자는 인텔 아키텍처에서 정의된 예외들이기 때문에 모든 x86-64 시스템에서 동일하다.

32에서 255까지의 숫자들은 운영체제에서 정의된 인터럽트와 트랩에 대응된다.

 

  1. Divide error
    0으로 나누려고 할 때나 나눗셈 인스트럭션의 결과가 목적지 오퍼랜드에 비해 너 무 큰 경우에 발생한다.
    Unix는 이를 복구하려고 하지 않으며 대신 프로그램을 중단한다.
    리눅스 쉘은 대개 나누기 에러를 “부동소수 예외(Floating exceptions)”로 보고 한다.

  2. General protection fault
    대개 프로그램이 가상메모리의 정의되지 않은 영역을 참조하거나,
    프로그램이 read-only 텍스트 세그먼트에 쓰려고 할 때 발생한다.
    리눅스는 이를 복구하려고 하지 않고 프로그램을 중단하며, “세그먼트 오류(Segmentation faults)” 로 알려준다.

  3. Page fault
    오류가 발생 인스트럭션이 다시 시작하는 예외 (…?)
    핸들러는 필요한 디스크의 가상메모리의 해당 페이지를 물리메모리의 페이지로 맵핑하고,
    그 후 오류 인스트럭션을 다시 시작한다.

  4. Machine check
    오류 인스트럭션을 실행하는 동안에 검출된 치명적인 하드웨어 에러의 결과다.
    절대로 제어를 응용프로그램에게 돌려주지 않는다.

 

리눅스 시스템 콜

 

리눅스의 각 시스템 콜은 커널 점프 테이블 (예외 테이블과는 다름) 의 offset에 대응되는 유일한 정수를 갖는다.

 

C 프로그램은 syscall 함수를 사용해서 직접 시스템 콜을 호출할 수 있으나,

표준 C 라이브러리는 대부분의 시스템 콜에 대해 wrapper 함수를 제공한다.

이들은 인자들은 패키징하고, 커널을 적절한 시스템 콜 인스트럭션으로 트랩을 걸고, 호출하는 프로그램으로 시스템 콜의 리턴 상태를 전달한다.

 

x86-64 시스템에서 시스템 콜은 syscall 이라는 트랩 인스트럭션을 통해서 제공된다.

리눅스 시스템 콜에 전달되는 모든 인자들은 스택보다는 범용 레지스터를 통해 이루어진다.

관습적으로 %rax 는 시스템 콜 번호를 보관하며, %rdi , %rsi , %rdx , %r10 , %r8 , %r9 에 최대 여섯 개의 인자들을 보관할 수 있다.

시스템 콜에서 리턴될 때, %rcx , %r11 은 값들이 지워지고, %rax 는 리턴 값을 보관한다.

 

write와 exit 시스템 콜을 직접 호출하는 코드
write와 exit 시스템 콜을 직접 호출하는 코드의 어셈블리 버전

 

 

프로세스

예외 상황은 프로세스 개념을 운영체제 커널이 제공할 수 있게 하는 기본 구성 블록이다.

프로그램을 실행하면 단 하나만 실행되며 프로세서와 메모리를 독점하는 것처럼 보이지만,

이러한 착각은 프로세스라는 개념에 의해서 이루어진다.

 

프로세스의 고전적인 정의는 실행 프로그램의 인스턴스이다.

시스템 내의 각 프로그램은 어떤 프로세스의 문맥(Context)에서 돌아간다.

문맥은 프로그램이 정확하게 돌아가기 위해서 필요한 상태로 구성된다.

 

논리적인 제어 흐름

디버거를 사용한다면, 프로그램 내의 인스트럭션들에게 일련의 PC값들이 대응된다는 것을 볼 수 있다.

이러한 PC값들의 배열을 논리적 제어흐름  논리흐름이라고 부른다.

 

 

위와 같이 세 개의 프로세스를 실행하는 시스템에 대해서

이 프로세서의 하나의 물리적 제어 흐름은 각 프로세스에 대해서 한 개씩 세 개의 논리 흐름으로 나누어진다.

각 수직선은 어떤 프로세스에 대한 논리적인 흐름의 일부를 나타낸다.

 

요점은 하나의 프로세서를 사용해서 여러 프로세스들이 교대로 돌아간다는 점이다.

각 프로세스는 자신의 흐름의 일부분을 실행하고 나서

다른 프로세스들로 순서를 바 꾸어 실행하는 동안 일시적으로 정지된다.

 

동시성 흐름

논리 흐름은 컴퓨터 시스템 내에서 여러 가지 다른 형태를 갖는다.

(예외 핸들러, 프로세스, 시그널 핸들러, 스레드, 자바 프로세스 등)

 

자신의 실행시간이 다른 흐름과 겹치는 논리흐름을 동시성 흐름이라고 부르며, 이 두 흐름은 동시에 실행한다.

(실행 시작 시점과는 연관 없이 같이 실행되는 모든 순간을 말함)

 

공동으로 실행되는 흐름의 일반적인 현상이 동시성이라고 알려져 있고,

프로세스가 다른 프로세스와 교대로 실행된다는 개념은 멀티태스킹이라고 알려져 있다.

한 프로세스가 자신의 흐름 일부를 실행하는데 매 시간 주기를 타임 슬라이스라고 부른다.

(위의 8.12 그림에서 프로세스 A에 대한 흐름은 2개의 타임 슬라이스로 구성된다.)

 

동시성 흐름에 대한 개념은 프로세스 코어나 컴퓨터의 개수와는 무관하다.

두 흐름이 시간상으로 중첩되면, 동일한 프로세스에서 돌아가고 있더라도 동시적이다.

 

병렬 흐름은 두 개의 흐름이 서로 다른 프로세스 코어나 컴퓨터에서 동시에 돌아가는 것이다.

 

사적 주소공간

n비트 주소를 갖는 머신에서, 주소공간은 2^n의 가능한 주소들을 갖는다.

프로세스는 각 프로그램에 자신만의 사적 주소공간을 제공한다.

이 공간의 특정 주소에 연결된 메모리의 바이트는 일반적으로 다른 프로세스에 의해서 읽히거나 쓰일 수 없다는 의미다.

 

x86-64 리눅스 프로세스에 대한 주소공간의 구조

 

주소 공간의 아랫부분은 일반적인 코드, 데이터, 힙, 스택 세그먼트를 갖는 사용자 프로그램을 위해 예약된다.

코드 세그먼트는 항상 주소 0x400000 에서 시작한다.

주소 공간의 윗부분은 커널을 위해 예약되어 있다.

이 부분은 커널이 프로세스를 대신해서 인스트럭션을 실행할 때 사용하는 코드, 데이터, 스택을 포함하고 있다. (응용이 시스템 콜을 실행할 때)

 

사용자 및 커널 모드

운영체제가 완벽한 프로세스 추상화를 제공하기 위해서

프로세서는 응용프로그램이 접근할 수 있는 주소 공간 뿐만이 아니라

응용이 실행할 수 있는 인스트럭션을 제한하는 메커니즘을 제공해야 한다.

 

프로세서는 대개 이러한 작업을 지원하기 위해 프로세스가 현재 가지고 있는 특권을 저장하는 일부 제어 레지스터로 모드 비트(mode bit)를 제공한다.

모드 비트가 설정되면 프로세스는 커널 모드(kernel mode) (슈퍼바이저 모드 (supervisor mode))로 동작한다.

커널 모드에서 돌고 있는 프로세스는 인스트럭션 집합의 어떤 인스트럭션도 실행할 수 있으며,

시스템 내의 어떤 메모리 위치도 접근할 수 있다.

 

모드 비트가 설정되지 않을 때 프로세스는 사용자 모드(user mode)에서 돌고 있다.

사용자 모드에서는 프로세서를 멈추거나, 모드 비트를 변경하거나, 입출력 연산을 초기화하는 특수 인스트럭션을 실행할 수 없다.

커널 영역에 있는 코드나 데이터를 직접 참조할 수도 없어서 시스템 콜을 통해 간접적으로 접근해야 한다.

 

응용 프로세스는 처음에 사용자 모드에 있게 된다.

커널모드로 진입하는 유일한 방법은 인터럽트, Fault, 트랩 시스템 콜 같은 예외를 통해서다.

예외가 발생해서 제어가 예외 핸들러 로 넘어가면, 프로세서는 사용자 모드에서 커널 모드로 변경한다.

핸들러는 커널 모드에서 돌아가고, 제어가 응용으로 돌아오면 프로세서는 사용자 모드로 다시 변경한다.

 

리눅스는 /proc 파일 시스템 이라고 하는 진보된 메커니즘을 제공하는데,

이것은 사용자 모드 프로세스가 커널 데이터 구조의 내용에 접근할 수 있게 해준다.

사용자 프로그램이 읽을 수 있는 텍스트 파일의 계층 구조로 많은 커널 데이터 구조의 내용을 내보낸다.

예를 들어 CPU 타입( /proc/cpuinfo ), 특정 프로세스가 용하는 메모리 세그먼트 ( /proc/${process_id}/maps ) 같은 일반적인 시스템 특성을 보여준다.

 

문맥 전환

운영체제는 문맥 전환(Context switch) 라고 알려진 예외적인 제어흐름의 상위수준 형태를 사용해서 멀티태스킹을 구현한다.

문맥 전환은 저수준 예외 메커니즘 위에 구축된다.

 

커널은 각 프로세스마다 문맥을 유지한다.

문맥은 범용 레지스터, 부동소수점 레지스터, 프로그램 카운터, 사용자 스택, 상태 레지스터, 커널 스택, 여러가지 커널 자료구조 같은 주소공간을 규정하는 페이지 테이블, 현재 프로세스에 관한 정보를 가지고 있는 프로세스 테이블, 프로세스가 오픈한 파일에 관한 정보를 저장하는 파일 테이블 같은 객체들의 값들로 구성된다.

 

커널은 프로세스가 실행되는 동안의 어떤 시점에 현재 프로세스를 일시 정지하고

이전에 일시 정지된 프로세스를 다시 시작할 것을 결정할 수 있다.

이 결정은 스케줄링이라고 하며, 스케줄러라고 하는 커널 내부의 코드에 의해 처리된다.

커널이 실행할 새 프로세스를 선택할 때, 커널이 그 프로세스를 스케줄했다고 말한다.

 

커널이 실행할 새 프로세스를 스케줄한 후에 현재 프로세스를 일시 정지하는 것을 문맥 전환(Context switch)라고 하며,

이 메커니즘을 사용해서 새로운 프로세스로 제어를 이동한다.

  1. 현재 프로세스의 컨텍스트를 저장
  2. 이전에 일시 정지된 프로세스의 저장된 컨텍스트 복원
  3. 제어를 새롭게 복원된 프로세스로 전달

문맥 전환은 커널이 사용자를 대신해서 시스템 콜을 실행하고 있을 때 일어날 수 있다.

시스템 콜이 어떤 이벤트의 발생을 기다리기 때문에 Block 된다면, 커널은 현재 프로세스를 sleep 시키고 다른 프로세스로 전환한다.

( read 시스템 콜이 디스크 접근을 요구하면 커널은 문맥 전환을 할 수 있으며, 디스크 로부터의 데이터를 기다리는 대신 다른 프로세스를 돌릴 수 있다.)

Block되지 않았다고 하더라도 커널은 시스템 콜을 호출했던 프로세스로 제어를 돌려 주는 대신에 문맥 전환을 수행할 수 있다.

 

또한 문맥 전환은 인터럽트의 결과로도 발생할 수 있다.

모든 시스템은 대개 1ms 또는 10ms마다 주기적인 타이머 인터럽트를 생성하는 어떤 메커니즘을 가지고 있다.

타이머 인터럽트가 일어날 때마다 커널은 현재 프로세스가 충분히 오래 실행되었다 고 판단하여 새로운 프로세스로 전환할 것을 결정할 수 있다.

 

 

위 그림은 두 개의 프로세스 사이에서 문맥 전환 사례를 보여준다.

read 시스템 콜을 실행해서 커널에 트랩을 걸 때까지는 사용자 모드로 돌고 있다.

 

커널의 트랩 핸들러  디스크 컨트롤러 에게 DMA 전송을 요청하며,

디스크 컨트롤러 가 데이터를 디스크에서 메모리로 전송을 완료한 후에 프로세서에 인터럽트를 걸도록 제어한다.

 

디스크는 데이터를 선입하는 데 비교적 긴 시간이 걸릴 것이며,

그래서 중간에 기다리고 아무것도 안 하는 대신에 커널은 프로세스 B로 문맥 전환을 수행한다.

전환하기 전에 커널은 프로세스 A 를 대신해서 사용자 모드에서 인스트럭션들을 실행하고 있었다는 점에 주의해야 한다.

 

전환의 첫 단계로 커널은 프로세스 A 를 대신해서 커널 모드에서 인스트럭션을 수행하고 있다.

그 후, 어떤 시점에서 프로세스 B 를 대신해서 인스트럭션의 실행을 시작한다.

전환 후에 커널은 프로세스 B 를 대신해서 사용자 모드에서 인스트럭션들을 실행하고 있다.

프로세스 B 는 그 후에 디스크가 인터럽트를 보내서 데이터가 디스크에서 메모리로 전송되었다고 알려줄 때까지 사용자 모드에서 잠시 동안 동작한다.

커널은 프로세스 B충분히 오랫동안 동작했다고 인식하고, 제어를 프로세스 Aread 시스템 콜 이후의 인스트럭션 위치로 리턴하는 문맥 전환을 수행한다.

 

 

『Computer Systems: A Programmer's Perspective』 책을 읽고 정리한 글입니다.
검색을 허용하지 않고, 수익을 창출하지 않습니다.
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.