..

컴퓨터 구조 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으로 나눌 때 (나누기 오류), 산술 연산이 오버플로우 날 때, 혹은 단순히 운영체제 기능을 요청할 때(시스템 콜)
    1. 핸들러는 제어를 현재 인스트럭션 Icurr로 돌려준다(문제 해결 후, 같은 명령 다시 실행)
    2. 핸들러는 제어를 다음 인스트럭션 Inext로 돌려주는데, 예외 상황이 발생하지 않았다면 다음에 실행되었을 인스트럭션을 실행(문제 해결 후, 다음 명령어로 진행)
    3. 핸들러는 중단된 프로그램을 종료

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) 시그널이라고 부른다
  • 시그널 kill 과정
    • 시그널을 /bin/kill 프로그램을 사용하기
    • 키보드에서 시그널 보내기
    • kill 함수로 시그널 보내기
    • alarm함수로 시그널 보내기

 

  • 시그널의 수신
    • 시그널을 수신하면 프로세스는 어떤 동작을 개시한다 동작은 아래와 같다
      • 프로세스가 종료한다
      • 프로세스는 종료하고 코어를 덤프한다
      • 프로세스는 SIGCONT 시그널에 의해 재시작될 때까지 정지한다
      • 프로세스는 시그널을 무시한다
    • 대기(pending) 중인 시그널을 확인, 블록되지 않은 시그널이 있으면, 커널은 프로세스의 제어 흐름을 강제로 바꿔서 시그널 처리 루틴을 실행한다

    • signal(signum, handler) 를 호출하면 시그널 번호에 대해 어떤 동작을 할지 지정할 수 있다
    • 시그널이 수신되었을떄 프로세스가 취할 수 있는 세 가지 처리 방법
    • 기본 동작(Default), 무시(Ignore), 사용자 핸들러(Catch)

    • 시그널 차단 이유
      • 시그널은 비동기적으로 들어옴, 중요한 코드(예: 공유 데이터 갱신 중)에 시그널 핸들러가 갑자기 실행되면 데이터 불일치나 충돌이 발생
      • 잠시 특정 시그널을 차단했다가, 안전한 시점에 해제

 

  • 시그널 블록(block) 은 크게 두 가지 방식
    • 묵시적 블록 (Implicit Blocking)
      • 운영체제가 자동으로 시그널을 차단하는 경우
    • 명시적 블록 (Explicit Blocking)
      • 프로그래머가 직접 코드를 작성해 특정 시그널을 차단하는 방법
  • 시그널 핸들러 작성하기
    • 주의점
      • 핸들러는 프로그램과 동시적으로 돌아가고, 전역변수를 공유, 다른 핸들러과 뒤섞일 수 있음
      • 시그널들이 수신될 수 있는지는 직관적이지 않다
      • 시스템마다 다른 시그널 처리 방식
    • 가이드 라인
      • 핸들러는 간단한게 유지
      • 핸들러에서 비동기성-시그널-안전한 함수만 호출(printf, sprintf 등 사용 비추천, Safe I/O 사용)
      • errno를 저장하고 복원하라(시스템 콜이나 라이브러리 함수가 실패했을 때, 실패 원인을 나타내는 오류 번호)
      • 모든 시그널을 블록시켜서 공유된 전역 자료구조들로부터 접근을 보호하라
      • 전역변수들을 volatitle로 선언 (매번 메모리에서 g 값을 읽어오도록 강요)
      • sig_atomic_t로 플래그들을 선언하라 (읽기/쓰기 연산이 원자적(atomic) 으로 보장되어 중간에 끊기지 않는다)
        • 중간에 끊기거나 잘리지 않고, 한 덩어리로 완전히 실행되는 연산
  • 동시성
    • 같은 메모리 자원을 읽고 쓸 때, 실행 순서가 어떻게 interleaving 되느냐에 따라 결과가 달라질 수 있음
    • 동기화(synchronization)를 통해 안전하게 제어
    • 시그널과 프로세스 제어의 동시성 버그를 피하기 위해, 시그널 블록을 이용해 흐름을 동기화해야 한다
  • 비지역성 점프
    • 비지역성 점프(nonlocal jump) 는 한 함수에서 실행되다가, 현재 함수나 중간 호출 함수들을 건너뛰고 다른 함수로 바로 점프하는 것을 말함 즉, 현재 호출 스택을 무시하고 특정 지점으로 점프하는 메커니즘
    • C에서는 표준 라이브러리 함수 setjmp 와 longjmp 를 사용
    • 에러 처리나 시그널 복구용으로 사용