[TIL] WEEK05 - CS:APP 8장 (추가 필요)

woo__j·2024년 4월 25일
0

TIL - Today I Learned

목록 보기
19/23

8. 예외적인 제어 흐름

제어 이동: PC값을 하나씩 증가시키며 순서대로 인스트럭션을 실행시키는 것
제어흐름: 제어이동의 배열

간단한 유형의 제어흐름은 ‘점진적인’ 순서로 각 인스트럭션이 나란히 인접해 있는 경우다.
점진적인 흐름에 갑작스런 변화가 생기는 건 각 인스트럭션이 인접해 있지 않는 경우인데, jump/call retrun 같은 프로그램 인스트럭션에 의해 발생한다. -> 프로그램 변수에 의한 내부 프로그램 상태 변화에 반응하기 위해 필요한 메커니즘이다.

이와 같이 제어흐름의 갑작스런 변화를 ‘예외적인 제어흐름(ECF)’라고 한다.

[시스템 에러 발생 시 프로세스->커널로 제어를 넘겨 해당 에러를 적절하게 처리한 후 다시 프로세스로 제어를 넘겨준다]

8.1 예외상황

예외상황?

: ‘어떤 프로세서 상태의 변화에 대한 대응’으로 제어흐름의 갑작스런 변화 (예외적인 제어흐름의 한 가지 형태)
프로세서의 상태 변화는 이벤트로 알려져 있는데, 현재 인스트럭션의 실행에 직접적으로 관련될 수 있다.

ex) 가상 메모리 페이지 오류, 산술 오버플로우가 발생하거나 divide by zero를 시도하는 경우

물론 관련이 없을 수도 있다.

ex) 시스템 타이머가 정지하거나 I/O 요청이 완료되는 경우

[예외상황]
1. 예외 발생: 프로그램이 실행되는 도중 특정 이벤트가 발생해 예외를 발생시킴

ex. 파일을 읽으려 할 때 파일이 존재하지 않거나, 네트워크 연결이 끊어진 상태에서 데이터를 전송하려 하는 경우
  1. 예외 전파: 예외가 발생해, 해당 예외를 처리할 수 있는 적절한 예외 처리기(handler)를 찾을 때까지 호출 스택을 거슬러 올라감
  2. 예외 처리: 적절한 예외 처리기가 예외를 잡아(catch) 처리, 오류 메세지를 표시하거나/필요한 경우 프로그램을 안전하게 종료하는 걸 포함할 수 있음

어느 경우에서든지 프로세서가 이벤트가 발생했다는 걸 감지하면, ‘예외 테이블’이라고 하는 점프 테이블을 통해 이벤트를 처리하기 위해 특별히 설계된 예외처리 핸들러로 간접 프로시저 콜을 하게 된다.

핸들러가 처리를 끝마치면, 예외상황을 발생시킨 이벤트의 종류에 따라 아래 세 가지 중 한 가지 일이 발생된다.

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

  • 핸들러?: 특정 상황을 해결하기 위한 소프트웨어 루틴, 문제 해결을 할 때 수행하는 일정한 절차
    [참고🙇‍♀️: 선배 기수 velog]

8.1.1 예외처리

예외처리는 프로그램이 실행되는 동안 발생할 수 있는 예외상황들을 관리&처리하는 방법
이러한 예외상황은 하드웨어가 감지하거나, 소프트웨어가 명시적으로 발생시키는 것일 수 있다.

[예외상황]
1. 하드웨어에 의해 감지되는 예외
: 하드웨어가 실행 중인 명령어가 문제를 일으킬 때, CPU가 예외상황을 감지하고 처리하기 위한 동작을 수행함

ex) divide by zero, 메모리 접근 오류, 불법 명령어, breakpoint
  1. 소프트웨어에 의해 명시적으로 발생시키는 예외
    : 프로그램에서 명시적으로 예외를 발생시켜 특정 상황을 처리하도록 함

[예외상황을 처리하려면?]
예외 처리를 위해 시스템은 부팅 시 ‘예외 테이블’을 할당하고 초기화해서 사용하는데, 이는 각 예외 번호와 해당 예외를 처리하는 ‘예외 처리기’의 주소를 매핑한다. 이 테이블의 시작주소는 ‘예외 테이블 베이스 레지스터’라는 특별한 CPU 레지스터에 저장되어 있다.

예외가 발생하면, 프로그램 카운터는 해당 예외를 처리할 수 있는 예외 처리기의 시작 주소로 변경되고, CPU는 이 예외 테이블을 참조해 적절한 예외 처리기를 실행한다.
예외 처리기는 해당 예외 상황에 대한 정보를 받아 적절한 조치를 취하는데, 이는 에러 메세지 보여주기/프로그램 안전하게 종료 시키기/상황을 복구하고 프로그램 계속 실행하기 등이 포함될 수 있다.

*예외번호는 한 시스템 내에서 가능한 예외상황의 종류마다 중복되지 않는 양의 정수로 할당한다.
일부는 프로세서 설계자가, 나머지는 운영체제 커널 설계자가 할당한다.

[프로세서 설계자가 할당하는 부분의 예]: divide by zero, 페이지 오류, 메모리 접근 위반, 산술연산 오버플로우, breakpoint 포함
[운영체제 커널 설계자가 할당하는 부분의 예]: 시스템 콜, 외부 I/O 디바이스로부터의 시그널 포함

8.1.2 예외의 종류

예외는 발생 원인&처리 방식에 따라 나눌 수 있다.

- 동기? 비동기?
동기: 응답을 받아 실행
비동기: 응답에 관계없이 실행

[동기적 발생]
1. Interrupt
원인: 프로세서 외부에 있는 I/O 디바이스로부터의 시그널 결과로 ‘비동기적’으로 발생한다.
하드웨어 인터럽트는 비동기적인데, 특정 인스트럭션을 실행해서 발생한 것이 아니라는 의미 (CPU가 현재 수행 중인 작업과는 독립적으로 발생) 그래서 하드웨어 인터럽트를 위한 예외 핸들러는 인터럽트 핸들러라고 부른다.
반환 행동: 핸들러가 리턴을 할 때 제어를 다음 인스트럭션으로 리턴한다. 제어흐름에서 인터럽트가 발생하지 않았다면 현재 명령 다음에 왔을 명령으로 돌려주는 건데, 이는 프로그램이 마치 인터럽트가 발생하지 않았던 것처럼 계속해서 실행되는 것

[비동기적 발생]
2. Trap
원인: 의도적인 예외상황, 어떤 인스트럭션을 실행한 결과로 발생한다.
프로그램의 실행 흐름에 따라 예상되는 지점에서 발생하기 때문에 ‘동기적’으로 발생
반환 행동: 트랩을 처리한 후, 제어를 다음 인스트럭션으로 리턴한다.

- 트랩의 가장 중요한 사용
: 시스템 콜, 운영체제의 커널이 제공하는 서비스에 대해, 응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스

3. Fault (fault와 error는 다르다)
원인: 핸들러가 정정할 수 있을 가능성이 있는, 잠재적으로 복구 가능한 에러 조건으로 인해 발생한다.
명령어 실행 중에 발생해 동기적이며, 해당 명령어가 원인이다.
반환행동: 오류가 발생했을 때 프로세서는 제어를 Fault Handler로 이동해준 후,
= Handler가 에러 조건을 정정할 수 있다면? 오류를 발생시킨 인스트럭션(Icurrent)으로 제어를 넘겨주어 거기서부터 재실행한다.
= 그렇지 않다면? 핸들러가 커널 내부 Abort 루틴으로 리턴해 오류를 발생시킨 응용프로그램을 종료.

4. Abort
원인: 대개 DRAM/SRAM이 고장날 때 발생하는 패리티 에러와 하드웨어 같은 ‘복구 불가능한’ 치명적인 에러에서 발생한다.
프로그램 실행 중에 직접적인 원인으로 발생되어 동기적이다.
반환행동: Abort Handler는 절대 응용프로그램으로 제어를 리턴하지 않는다. 제어를 응용프로그램을 종료하는 중단 루틴으로 넘겨준다 = 즉 프로그램을 중단시킨다.

8.5 시그널

시그널은 작은 메세지 형태로, 프로세스에게 시스템 내에서 어떤 종류의 ‘이벤트’가 일어났다는 것을 알리기 위한 시스템이다.
프로세스가 다른 프로세스에게 알리거나, 운영체제가 프로세스에게 알릴 때 사용되는데, 이것은 프로세스가 비동기적으로 이벤트를 받아 처리할 수 있도록 하는 방식이다.

예를 들어, 사용자가 키보드로 인터럽트를 요청(Ctrl+C)하거나, 프로세스에 문제가 발생했을 때 시그널이 발생된다.
프로세스는 이러한 시그널을 처리하는 방법을 정의할 수 있고, 기본 동작을 무시하고 사용자 정의 핸들러를 사용해 시그널을 처리할 수도 있다. 

각 시그널 타입은 특정 종류의 시스템 이벤트에 대응된다.
하위수준 하드웨어 예외는 커널의 예외 핸들러에 의해 처리되고, 정상적으로는 사용자 프로세스에서는 볼 수 없다. 이 때 시그널은 이러한 예외들을 사용자 프로세스에 노출해주는 메커니즘을 제공한다.

SIGFPE: 0으로 나누려고 할 때 보내는 시그널
SIGILL: 잘못된 인스트럭션을 실행하려 할 때 보내는 시그널
SIGSEGV: 잘못된 메모리 참조를 할 때 보내는 시그널

다른 시그널들은 커널 내 또는 다른 사용자 프로세스 내부의 상위수준 소프트웨어 이벤트에 대응된다.

SIGINT: 프로그램을 인터럽트하기 위해 보내지는 시그널, 사용자가 키보드에서 Ctrl+C를 눌렀을 때 발생
SIGKILL: 프로세스를 즉시 종료하기 위해 보내지는 시그널, 무시할 수 없음
SIGCHILD: 자식 프로세스의 상태가 변경되었을 때(종료/정지) 부모 프로세스에게 보내지는 시그널

8.5.1 시그널 용어

시그널을 목적지 프로세스로 전달하는 것은 크게 두 단계로 이루어진다.

1. 시그널 보내기
커널은 목적지 프로세스의 컨텍스트 내에 있는 일부 상태를 갱신해 시그널을 목적지 프로세스로 보낸다(Deliver)
프로세스는 시그널을 자기 자신에게 보낼 수도 있다.

시그널이 배달되는 이유?
1) 커널이 divide by zero, 자식 프로세스의 종료같은 시스템 이벤트를 감지
2) 어떤 프로세스가 커널에 명시적으로 시그널을 목적지 프로세스에 보낼 것을 요구하기 위해 kill 함수 호출

2. 시그널 받기
목적지 프로세스는 배달된 신호에 대해 커널이 어떤 방식으로 반응해야 할 때, 목적지 프로세스는 시그널을 받는다.
프로세스는 시그널 핸들러라고 부르는 사용자수준 함수를 실행해 시그널을 ‘무시’ or ‘종료’ or ‘획득’할 수 있다.

[시그널의 상태/행동]
1. Deliver(배달): 시그널이 프로세스에 전달되어 처리되길 기다리고 있는 상태
2. Pending(보류): 프로세스가 시그널을 받았지만 아직 처리하지 않은 상태, 프로세스가 처리할 준비가 될 때까지 보류
3. Block(차단): 프로세스가 특정 시그널을 일시적으로 무시하도록 설정하는 것, 차단된 시그널은 프로세스에 의해 처리X

[시그널의 처리 과정]
1. 시그널 발생: 운영체제 or 다른 프로세스에 의해 시그널 발생
2. 시그널 배달: 발생한 시그널이 대상 프로세스에게 전달
3. 시그널 처리: 프로세스는 시그널을 처리, 기본 동작 또는 사용자 정의 핸들러를 통해 처리
4. 시그널 반환: 시그널 핸들러가 실행을 완료하면, 프로세스는 일반적인 실행 흐름으로 돌아감

-> 보내졌지만 아직 받지 않은 시그널은 ‘펜딩(pending) 시그널’이라고 한다.
시간 상 어떤 시점에서, 특정 타입에 대해 최대 한 개의 펜딩 시그널이 존재할 수 있다.
어떤 프로세스가 타입 k의 펜딩 시그널을 가지고 있다면 이 프로세스로 다음에 발생하는 k 타입의 시그널은 큐에 들어가지 않고 단순히 버려진다.

또 프로세스는 선택적으로 어떤 시그널의 수신을 ‘블록(Block)’할 수 있다.
특정 시그널이 블록될 때 배달은 될 수 있지만, 펜딩 시그널은 이 프로세스가 시그널의 블록을 풀 때까진 수신되지 않는다.

8.5.2 시그널 보내기

프로세스 간 시그널을 보내는 것은 프로세스가 다른 프로세스에게 특정 이벤트를 알리기 위해 사용되는데,
시그널을 프로세스로 보내는 모든 메커니즘은 ‘프로세스 그룹’ 개념을 사용한다.
모든 프로세스는 정확히 한 개의 프로세스 그룹에 속하며, 하나의 프로세스 그룹에 속한 모든 프로세스는 동일한 시그널을 받을 수 있다. 프로세스 그룹은 양수 process groupID로 식별한다.

getpgrp 함수: 현재 프로세스의 프로세스 그룹ID return

기본적으로, 자식 프로세스는 자신의 부모와 동일한 프로세스 그룹에 속하고,
프로세스는 자신의 프로세스 그룹 또는 다른 프로세스의 그룹을 setpgid 함수를 사용해 변경할 수 있다

[시그널 보내기]

- bin/kill 프로그램
다른 프로세스로 임의의 시그널 보내기, 음수 PID은 프로세스 그룹 PID 내 모든 프로세스에게 보냄

ex) /bin/kill -9 15213: 시그널 9번(SIGKILL)을 프로세스 15213에 보냄
	/bin/kill -9 -15213: SIGKILL 시그널이 프로세스 그룹 15213 내 모든 프로세스에게 보냄

[/bin/kill]과 같이 완전 경로를 사용하는 것은 -> 일부 Unix 쉘이 자신만의 내장 kill 명령어를 가지고 있기 때문

- 키보드에서 시그널 보내기
Ctrl+C 또는 Ctrl+Z 입력 시, 시그널이 포그라운드 프로세스 그룹의 모든 작업으로 전송됨

Ctrl+C: SIGINT 시그널 전송 -> 포그라운드 작업 ‘종료’
Ctrl+Z: SIGSTP 시그널 전송 -> 포그라운드 작업 ‘정지’

- kill 함수
프로세스는 kill 함수를 호출해 시그널을 다른 프로세스에게 보냄 (이 때, 자기 자신 포함)

int kill(pid_t pid, int sig);
-> pid > 0: kill 함수는 시그널 번호 sig를 프로세스 pid로 보냄
-> pid = 0: kill 함수는 호출하는 프로세스인 자신을 포함해 프로세스 그룹 내 모든 프로세스에 시그널 번호 sig를 보냄
-> pid < 0: kill 함수는 프로세스 그룹 |pid| 내의 모든 프로세스로 보냄

- alarm 함수
커널이 sec초마다 프로세스로 SIGALRM 시그널을 보내도록 함 (secs=0이면 새로운 알람 스케줄x)
이 때 어떤 이벤트가 발생하더라도 alarm으로의 호출은 대기하는 모든 알람을 취소하고,

alarm 호출이 취소되지 않았다면 = 대기하던 알람이 전달되었어야 할 때까지 남은 시간을 초 단위 숫자로 반환하거나
대기하고 있는 알람이 없었다면 = 0을 Return

8.5.3 시그널 수신

커널이 프로세스 p를 커널모드에서 사용자 모드로 전환할 때 (ex. 시스템 콜에서 리턴하거나 문맥 전환을 끝마치는 것 같은)

  1. 커널은 프로세스 p에 대한 블록되지 않은 펜딩 시그널(pending & ~blocked)의 집합을 체크한다
    • 만일 비어있다면: 제어를 p의 논리 제어흐름 내 다음 인스트럭션(Inext)로 전달
  2. 비어있지 않다면 , 집합 내 어떤 시그널 k를 선택해 p가 시그널 k를 수신하도록 함
  3. 시그널을 수신하면 프로세스는 시그널 처리 작업을 수행하고, 완료하면 제어는 p의 논리 제어흐름 내 다음 인스트럭션으로 돌아감
  4. 이 과정을 블록되지 않은 펜딩 시그널의 집합이 빌 때까지 반복

[각 시그널 타입의 사전 정의된 기본 동작]
1. 프로세스 종료
2. 프로세스 종료 후 코어를 덤프
3. 프로세스는 SIGCONT 시그널에 의해 재시작될 때까지 정지(지연)
4. 프로세스가 시그널 무시

-> 이 기본동작은 sighandler_t signal(int signum, sighandler_t handler);에 의해 변경 가능 (SIGSTOP & SIGKILL은 변경X), signal 함수는 시그널 signum과 연결된 동작을 세 가지 방법 중 하나로 바꿀 수 있다.

- handler = SIG_IGN: signum 타입의 시그널 무시
- handler = SIG_DFL: signum 타입의 시그널에 대한 동작은 기본 동작으로 돌아감
- 그 외 경우: handler는 시그널 핸들러의 주소가 되어 signum 시그널을 수신할 때마다 호출
			핸들러의 주소를 signal 함수로 넘겨주는 방법은 기본 동작을 변경하는 것으로, 핸들러를 ‘설치’한다라고 함
			핸들러의 호출은 시그널을 잡는다(catching the signal)
			핸들러의 실행은 시그널을 처리한다(handling the signal)

734p부터 추가하기… 8.5장 넘 길다… 시그널 블록/블록 해제, 시그널 핸들러 작성, 명시적/묵시적 등등…ㅠ

0개의 댓글

관련 채용 정보