컴퓨터 구조 CS:APP 8장
예외적 제어 흐름(Exceptional Control Flow, ECF)
- ECF는 하드웨어, 커널, 응용 프로그램 모든 수준에서 필수적인 매커니즘
- ECF는 예기치 않은 이벤트에 응답하거나, 컨텍스트 전환을 수행하거나, 인터럽트를 처리하기 위해 사용
- 시스템 호출과 같은 명시적인 흐름 전환도 ECF의 한 형태
하드웨어 수준 ECF: 예외 및 인터럽트
- 예외(Exceptions)
- 프로세서 내부 이벤트
- 예: 0으로 나누기, 페이지 폴트, 시스템 호출
- 인터럽트(Interrupts)
- 외부 디바이스가 CPU에 개입하는 매커니즘
- 트립 핸들러
- 예외/인터럽트 발생 시 커널 모드로 전환 -> 커널 코드 실행 -> 다시 사용자 모드 복귀
- 컨트롤 흐름을 커널로 전달하는 핵심
소프트웨어 수준 ECF: 프로세스 간 전환
- 프로세스(Processes)
- OS가 관리하는 실행 단위
- 컨텍스트 스위칭(Context Switching)
- 한 프로세스에서 다른 프로세스로의 전환
- 타이머 인터럽트나 I/O 인터럽트를 통해 커널이 현재 작업 저장 -> 다음 작업으로 전환
시그널(Signals): 유저 수준의 ECF
- OS가 프로세스에 보내는 작은 메시지
- 시그널 처리 방식
- 기본 동작 또는 핸들러 등록 가능
- sigaction, signal 함수로 사용자 정의 핸들러 지정 가능
- 동기/비동기 시그널
- 동기적: 오류로 인해 현재 명령어에서 발생
- 비동기적: 외부 이벤트로 인해 발생
시스템 호출(System Call)
- 사용자 프로그램이 OS 기능을 요청하는 방법
- 명령을 통해 트랩 발생 -> 커널 모드 전환
에러 처리와 프로세스 종료 흐름
- exit, _exit, abort, atexit
- 프로그램 정상 종료와 비정상 종료를 구분하는 다양한 방법 제공
- 시그널에 의한 강제 종료
- kill 또는 CTRL+C 등
ECF(예외적인 제어흐름)
- 하드웨어 수준에서 하드웨어에 의해서 검출되는 이벤트들은 예외 핸들러로 갑작스런 제어이동을 발생시킨다
- jump, call 리턴 같은 친숙한 프로그램 인스트럭션에 의해 발생한다
- 트랩 또는 시스템콜이라고 알려진 ECF의 한 가지 형태를 사용해서 운영체제로부터 서비스를 요청한다
8.1 예외상황 분석
프로그램이 실행되다가 특별한 사건(문제나 요청)이 생김
- 없는 메모리 페이지에 접근하려 할 때 (페이지 오류), 0으로 나눌 때 (나누기 오류), 산술 연산이 오버플로우 날 때, 혹은 단순히 운영체제 기능을 요청할 때(시스템 콜)
- 핸들러는 제어를 현재 인스트럭션 Icurr로 돌려준다(문제 해결 후, 같은 명령 다시 실행)
- 핸들러는 제어를 다음 인스트럭션 Inext로 돌려주는데, 예외 상황이 발생하지 않았다면 다음에 실행되었을 인스트럭션을 실행(문제 해결 후, 다음 명령어로 진행)
- 핸들러는 중단된 프로그램을 종료
8.1.1 예외처리
- 예외상황의 종류마다 중복되지 않은 양의 정수를 예외번호로 할당
- 이 숫자들의 일부는 프로세서 설계자가 부여, 나머지 번호는 운영체제 커널 설계자가 할당
- 시스템 부팅 시 운영체제는 예외 테이블이라고 하는 점프 테이블을 할당하고 초기화
- 프로그램 런타임에 프로세서는 이벤트가 발생 했다는 걸 감지 -> 대응되는 예외번호 k를 결정
- 예외 테이블에는 각 번호별로 “어떤 핸들러 코드로 점프할지” 주소가 들어있음
- 예외번호는 예외 테이블에서 인덱스이며, 이 테이블의 시작주소는 예외 테이블 베이스 레지스터라는 특별한 CPU 레지스터에 저장
예외의 종류
- 예외는 네 가지 종류로 구분할 수 있다
- 인터럽트, 트랩, 오류(fault), 중단
- 인터럽트: 프로세서 외부에 있는 입출력 디바이스로부터의 시그널의 결과로 비동기적으로 발생
- 트랩: 의도적 예외상황
- 시스템 콜: 프로그램과 커널 사이의 프로시저와 유사한 인터페이스를 제공
- read(읽기), fork(프로세스 만들기), execve(프로그램 로드)
- 오류: 핸들러가 정정할 수 있을 가능성이 있는 에러 조건
- 중단: DRAM이나 SRAM이 고장날 때 발생하는 패리티 에러와 하드웨어 같은 복구할 수 없는 치명적인 에러에서 발생
- 동기형 예외 - 트랩, 오류, 중단
- 비동기형 예외 - 인터럽트
리눅스 오류와 중단
- 나누기 에러: 0으로 나눌때
- 일반 보호 오류: 메모리의 정의되지 않은 영역을 참조
- 페이지 오류: 오류 발생 인스트럭션이 재시작하는 예외
- 머신 체크: 치명적인 하드웨어 에러의 결과

-
각 시스템 콜은 커널 점프 테이블의 오프셋에 대응되는 유일한 정수를 갖는다
-
C 프로그램은 syscall 함수를 사용해서 직접 시스템 콜을 호출할 수 있다
-
관습적으로 레지스터 %rax는 시스템 콜 번호를 보관하며 %rdi, %rsi, %rdx, %r10, %r8, %r9에 최대 여섯 개의 인자들을 보관할 수 있다
8.2 프로세스
-
프로세스 개념을 운영체제 커널이 제공할 수 있게 하는 기본 구성 블록
-
고전적인 정의는 실행 프로그램의 인스턴스(프로그램의 코드와 데이터, 스택, 범용 레지스터의 내용, 프로그램 카운터, 환경변수 등 메모리에 저장된 프로그램 데이터)
-
하나의 프로세서를 사용해서 여러 프로세스들이 교대로 돌아간다는 점
8.2.2 동시성 흐름
- 동시성: 번갈아가면서 실행(컨텍스트 스위칭)
- 병렬성: 동시에 실행
- 멀티태스킹: 프로세스가 다른 프로세스들과 교대로 실행된다는 개념
- 타임 슬라이스: 한 프로세스가 자신의 흐름 일부를 실행하는 매 시간 주기
8.2.3 사적 주소공간

- n 비트 주소를 갖는 머신에서 주소공간은 \((2^n - 1)\)을 갖는다
- 주소공간의 아랫부분은 코드, 데이터, 힙, 스택 세그먼트를 갖는 사용자 프로그램을 위해 예약
- 코드 세그먼트, 데이터 세그먼트, 힙, Memory-mapped region for shared libraries, User stack (스택), Kernel virtual memory
- 코드 세그먼트는 항상 주소 0x400000에서 시작한다
- 프로그램 코드(명령어) 와 읽기 전용 데이터(.text, .rodata, .init)가 들어 있음
- 읽기 전용이라 수정 불가. 실행만 가능
- 데이터 세그먼트
- 프로그램이 실행될 때 필요한 전역 변수, 정적 변수 등이 저장(.data, .bss)
- Run-time heap (힙)
- 동적 메모리(malloc, new)로 할당되는 영역
- 낮은 주소에서 높은 주소 쪽으로 확장(brk 또는 sbrk 시스템 콜로 조정)
- Memory-mapped region for shared libraries
- 공유 라이브러리(.so 파일), 동적 로딩된 코드, 파일 매핑된 메모리 등이 저장되는 영역
- User stack (스택)
- 함수 호출과 복귀를 관리하기 위한 스택 프레임들이 쌓이는 영역
- Kernel virtual memory
- 사용자 영역 맨 위쪽(가장 높은 주소 영역)
- 사용자 프로그램은 이 영역에 직접 접근할 수 없음 (보호 모드), 시스템 콜이나 인터럽트를 통해서만 간접 접근 가능
8.2.4 사용자 및 커널 모드
- 모드 비트가 설정되면 프로세스는 커널 모드로 동작한다
- 커널 모드에서 돌고 있는 프로세스는 어떤 인스트럭션도 실행 가능, 시스템 내의 어떤 메모리 위치도 접근 가능
- 모드 비트가 설정되지 않을 때, 프로세스는 사용자 모드에서 돌고 있는 것이다
- 사용자 모드의 프로세스는 특수 인스트럭션을 실행할 수 없다. 또한 주소공간의 커널 영역에 있는 코드나 데이터를 직접 참조할 수도 없다
- 대신 사용자 프로그램은 시스템 콜을 통해서 커널 코드와 데이터에 간접적으로 접근해야 한다
문맥 전환
- 컨텍스트 스위칭은 시스템 콜이 어떤 이벤트의 발생을 기다리기 때문에 블록된다면 커널은 현재 프로세스를 sleep시키고 다른 프로세스로 전환한다
- 수행 절차는 1. 프로세스의 컨텍스를 저장 2. 이전에 선점된 프로세스의 저장된 컨텍스트를 복원 3. 제어를 이 새롭게 복원된 프로세스로 전달
스케줄러
- 운영체제 커널의 일부로서, CPU를 어떤 프로세스에게 할당할지 결정하는 모듈
- 여러 프로세스가 CPU를 효율적으로 공유할 수 있도록 CPU 제어권을 배분
8.4 프로세스 제어
- 프로세스의 ID = PID
8.4.2 프로세스의 생성과 종료
- Running (실행 중)
- Stopped (중지됨): 특정 시그널을 받으면 멈추고, SIGCONT 시그널 받아야 실행 가능
- Terminated (종료됨): 영구적으로 멈춘 상태
- fork(): 시스템 콜을 호출하여 새로운 자식 프로세스를 생성
- 자식 프로세스는 부모의 가상 주소 공간(코드, 데이터, 힙, 스택 등)을 그대로 복사본가짐
- 차이점은 PID가 다르다
8.4.3 자식 프로세스의 청소
- 종료되었지만 아직 청소되지 않은 프로세스를 좀비
- 프로세스는 waitpid함수를 호출해서 자신의 자식들이 종료되거나 정지되기를 기다린다
8.4.4 프로세스 재우기
- sleep 함수는 일정 기간 동안 프로세스를 정지시킨다
8.4.5 프로그램의 로딩과 실행
- execve함수는 현재 프로그램의 컨택스트 내에서 새로운 프로그램을 로드하고 실행함
int main(int argc, char **argv, char **envp);
int main(int argc, char *argv[], char *envp[]);
argc: argv[] 배열 안에 몇 개의 문자열이 들어있는지 나타내는 개수
argv: 프로그램 실행 시 전달된 명령줄 인자(command-line arguments) 들을 가리키는 포인터 배열
envp: 프로그램 실행 환경을 나타내는 환경 변수(environment variables) 문자열 배열
./myecho arg1 arg2
argc = 3
argv[0] = “./myecho”
argv[1] = “arg1”
argv[2] = “arg2”
argv[3] = NULL
envp[0] = “PWD=/home/dohoon”
envp[1] = “SHELL=/bin/bash”
8.5 시그널
- 시그널은 프로세스에게 특정 이벤트가 발생했음을 알려주는 작은 메시지
- 키보드에서 Ctrl+C 입력 → SIGINT 시그널 전송
- 잘못된 메모리 접근 → SIGSEGV (세그폴트)
- 0으로 나누기 → SIGFPE
- 자식 프로세스 종료 → SIGCHLD
- 시그널 함수: 운영체제가 보내는 외부 사건(시스템 레벨 예외/인터럽트)을 잡는 장치
-
이벤트 함수: 프로그램 내부에서 정의된 논리적 사건(클릭, 입력 등)을 처리하는 장치
- 시그널 전달 과정
- Sending
- 커널이 이벤트를 감지하거나, kill을 호출해서 시그널을 특정 프로세스로 보냄
- Receiving
- 목적지 프로세스가 시그널을 받는다
- 보냈지만 아직 받지 않은 시그널은 펜딩(pending) 시그널이라고 부른다
- Sending
- 시그널 kill 과정
- 시그널을 /bin/kill 프로그램을 사용하기
- 키보드에서 시그널 보내기
- kill 함수로 시그널 보내기
- alarm함수로 시그널 보내기
- 시그널의 수신
- 시그널을 수신하면 프로세스는 어떤 동작을 개시한다 동작은 아래와 같다
- 프로세스가 종료한다
- 프로세스는 종료하고 코어를 덤프한다
- 프로세스는 SIGCONT 시그널에 의해 재시작될 때까지 정지한다
- 프로세스는 시그널을 무시한다
-
대기(pending) 중인 시그널을 확인, 블록되지 않은 시그널이 있으면, 커널은 프로세스의 제어 흐름을 강제로 바꿔서 시그널 처리 루틴을 실행한다
- signal(signum, handler) 를 호출하면 시그널 번호에 대해 어떤 동작을 할지 지정할 수 있다
- 시그널이 수신되었을떄 프로세스가 취할 수 있는 세 가지 처리 방법
-
기본 동작(Default), 무시(Ignore), 사용자 핸들러(Catch)
- 시그널 차단 이유
- 시그널은 비동기적으로 들어옴, 중요한 코드(예: 공유 데이터 갱신 중)에 시그널 핸들러가 갑자기 실행되면 데이터 불일치나 충돌이 발생
- 잠시 특정 시그널을 차단했다가, 안전한 시점에 해제
- 시그널을 수신하면 프로세스는 어떤 동작을 개시한다 동작은 아래와 같다
- 시그널 블록(block) 은 크게 두 가지 방식
- 묵시적 블록 (Implicit Blocking)
- 운영체제가 자동으로 시그널을 차단하는 경우
- 명시적 블록 (Explicit Blocking)
- 프로그래머가 직접 코드를 작성해 특정 시그널을 차단하는 방법
- 묵시적 블록 (Implicit Blocking)
- 시그널 핸들러 작성하기
- 주의점
- 핸들러는 프로그램과 동시적으로 돌아가고, 전역변수를 공유, 다른 핸들러과 뒤섞일 수 있음
- 시그널들이 수신될 수 있는지는 직관적이지 않다
- 시스템마다 다른 시그널 처리 방식
- 가이드 라인
- 핸들러는 간단한게 유지
- 핸들러에서 비동기성-시그널-안전한 함수만 호출(printf, sprintf 등 사용 비추천, Safe I/O 사용)
- errno를 저장하고 복원하라(시스템 콜이나 라이브러리 함수가 실패했을 때, 실패 원인을 나타내는 오류 번호)
- 모든 시그널을 블록시켜서 공유된 전역 자료구조들로부터 접근을 보호하라
- 전역변수들을 volatitle로 선언 (매번 메모리에서 g 값을 읽어오도록 강요)
- sig_atomic_t로 플래그들을 선언하라 (읽기/쓰기 연산이 원자적(atomic) 으로 보장되어 중간에 끊기지 않는다)
- 중간에 끊기거나 잘리지 않고, 한 덩어리로 완전히 실행되는 연산
- 주의점
- 동시성
- 같은 메모리 자원을 읽고 쓸 때, 실행 순서가 어떻게 interleaving 되느냐에 따라 결과가 달라질 수 있음
- 동기화(synchronization)를 통해 안전하게 제어
- 시그널과 프로세스 제어의 동시성 버그를 피하기 위해, 시그널 블록을 이용해 흐름을 동기화해야 한다
- 비지역성 점프
- 비지역성 점프(nonlocal jump) 는 한 함수에서 실행되다가, 현재 함수나 중간 호출 함수들을 건너뛰고 다른 함수로 바로 점프하는 것을 말함 즉, 현재 호출 스택을 무시하고 특정 지점으로 점프하는 메커니즘
- C에서는 표준 라이브러리 함수 setjmp 와 longjmp 를 사용
- 에러 처리나 시그널 복구용으로 사용