SP - 1.2 Exception Details & Process

hyeok's Log·2022년 3월 22일
2

SystemProgramming

목록 보기
2/29
post-thumbnail

Async Exception vs Sync Exception

  동기식과 비동기식 예외를 구분하는 가장 좋은 비유는 아래와 같다.

동기식 예외는, 햄버거를 주문해놓고 카운터에서 가만히 기다리는 것이고, 비동기식 예외는 햄버거를 주문해놓고 다른 일(유튜브 보기)을 하면서 기다리는 것!

ex) 따라서, 지난 포스팅에서 이야기한 fread 예시(I/O를 요청한 상황)는, 디스크에서 데이터를 읽어오는 것이 오래 걸리므로, "나(CPU) 잠시 다른 일 하고 있을테니까 데이터 읽어와~"하는 것이다. 그리고, "데이터가 도착하면 나한테 인터럽트 걸어줘!" 이러는 것이다.


Async Exception - Interrupt

  • 프로세서 외부의 이벤트(I/O)에 의해 발생한다.

  • CPU에는 Interrupt Controller가 있고, 거기엔 Interrupt Pin이란 것이 있다.

  • 하드웨어에서 전기적 신호인 Interrupt를 CPU의 Controller에 보내면, Pin이 HIGH가 된다.

    • Pin이 HIGH가 되는 것이 곧 Interrupt가 걸리는 것!
    • 이 순간, CPU는 다른 일을 하고 있는 상태이다.
  • CPU는 'Fetch & PC++ - Decode - Execute - WriteBack' Control Flow를 흐른다.

    • 이때, CPU는 항상 Fetch를 하면서 Interrupt Pin의 상태를 확인한다. ★★★
      • Pin이 HIGH이면, Interrupt Handler를 수행한다.

        인터럽트가, CPU의 흐름에서 언제 꽂힐지 모르므로 비동기식 예외라고 하는 것이다.

Timer Interrupt

  • 대표적인 인터럽트 예시이다.
  • 매우 짧은 마이크로 세컨드(일반적으로 Duration 10ms)가 지나면, CPU '외부'의 Timer Chip이 CPU에 인터럽트를 걸어준다.
  • Process Context Switch가 바로 Timer Interrupt가 걸리는 것으로, 비동기식 처리이다.
    • 따라서, Timer Interrupt를 걸고 다른 일을 수행하는 것이다.
      • 이들이 중첩되는 것!
    • 추후 이는 더 자세히 다룰 것이다.

I/O Interrupt

  • 역시나 대표적인 인터럽트 예시로, 지난 포스팅의 fread 상황이 여기에 해당한다.
  • 'Ctrl+C'를 키보드로 누르는 상황도 해당한다. (복사가 아님)
  • 키보드 하나를 쭉 누르면 삐삐 소리가 나는 상황도 인터럽트에 해당함.
  • 네트워크 패킷이 도착하는 상황도 여기에 해당한다.
  • 디스크에서 데이터를 긁어오는 상황도 여기에 해당한다. ex) fread

I/O 인터럽트는, CPU 외부의 장치(SSD, Disk(HDD), Network, Keyboard)로부터 데이터를 주고받을때 발생한다.

읽어오는 과정이 외부 장치다 보니 오래걸리고, 그러므로 CPU가 시간이 아까워서 다른일을 하는 것이다.

~> 프로세스 A를 Block시켜놓고 프로세스 B로 Switch하는 이유 : 프로세스 A가 외부 데이터를 받지 않고서는 이후의 명령을 수행할 수 없으므로. ★★★


Sync Exception - Trap, Fault, Abort

비동기식 예외는 프로그램의 '명령이 외부 장치에 무언가를 요청'해서 걸린다.
반대로, 동기식 예외는 프로그램 '명령의 결과'로서 일어난다.

Trap

  • 의도적인(Intentional) 동기식 예외이다.

  • System Call, Breakpoint Trap, 그리고 몇가지 특수 명령들에 의해 Trap이 발생한다.

  • System Call : 프로그램에서 의도적으로 Kernel Code를 수행하고자 할 때 호출하는 것!

    • fopen 같은 명령이 바로 이 System Call에 해당한다. (fread는 인터럽트)
      • 사용자가 fopen을 명령하면, 커널에 있는 함수를 수행하게 된다.
커널 내에 있는 System Call의 어셈블리 코드 (Kernel Code)
00000000000e5d70 <__open>:
...
e5d79: 	b8 02 00 00 00 		mov $0x2,%eax
e5d7e: 	0f 05 				syscall 		# Return value는 %rax 레지스터로!
e5d80: 	48 3d 01 f0 ff ff 	cmp $0xfffffffffffff001,%rax 
...
e5dfa: c3 retq

~> 이 명령이 수행되면 제어권이 커널로 넘어가게(예외의 정의) 된다.
~> 시스템 콜로 인한 커널 코드를 수행하고 나면, 시스템 콜을 야기한 I_curr의 다음인 I_next를 수행하게 된다.

이때, System Call 시, CPU는 다른 프로세스를 수행하지 않고 잠시 멈추어 커널 코드가 수행되는 것을 기다리고, 그 다음 I_next를 수행하게 된다. (그래서 동기식!)


Fault

  • 비-의도적인(Unintentional) 동기식 예외이다.

  • Potentially Recoverable하다. 즉, 다시 돌아올수도, 아니면 끝날수도 있다.

  • Page Fault, Protection Fault(Segmentation Fault), Floating Point Exception 등이 해당된다.

  • Page Fault는 Recoverable, Protection Fault는 Unrecoverable하다.

    • 이때, Recover는 오로지 I_curr로의 복귀이다. (실패 명령 재실행)
    • 이때, Unrecover는 Abort이다. (바로 종료)

Fault와 Trap의 차이는, Return할 때 I_curr로 오느냐, I_next로 오느냐이다.

  • 지난 포스팅에서 길게 설명한 Page Fault가 바로 대표적인 Recoverable Fault로, Fetch하려했는데, DRAM이 아니라 Disk에서 가져와야해서 잠시 멈추고, Disk에서 DRAM으로 데이터를 옮겨놓고, 다시 I_curr로 돌아와 LOAD를 수행해 DRAM에서 Fetch하는 것이다.

    • Question) 아니, I/O에서 fread로 디스크에 저장된 파일의 데이터 끌어오는 인터럽트 예외에서는 CPU가 성급해서 기다리지 않고 다른 일을 하는데, 왜 LOAD할 때 Page Fault가 나면 똑같이 디스크 접근해서 데이터 가져오는데, 이때는 왜 CPU가 멈추고 기다리는 거에요?

      • Answer) 허무하겠지만, System Programming 관점에선 별 이유가 없다. OS 구조를 알면, 이 차이를 알 수 있는데, 현재 수준에서는 별다른 이유를 설명할 수 없다.

        즉, 그냥 받아들여야한다. fread로 디스크에서 파일 읽어올 때는 비동기식 인터럽트가, load 시 Page Fault 발생해서 디스크에서 파일 읽어올 때는 동기식 폴트가 발생한다.


Abort

  • 비-의도적이고 회생불가능한(Unrecoverable) HW 예외!
  • 'HW Failure' like 패리티 에러, 기계어 에러 등
  • 무조건 현재 프로세스를 종료(Abort)한다.


Example

System Calls (almost Trap)

  x86 프로세서 기반 시스템들에는 아래와 같이 System Call ID들이 있다.

0 		read 		Read file
1 		write		Write file
2 		open 		Open file
3 		close 		Close file
4 		stat 		Get info about file
57 		fork 		Create process
59 		execve 		Execute a program
60 		_exit 		Terminate process
62 		kill 		Send signal to process

~> 얘네들이 System Call들이다. 이런게 있구나~하고 넘어가자. 어차피 이후에 다 자세히 다루고 사용할 것이다.


Page Fault

int arr[5000];
int main (void) {
	arr[500] = 27;		// arr[500] 주소는 OS 가상 메모리 상에서 DRAM에 맵핑이 되어있을수도, 
}						// 아닐수도 있는데, 여기선 아니라고 가정되어 있는 것임!!!!

~> "어? DRAM에 없네? 그럼 Disk에서 잠깐 긁어서 DRAM으로 옮겨놓을테니까, 너(CPU) 잠시 가만히 기달려!"


Segmentation Fault

  • OS가 프로세스에게 SIGSEGV Signal을 보내어 이 예외가 발생한다. (Signal은 추후 다룸)
  • Seg Fault는 Protection Fault로, Page Fault와 다르게 바로 Abort한다!
    (참고로, Exception 범주에서의 Abort는 HW Failure와 관련되어야 해당한다. 여기서 말하는 Abort와는 다르다.)
int arr[5000];
int main (void) {
	arr[5001] = 27;		// arr[5001]은 미지의 주소!
}

~> "야, 야, Process;; 여기는 도대체 어디를 말하는거야? 너 종료해!!"

  • 프로그램이 메모리에 적재되어 프로세스가 되면, 프로세스는 메모리 공간에 Code(소스코드), Data(전역), Heap(동적할당), Stack(지역, 서브루틴, 반환주소 등) 영역으로 추상화된다.
    • 이때, arr[5000]은 Data 영역에 잡힌다. (Global이므로)
    • 이때, "arr[5001] = 27;"이란 명령에 대해, C언어는 Boundary Check를 컴파일 타임에 수행하지 않으므로 정상적으로 컴파일된다.
    • 반면, 컴파일러와 다르게, OS는 항상 가상 메모리를 접근할 때 '접근 가능한 메모리인지'를 체크한다.
    • OS가 보았을 때, 접근하려고 하는 메모리 주소가, Page Table에 맵핑된적이 없다는 사실을 확인하게 되면, SIGSEGV라는 Signal을 프로세스에 보내게 되는 것이다. ★★
    • 프로세스는 항상 수행 전에 Signal을 체크하는데, "어라랏? SIGSEGV라고? 알겠어!"라고 한 다음, Abort하는 것이다. ★★

※ OS의 Virtual Memory : 가상으로 넓은 페이지(Page Table, Mapping Table)를 추상화한다. 여기는 2차원 배열 형태로 되어 있다. 각 칸들엔 Flag가 있다. 그 칸이 DRAM과 맵핑되었는지, 디스크(SSD)와 맵핑되었는지를 이 Flag에 기록한다. 이 값을 보고, Page Fault나 Seg Fault를 발생시키게 되는 것이다.


Process

  프로세스는 프로그램이 주기억장치(Volatile, DRAM)에 적재되었을 때 불리는 이름이라고 했다. 좀 더 엄밀한 정의는 아래와 같다.

Process : 수행되고 있는 프로그램의 인스턴스

  • 프로세스는 프로그램에게 두 가지 중요한 추상화를 제공한다.
    • Logical Control Flow : CPU 관점에서 본 프로세스

      • 여러 프로그램을 동시에 돌릴 때, 각 프로그램은 마치 CPU를 혼자서 독점해 쓰는 것처럼 보인다. (실제론 공유하는데 말이다)
      • Context Switching이 이를 가능케 한다. (Timer Interrupt에서 다루었던 Process Context Switch를 말함)

    • Private Address Space : 메모리 관점에서 본 프로세스

      • 각 프로그램은 마치 메모리를 혼자서 독점해 쓰는 것처럼 보인다. (역시나, 실제론 공유하는데 말이다)
      • OS의 Virtual Memory가 이를 가능케 한다.

  • Process는 PID(Process ID)라는, 마치 주민등록번호와 같은 고유 ID로 식별한다.

Illusion

  마치 모든 프로세스가 CPU와 메모리를 독점해서 각각 동시에 프로세싱하는 것처럼 보인다.

  • CPU는 하나인데, 마치 컴퓨터가 각 프로세스마다 CPU를 두고 동시에 수행하는 것처럼 보인다.

Reality

  하지만, 실상은, 하나의 CPU를 두고 Time-Sharing(시분할)을 하고 있다. (과거의 단일 CPU 시대를 기준으로 설명한다고 지난 포스팅에서 말한 바 있다)

  • 각 프로세스에게 예를 들어 약 10ms 씩 할당한다.

    • 10ms마다 하나의 프로세스를 수행하고, 10ms 지나면 다음 프로세스 수행하고,..

    • 이런 과정이 매우매우 빠르게 전환되고 반복된다.

    • 이를 Timer Exception(인터럽트의 예시)이라 한다고 했다.

    • 이때, 다음 프로세스로 넘어갈때, 'Snapshot'을 찍어서 Saved Registers에 기록해둔다.

      • 돌아오려면 정보를 기억하고 있어야하니까!!
      • Timer Interrupt가 걸리면 다음으로 Switch하는데, 나중에 다시 프로세스로 돌아왔을 때 멈췄던 시점 이후부터 실행하기 위해 그 시점에서의 정보를 Set으로 하여 Saved Registers에 기억해두는 것이다.

1) 단일 프로세서가 복수의 프로세스를 Concurrent하게 수행한다.
~> Multitasking이라고 한다.
~> 메모리 공간은 'OS Virtual Memory'가 관리한다.
~> 다음 Process로 넘어가기 전에 현재 Process가 사용하던 Register Value들을 특정 공간(Saved Registers)에 저장해두고 넘어간다. 다시 돌아오기 위해서! (스냅샷 개념)

2) OS 스케쥴러가 다음 프로세스를 스케쥴한다. (스케쥴 알고리즘 존재)

3) 해당 (다음) 프로세스에 대한 Saved Registers 값들을 Load하고, 그에 해당하는 메모리 주소 공간으로 이동해 해당 프로세스를 수행한다.


Processor Today

  현대의 프로세서는 'Multicore'가 대세이다.

  • 단일 칩에 여러개의 CPU를 둔다.
  • 이 여러 CPU들이 메인 메모리를 공유한다.
  • 각 CPU가 각자가 담당하는 프로세스를 위의 방식처럼 Switching해가며 수행한다.
    • 그래서 물리적으로 더 빨라질수밖에 없는것!
  • OS 스케쥴러가 이 스케쥴링 작업을 담당한다.

  금일 포스팅은 여기까지 하겠다.

0개의 댓글