9. Exceptional Control Flow(흐름 제어)
이번에 배울 예외적인 흐름 제어는 일반적인 애플리케이션에서 처리하지 못하는 예외들을 OS로 넘겨 처리하는 기법을 말한다.
우선 이전에 배운 것들을 다시 상기해보자.
우리가 지금까지 배운 컴퓨터 시스템 안에서의 상호작용
- Assembly language : 명령어 사이의 상호작용
- Procedure calls : 함수 사이의 상호작용
- Linking : 오브젝트 파일 사이의 상호작용
Control Flow(흐름 제어)란?
일반적인 컴퓨터는 프로그램이 시작하면 명령어들이 순차적으로 진행이 되고 끝나게 된다.
이러한 과정 속에서 CPU는 명령어를 한 번에 한 개씩 수행할 수 있게 되는데, 이 순서를 CPU의 Control Flow라고 한다.
만약 Control Flow가 제대로 동작하지 않는다면, 명령어들이 비순차적으로 난잡하게 수행될 수 있다.
Control Flow가 바뀌는 요인들
보통 흐름제어 또는 제어 흐름이 바뀌는 경우는 우리도 많이 겪어봤을 것이다.
예를 들어 Jump, branch 와 같은 분기 명령어를 사용한다던지 함수 call, return 명령어를 사용해 정해진 곳으로 pc가 이동할 때, 우리는 "프로그램의 상태가 변화하였다" 즉, 흐름 제어가 바뀌었다 라고 한다.
프로그램의 상태 변경이 어려운 상황
그런데, 프로그램이 진행을 할 때 상태 변경이 어려운 상황이 있는데, 그런 경우는 다음과 같다.
- 디스크 또는 네트워크 어댑터에서 데이터를 가져올 경우 (System 자원을 사용할 때)
- 0으로 나누는 명령어의 경우 (대표적인 예외 처리 상황)
- 사용자가 Ctrl + c 버튼을 눌렀을 경우 (이 경우 시그널이라고 하는데, 다음 장에서 좀 더 자세히 살펴본다.)
- 시스템 타이머가 만료된 경우
-> 이런 경우, 프로그램 자체가 할 수 있는 범위를 넘어서게 된다.
해법
따라서 애플리케이션은 이와 같은 상황을 간접적으로 처리하기 위해 상황 발생 시 OS에게 제어권을 넘겨 처리하게 한다.
예외처리 상황에서의 제어 흐름
위에서 언급한 해법을 도식화한 그림이다.
순서는 다음과 같다.
- I_current에서 exception event 발생 -> OS로 제어 전달
- OS에서는 exception handler를 통해 예외 처리
- 이후 OS는 유저 프로그램에 다시 제어를 넘기는데, 넘기는 위치는 명령어에 따라 다르며 3가지 경로가 있다.
(왔던 곳으로 다시가기, 왔던 곳 그다음 줄로 가기, 그냥 종료하기)
왜 Exception이 필요한가?
- Application은 직접적으로 I/O device를 사용할 수 없기 때문에 (보안 문제) I/O로 접근할 수 있는 경로가 필요하다.
- I/O device는 하나의 표준화된 인터페이스 제공이 필요하다.
(application 단계에서 건드리면 여러 방법이 있을 수 있기 때문에 관리할 수 있도록 하나로 통합한다는 의미이고, 이 방법이 Exception이라고 한다.) - I/O device는 CPU에게 언제든지 인터럽트를 발생시킬 수 있다. (인터럽트 = 예외처리라고 보면되는데, 여기서 "언제든지 발생"한다는 건 CPU가 Exception이 나올 때까지 기다리지 않고 다른 일을 하고 있다는 것을 의미한다)
이게 이해하기 어려울 수 있는데, Exception 덕분에 CPU의 효율을 높일 수 있다고 이해하자. - OS는 마치 application이 많은 자원을 갖고 있는 것처럼 보이게한다. (이 또한 Exception 덕분에 가능한 일)
- Exception은 OS로 control을 넘길 수 있는 유일한 방법이다.
ARM에서의 Exception의 종류들
- Reset : 초기화 이벤트
- Undefined Instruction : 정의되지 않은 명령어
- SWI : software interrupt(os 쪽으로 보내기 위함)
- Aborts : 세그먼트 또는 페이지 폴트 발생 시 중단 (종류 : data aborts, prefetch aborts)
- Interrupt : 외부에 의한 비동기적 예외 (IRQ : 일반적, FIQ : 빠른 처리)
Exception을 나누는 기준
보통 Exception은 Instruction이 동기인지, 비동기인지에 따라 나뉠 수 있는데 그들의 특징은 다음과 같다.
Synchronous Exceptions (동기)
동기식 Exception은 발생하는 이벤트가 예측 불가능하지 않고, 기준 또는 시간에 맞추어 실행시키는 것을 의미한다.
따라서 보통은 명령어 실행의 결과로 발생하는 경우가 대부분이다.
추가로 이 경우 Trap, Fault가 존재한다.
Trap : 의도적으로 조건을 걸어, 상황에 맞게 처리하도록 매핑한다. 다음 명령어를 리턴하게 된다.
ex) SWI, breakpoint trap, undefined instruction
Fault : 비의도적이지만 회복이 될 수 있는 이벤트이다. 이 경우 리턴 값은 현재 명령어가 될 수도, 다음 명령어가 될 수도 있다.
ex) page fault, protection fault
Asynchronous Exceptions (비동기)
비동기 Exception은 프로세서 외부의 이벤트에 의해 발생하는 것을 의미하는데
외부의 이벤트는 정해진 기준이 따로 없어서 예측이 불가능하다. 예시로는 I/O interrupt, Reset 등이 있다.
추가로 비동기 예외가 발생하면 인터럽트 핀을 설정한다.
Fault 사례
Page Fault (Prefetch Abort)
일반적으로 물리 메모리의 크기 단위를 프레임이라고 한다면, 페이지는 가상 메모리의 크기 단위를 의미한다.
보통 데이터를 저장할 때 물리적 메모리의 공간이 부족하면, 가상 메모리를 사용하게 된다.
[가상 메모리는 RAM을 관리하는 방법 중 하나로, 각 프로그램에 실제 메모리 주소가 아닌 가상의 메모리 주소를 할당하는 방식을 말한다.]
따라서 물리 메모리와 가상 메모리는 크기 차이가 발생하게 되고, 가상 메모리에 있는 데이터(Page)가 물리 메모리에 부재하는 상황이 발생한다. 이를 Page Fault 라고 한다.
참고 : https://preamtree.tistory.com/21
이건 말로만 이해하면 그림으로 나왔을 때, 전혀 파악을 못하는 경우가 있어서 그림을 가져왔다.
위 그림과 같은 상황이 Page Fault의 예시이다.
실제로는 위 3줄이 실행이 되지만, add r1, r1, r0 줄 까지로 page가 끊기는 상황이 발생했고, cmp r4, r1을 fetch할 때, 물리 메모리에 해당 데이터가 없는 것을 발견 후 exception이 발생하게 된다.
따라서 cmp r4, r1 명령어는 물리 메모리에 저장되지 않았기 때문에 Fetch가 되지 않는 결과로 이어져버렸다.
-> 하지만 Page Fault는 OS가 처리할 수 있는 범주 내에 있기 때문에 OS는 cmp r4, r1를 가상 메모리에 저장하고, 다시 실행하면 될 것이다.
[추가] Data abort
이번엔, r2가 가리키는 주소의 메모리가 가상 메모리가 들어와 있지 않을 때의 상황이다. 이 경우도 아까와 같이 메모리에 없는 것을 넣어주면 되는 건 같으나, 리턴할 때 왔던 자리로 다시 간다. 왔던 곳에서 다시 데이터를 담아야하기 때문이다.
Segmentation Fault
r0에 #0xffffffff를 대입한 후, ldr로 접근하려는 상황이다.
#0xffffffff 이 수는 유저 공간이 아닌 커널 주소이기 때문에 ldr로 접근이 안되고 segmentation fault가 발생하게 된다.
그렇게 되면 위 그림과 같이 돌아오지 않는 회복 불가 상태가 되며 끝나게 되는데, 이렇게 되면 유저가 왜 끝났는지 모르기 때문에 시그널을 보내게 된다.
Interrupts (IRQ or FIQ)
mov r4, r0 명령어를 실행한 후, 인터럽트 예외가 발생한 상황이다.
그 때, 인터럽트 핀을 설정하고 -> 예외 핸들링을 한 후 -> 컨트롤은 다음 명령어로 향하게 된다.
Register Organization in ARM
위 그림은 ARM에는 레지스터의 여러 모드를 보여준다.
자세히 보면 모드마다 다 구조가 다른데, 이는 exception handling 을 할 때 하는 일들마다 어느정도 차이가 있을 수도 있어 상황에 맞게 효율적으로 처리하기 위함이다.
Exception Handling in ARM
이번엔 arm에서 exception handling을 어떤 방식으로 하는지 자세히 알아보자.
예외 발생 시
- cpsr을 spsr에 복사
- cpsr 비트를 조정한다.
- return 주소를 계산 후, lr_<mode>에 저장한다.
- pc 값을 vector address에 저장. (vector address는 분기(b) 명령어를 담고 있어, exception handling을 할 수 있게 한다.
예외 처리 후 돌아올 때
- spsr_<mode> 로부터 cpsr 가져오기
- lr_<mode> 로부터 pc값 가져오기
왜 빠른 FIQ 대신 IRQ를 쓰는 가?
FIQ가 들어오면, 다른 IRQ는 다 disable 된다.
때문에 실행하는 동안 IRQ가 안 걸리게 되고, 만약 FIQ 실행시간이 오래 걸리면, 다른 작업을 못하게 된다. (멀티태스킹이 안된다는 의미)
이는 버퍼 오버런, 오버플로우 등의 결과를 초래할 가능성이 있다.
따라서 FIQ는 정~말 critical 한 부분에 쓰인다.
예외 처리 후엔 어떤 일이 생기나?
- 컨트롤은 원래 자리로 돌아간다.
- 컨트롤이 원래 자리로 안돌아오는 경우도 있는데, 예를 들어 page fault 발생 시, I/O를 하기 위해 swi를 부른다거나.. 이 경우 컨트롤은 다른 프로그램으로 돌린다. → multi program
- 만약 예외처리 핸들링 시간이 길어지면, 주 작업을 일반 OS에 위임하고 일반 OS는 시스템 모드에서 작동한다.
-> Exception은 반드시 짧은 시간에 실행되어야 하기 때문이다.
Processes란?
엄밀히 말하면 program과 process는 차이가 있다. process의 정의는 "실행하는 program의 상태"라는 것인데
하는 일은 프로그램에게 2가지를 제공한다.
- Logical control flow : 프로세스를 통해 논리적 제어 흐름이 통한다.
- private virtual address space : 프로그램 관점에서는 메모리 전체를 다 쓰는 것처럼 만드는 것. (개인 가상 메모리 공간을 제공 → 메모리 스와핑)
2번이 가능한 이유?
프로세스는 interleaved (멀티태스킹) 되어 있거나, cpu가 하나가 더 있다면 나눠진 코어에서 동작하기 때문이다.
Concurrent Process
Concurrent Process는 위 그림처럼 2개의 프로세스가 하나의 시간대에 동시에 실행되는 것을 의미한다.
Context Switching이란?
멀티프로세스 환경에서 CPU가 어떤 하나의 프로세스를 실행하고 있는 상태에서
인터럽트 요청에 의해 다음 우선순위의 프로세스가 실행되어야 할 때
기존의 프로세스의 상태 또는 레지스터 값(Context)을 저장하고 CPU가 다음 프로세스를 수행하도록 새로운 프로세스의 상태 또는 레지스터 값(Context)을 교체하는 작업을 Context Switch(Context Switching)라고 한다.
문장이 너무 길어졌는데, 간단하게 기존 값 저장 후, 새로운 프로세스 교체 작업이라고 알아두자!