동기식과 비동기식 예외를 구분하는 가장 좋은 비유는 아래와 같다.
동기식 예외는, 햄버거를 주문해놓고 카운터에서 가만히 기다리는 것이고, 비동기식 예외는 햄버거를 주문해놓고 다른 일(유튜브 보기)을 하면서 기다리는 것!
ex) 따라서, 지난 포스팅에서 이야기한 fread 예시(I/O를 요청한 상황)는, 디스크에서 데이터를 읽어오는 것이 오래 걸리므로, "나(CPU) 잠시 다른 일 하고 있을테니까 데이터 읽어와~"하는 것이다. 그리고, "데이터가 도착하면 나한테 인터럽트 걸어줘!" 이러는 것이다.
프로세서 외부의 이벤트(I/O)에 의해 발생한다.
CPU에는 Interrupt Controller가 있고, 거기엔 Interrupt Pin이란 것이 있다.
하드웨어에서 전기적 신호인 Interrupt를 CPU의 Controller에 보내면, Pin이 HIGH가 된다.
CPU는 'Fetch & PC++ - Decode - Execute - WriteBack' Control Flow를 흐른다.
인터럽트가, CPU의 흐름에서 언제 꽂힐지 모르므로 비동기식 예외라고 하는 것이다.
I/O 인터럽트는, CPU 외부의 장치(SSD, Disk(HDD), Network, Keyboard)로부터 데이터를 주고받을때 발생한다.
이 읽어오는 과정이 외부 장치다 보니 오래걸리고, 그러므로 CPU가 시간이 아까워서 다른일을 하는 것이다.
~> 프로세스 A를 Block시켜놓고 프로세스 B로 Switch하는 이유 : 프로세스 A가 외부 데이터를 받지 않고서는 이후의 명령을 수행할 수 없으므로. ★★★
비동기식 예외는 프로그램의 '명령이 외부 장치에 무언가를 요청'해서 걸린다.
반대로, 동기식 예외는 프로그램 '명령의 결과'로서 일어난다.
의도적인(Intentional) 동기식 예외이다.
System Call, Breakpoint Trap, 그리고 몇가지 특수 명령들에 의해 Trap이 발생한다.
System Call : 프로그램에서 의도적으로 Kernel Code를 수행하고자 할 때 호출하는 것!
커널 내에 있는 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를 수행하게 된다. (그래서 동기식!)
비-의도적인(Unintentional) 동기식 예외이다.
Potentially Recoverable하다. 즉, 다시 돌아올수도, 아니면 끝날수도 있다.
Page Fault, Protection Fault(Segmentation Fault), Floating Point Exception 등이 해당된다.
Page Fault는 Recoverable, Protection Fault는 Unrecoverable하다.
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가 멈추고 기다리는 거에요?
즉, 그냥 받아들여야한다. fread로 디스크에서 파일 읽어올 때는 비동기식 인터럽트가, load 시 Page Fault 발생해서 디스크에서 파일 읽어올 때는 동기식 폴트가 발생한다.
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들이다. 이런게 있구나~하고 넘어가자. 어차피 이후에 다 자세히 다루고 사용할 것이다.
int arr[5000];
int main (void) {
arr[500] = 27; // arr[500] 주소는 OS 가상 메모리 상에서 DRAM에 맵핑이 되어있을수도,
} // 아닐수도 있는데, 여기선 아니라고 가정되어 있는 것임!!!!
~> "어? DRAM에 없네? 그럼 Disk에서 잠깐 긁어서 DRAM으로 옮겨놓을테니까, 너(CPU) 잠시 가만히 기달려!"
int arr[5000];
int main (void) {
arr[5001] = 27; // arr[5001]은 미지의 주소!
}
~> "야, 야, Process;; 여기는 도대체 어디를 말하는거야? 너 종료해!!"
※ OS의 Virtual Memory : 가상으로 넓은 페이지(Page Table, Mapping Table)를 추상화한다. 여기는 2차원 배열 형태로 되어 있다. 각 칸들엔 Flag가 있다. 그 칸이 DRAM과 맵핑되었는지, 디스크(SSD)와 맵핑되었는지를 이 Flag에 기록한다. 이 값을 보고, Page Fault나 Seg Fault를 발생시키게 되는 것이다.
프로세스는 프로그램이 주기억장치(Volatile, DRAM)에 적재되었을 때 불리는 이름이라고 했다. 좀 더 엄밀한 정의는 아래와 같다.
Process : 수행되고 있는 프로그램의 인스턴스
Logical Control Flow : CPU 관점에서 본 프로세스
Private Address Space : 메모리 관점에서 본 프로세스
마치 모든 프로세스가 CPU와 메모리를 독점해서 각각 동시에 프로세싱하는 것처럼 보인다.
하지만, 실상은, 하나의 CPU를 두고 Time-Sharing(시분할)을 하고 있다. (과거의 단일 CPU 시대를 기준으로 설명한다고 지난 포스팅에서 말한 바 있다)
각 프로세스에게 예를 들어 약 10ms 씩 할당한다.
10ms마다 하나의 프로세스를 수행하고, 10ms 지나면 다음 프로세스 수행하고,..
이런 과정이 매우매우 빠르게 전환되고 반복된다.
이를 Timer Exception(인터럽트의 예시)이라 한다고 했다.
이때, 다음 프로세스로 넘어갈때, 'Snapshot'을 찍어서 Saved Registers에 기록해둔다.
1) 단일 프로세서가 복수의 프로세스를 Concurrent하게 수행한다.
~> Multitasking이라고 한다.
~> 메모리 공간은 'OS Virtual Memory'가 관리한다.
~> 다음 Process로 넘어가기 전에 현재 Process가 사용하던 Register Value들을 특정 공간(Saved Registers)에 저장해두고 넘어간다. 다시 돌아오기 위해서! (스냅샷 개념)
2) OS 스케쥴러가 다음 프로세스를 스케쥴한다. (스케쥴 알고리즘 존재)
3) 해당 (다음) 프로세스에 대한 Saved Registers 값들을 Load하고, 그에 해당하는 메모리 주소 공간으로 이동해 해당 프로세스를 수행한다.
현대의 프로세서는 'Multicore'가 대세이다.
금일 포스팅은 여기까지 하겠다.