Study/TIL(Today I Learned)

24.09.05 CSAPP복습 ,C++

에린_1 2024. 9. 5. 18:22
728x90

CSAPP

8.1.2 예외의 종류

  • 예외상황은 네 가지 종류로 구분 할 수 있다.
    • 인터럽트
    • 트랩
    • 오류 Fault
    • 중단 abort

인터럽트

  • 인터럽트는 프로세서 외부에 있는 입출력 디바이스로부터 시그널의 결과로 비동기적으로 발생한다. 하드웨어 인터럽트는 비동기적이며, 즉 특정 인스트럭션을 실행해서 발생하는 것이 아니라는 의미에서 그렇다. 하드웨어 인터럽트를 위한 예외 핸들러는 종종 인터럽트 핸들러 라고 부른다.
  • 네트워크 어댑터, 디스크 컨트롤러, 타이머 칩 같은 입출력 디바이스들은 프로세서 칩의 핀에 시그널을 보내서 인터럽트를 발생시키고, 인터럽트를 발생시킨 디바이스를 식별하는 예외번호를 시스템 버스에 보낸다.
  • 현재의 인스트럭션이 실행을 완료한 후에, 프로세서는 인터럽트 핀이 high로 올라갔다는 것을 발견하고 시스템 버스에서 예외번호를 읽으며, 적절한 인트럽트 핸들러를 호출한다. 핸들러가 리턴할 때, 제어를 다음 인스트럭션으로 돌려준다.(즉, 제어흐름에서 인터럽트가 발생하지 않았다면 현재 인스트럭션 다음에 왔을 인스트럭션) 그 효과는 프로그램이 인터럽트가 마치 발생하지 않았던 것처럼 계속해서 실행되는 것이다. 나머지 예외의 종류들(트랩, 오류 ,중단)은 지금의 인스트럭션을 실행한 결과로 동기적으로 일어난다. 우리는 이것을 오류 인스트럭션(faulting instruction)이라고 부른다.

트랩과 시스템 콜

  • 트랩은 의도적인 예외상황으로, 어떤 인스트럭션을 실행한 결과로 발생한다. 인터럽트 핸들러와 마찬가지로 트랩 핸들러는 제어를 다음 인스트럭션으로 리턴한다. 트랩의 가장 중요한 사용은 시스템 콜이라고 알려진 사용자 프로그램과 커널 사이의 프로시저와 유사한 인터페이스를 제공하는 것이다.
  • 사용자 프로그램은 파일을 읽거나(read), 새로운 프로세스를 만들거나(fork), 새 프로그램을 로드하고(execve), 현재 프로세스를 종료하는 등의 서비스를 종종 커널에게 요청할 필요가 있다. 이러한 커널 서비스의 제한된 접근을 하기 위해서 프로세서는 특별한 ‘n’ 인스트럭션을 제공하며 이들은 사용자 프로그램이 서비스 n을 요청하고자 할 때 사용자 프로그램이 사용할 수 있는 인스트럭션이다. syscall 인스트럭션을 실행하면 트랩 인자들을 해독하고 적절한 커널 루틴을 호출하는 예외 핸들러로 가게 한다. 프로그래머의 관점에서의 함수 호출과 동일하
  • 그렇지만 실제 구현은 매우 다르다. 보통의 함수는 사용자 모드에서 돌아가며, 이 때문에 이들이 실행할 수 있는 인스트럭션은 제한적이며, 이들은 호출하는 함수와 동일한 스택을 사용한다. 시스템 콜은 커널 모드에서 돌아가며, 이로 인해 커널 내에서 정의된 스택에 접근하며, 특권을 가진 인스트럭션을 실행할 수 있도록 해준다.

오류(fault error와 다르다)

  • 오류는 핸들러가 정정할 수 있을 가능성이 있는 에러조건으로 부터 발생한다. 오류가 발생하며 프로세서는 제어를 오류 핸들러로 이동해준다. 만일 핸들러가 에러조건을 정정할 수 있다면, 제어를 오류 핸들러로 이동해준다. 만일 핸들러가 에러조건을 정정할 수 있다면, 제어를 오류를 발생시킨 인스트럭션으로 돌려주어서 거기서부터 재실행한다. 그렇지 않다면 핸들러는 커널 내부의 abort루틴으로 리턴해서 오류를 발생시킨 응용프로그램을 종료한다.
  • 오류의 고전적인 예는 페이지 오류 예외로, 이것은 인스트럭션이 가상메모리 테이블을 참조했을 때 대응되는 실제 메모리 page가 존재하지 않는 상황이며, 따라서 디스크에서 가져와야 할 때 발생한다. 페이지는 가상 메모리의 연속적인 블록이다(대개 4KB). 페이지 오류 핸들러는 디스크에서 적절한 페이지를 로드해서 오류를 발생시킨 인스트럭션으로 제어를 넘겨준다. 이 인스트럭션이 다시 실행될 때 적절한 페이지는 메모리에 있게 되므로 인스트럭션은 오류를 발생시키지 않고 완료할 때까지 동작할 수 있다.

중단

  • 중단은 대개 DRAM이나 SRAM이 고장날 때 발생하는 패리티 에러와 하드웨어 같은 복구할 수 없는 치명적인 에러에서 발생한다. 중단 핸들러는 절대로 응용 프로그램으로 제어를 리턴하지 않는다. 핸들러는 제어를 응용프로그램을 종료하는 중단 루틴으로 넘겨준다.

8.13 리눅스/x86-64 시스템에서의 예외상황

리눅스/x86-64 오류와 중단

  • 나누기 에러 : 나누기 에러(예외번호 0) 는 응용이 0으로 나누려고 할 떄, 또는 나눗셈 인스트럭션의 결과가 목적지 오퍼랜드에 비해 너무 큰 경우 발생한다. Unix는 나누기 에러에서 복구하려는 시도를 하지 않으며, 대신 시스템을 중단하는 것을 선택한다. 리눅스 쉘은 대개 나누기 에러를 ‘부동 소수 예외’로 보고한다.
  • 일반 보호 오류 : 악명 높은 일반 보호 오류(예외번호 13) 여러가지 이유로 발생하며, 대개 프로그램이 가상 메모리의 정의되지않은 영역을 참조하거나 프로그램이 read-only 텍스트 세크먼트에 쓰려고 하기 때문이다. 리눅스는 이 오류에서 복구하려는 시도를 하지 않는다. 리눅스 쉘은 대개 ‘세그먼트 오류’ 같은 일반적인 텍스트로 오류를 알려준다.
  • 페이지 오류 : 페이지 오류(예외 14)는 오류 발생 인스트럭션이 재시작하는 예외의 예다. 핸들러는 필요한 디스크의 가상 메모리의 해당 페이지를 물리 메모리의 페이지로 매핑하고, 그 후 오류 인스트럭션을 다시 시작한다.
  • 머신체크 : 머신체크(예외 18)은 오류 인스트럭션을 실행하는 동안에 검출된 치명적인 하드웨어의 결과로 발생한다. 머신체크 핸들러는 절대로 제어를 응용 프로그램에게 돌려주지 않는다.

리눅스/x86-64 시스템 콜

  • 리눅스는 파일을 읽거나 쓸 때, 또는 새로운 프로세스를 만들 때 응용프로그램이 사용 할 수 있는 수 백개의 시스템 콜을 제공한다. 시스템 콜은 커널 점프테이블의 오프셋에 대응되는 유일한 정수를 갖는다.(이 점프테이블은 예외 테이블과는 다르다는 점에 유의)
  • 표준 C라이브러리는 대부분 시스템 콜에 대해서 편리한 래퍼(wrapper) 함수들을 제공한다. 래퍼함수는 인자들을 패키징하고, 커널을 적절한 시스템 콜 인스트럭션으로 트랩을 걸고, 호출하는 프로그램으로 시스템 콜의 리턴상태를 전달한다.
  • x86-64 시스템에서 리눅스 시스템을 직접 호출하기 위해서 이 인스트럭션을 어떻게 사용할 수 있는지 배우는 것은 상당히 흥미롭다. 리눅스 시스템 콜에 전달되는 모든 인자들은 스택보다는 범용레지스터를 통해서 이루어진다. 관습적으로, 레지스터 %rax는 시스템 콜 번호를 보관하며, %rdi, %rsi, %rdx, %r10, %r8, %r9에 최대 여섯 개의 인자들을 보관할 수 있다. ‘시스템 콜에서 리턴될 떄, 레지스터 %rcx, %r11 은 값들이 지워지고, %rax는 리턴값을 보관한다. -4,095에서 -1 사이의 음수 리턴값은 음수의 errno에 대응하는 에러를 나타낸다.

8.5 시그널

  • 리눅스 시그널이라고 알려진 상위 수준의 소프트웨어 형태의 예외적 제어흐름 이 시그널은 프로세스와 커널이 다른 프로세스를 중단하도록 한다.
  • 시그널은 작은 메세지 형태로, 프로세스에게 시스템 내에 어떤 종류의 이벤트가 일어났다는 것을 알려준다.
  • 각 시그널 타입은 특정 종류의 시스템 이벤트에 대응된다. 하위수준 하드웨어 예외는 커널의 예외 핸들러에 의해 처리되며, 정상적으로는 사용자 프로세스에서는 볼 수 없다. 시그널은 이러한 예외들을 사용자 프로세스에 노출해주는 메커니즘을 제공한다.

8.5.1 시그널 용어

  • 시그널을 목적지 프로세스로 전달하는 것은 두 단계로 이루어진다.
    • 시그널 보내기 : 커널은 목적지 프로세스의 컨텍스트 내에 있는 일부 상태를 갱신해서 시그널을 목적지 프로세스로 보낸다(배달한다). 시그널은 다음 두 가지 이유 중의 하나로 배달된다.
      1. 커널이 0으로 나누거나 자식 프로세스의 종료같은 시스템 이벤트를 감지한다.
      2. 어떤 프로세스가 커널에 명시적으로 시그널을 목적지 프로세스에 보낼 것을 요구하기 위해서 kill 함수를 호출하였다.
    • 시그널 받기 : 목적지 프로세스는 배달된 신호에 대해서 커널이 어떤 방식으로 반응 해야 할 떄 목적지 프로세스는 시그널을 받는다. 프로세서는 시그널 핸들러라고 부르는 사용자 수준 함수를 실행해서 시그널을 무시, 종류, 획득 할 수 있다.
  • 보내졌지만 아직 받지 않은 시그널은 펜딩(pending) 시그널 이라고 부른다. 시간 상으로 어떤 시점에서, 특정 타입에 대해 최대 한 개의 펜딩 시그널이 존재할 수 있다. 만일 어떤 프로세스가 타입 k의 펜딩 시그널을 가지고 있다면 이 프로세스로 다음에 발생하는 k타입의 시그널은 큐에 들어가지 않는다.(이들은 단순히 버려진다) 프로세스는 선택적으로 어떤 시그널의 수신을 블록할 수 있다. 어떤 시그널이 블록될 때 배달은 될 수 있지만 펜딩 시그널은 이 프로세스가 시그널의 블록을 끌 때까지는 수신되지 않는다.
  • 펜딩 시그널은 최대 한 번만 수신된다. 각 프로세스에 대해, 커널은 pending 비트 벡터 내에 펜딩하고 있는 시그널의 집합을 관리하며, blocked 비트 벡터내에서 블록된 시그널 집합을 관리한다. 커널은 pending 내에 비트 k를 타입 k의 시그널이 배달될 때마다 설정하며, 시그널 타입 k가 수신될 때마다 pending의 비트k를 0으로 만든다.

8.5.2 시그널 보내기

프로세스 그룹

  • 모든 프로세스는 정확히 한 개의 프로세스 그룹에 속하며, 이것은 양수 process group ID로 식별한다. getpgrp 함수는 현재 프로세스의 프로세스 그룹 ID를 리턴한다.
  • 기본적으로, 자식 프로세스는 자신의 부모와 동일한 프로세스 그룹에 속한다. 프로세스는 자신의 프로세스 그룹 또는 다른 프로세스의 그룹을 setpgid 함수를 사용해 변경할 수 있다.

키보드에서 시그널 보내기

  • Unix 쉘은 작업의 추상화를 사용해서 한 개의 명령줄을 해석한 결과로 만들어진 프로세스에 반영한다. 시간상의 어떤 시점에서도 최대 한 개의 포그라운드 작업과 0또는 그 이상의 백그라운드 작업이 존재한다.
  • 쉘은 각 작업마다 별도의 프로세스 그룹을 만든다. 일반적으로 프로세스 그룹 ID는 작업 내에 부모 프로세스 중 하나에서 가져온다.

8.5.3 시그널의 수신

  • 커널이 프로세스 P를 커널모드에서 사용자모드(시스템 콜에서 리턴하거나 문맥전환을 끝마치는 것과 같은)로 전환할 때, 커널은 프로세스 P에 대한 블록되지 않은 펜딩 시그널(pending & ~blocked) 의 집합을 체크한다. 만일 이 집합이 비어있다면(대개의 경우), 커널은 제어를 p의 논리 제어 흐름내의 다음 인스트럭션으로 전달한다.
  • 비어있지 않다면, 커널을 집햅 내 어떤 시그널 k를 선택해서(대개 가장 작은 k) p가 시그널 k를 수신하도록 한다. 시그널을 수신하면 프로세스는 어떤 동작을 개시한다. 일단 프로세스가 이 동작을 완료하면, 제어는 p의 논리 제어 흐름내의 다음 인스트럭션으로 돌아간다. 각 시그널 타입은 사전 정의된 기본 동작을 가지며, 이들은 다음 동작 중의 하나이다.
    • 프로세스가 종료한다.
    • 프로세스는 종료하고 코어를 덤프한다.
    • 프로세스는 SIGCONT 시그널에 의해 재시작 될 때 까지 정지한다.(지연한다)
    • 프로세스는 시그널을 무시한다.
  • 프로세스는 시그널과 연결된 기본동작을 signal 함수를 사용해서 수정할 수 있다.
  • signal 함수는 signum과 연결된 동작을 다음의 세 가지 방법 중의 하나로 바꿀 수 있다.
    • handler가 SIG_IGN이면, signum 타입의 시그널은 무시된다.
    • handler가 SIG_DFL이면, signum 타입의 시그널에 대한 동작은 기본 동작으로 돌아간다.
    • 그외의 경우, handler는 사용자가 정의한 함수의 시그널 핸들러 라고 부르는 주소가 되며, 이것은 프로세스가 signum 타입의 시그널을 수신할 때마다 호출될 것이다.핸들러의 주소를 signal 함수로 넘겨주는 방법은 기본 동작을 변경하는 것으로, 핸들러를 설치한다.(instailling), 핸들러의 호출은 시그널을 잡는다 (catching the signal) 고 부른다. 핸들러 실행은 시그널을 처리한다 (handling the signal)고 부른다.
  • 어떤 프로세스가 타입 k의 시그널을 잡을 때, 시그널 k를 위해 설치된 핸들러는 k에 설정된 한 개의 정수 인자를 사용해서 호출한다. 이 인자는 동일한 핸들러 함수가 서로 다른 종류의 시그널을 잡을 수 있도록 한다.
  • 핸들러가 return 문장을 실행할 때, 제어는(대개) 프로세스가 시그널의 수신으로 중단되었던 제어흐름 내의 인스트럭션으로 다시 전달된다. 여기서 ‘대개’라고 했는데, 일부 시스템의 경우 중단된 시스템 콜들이 에러가 발생하면 즉시 리턴하기 때문이다.

8.5.4 시그널 블록하기와 블록 해제하기

  • 리눅스는 시그널을 블록하기 위해 묵시적이고 명시적인 방법을 제공한다.
    • 묵시적 블록 방법 : 기본적으로 커널은 핸들러에 의해 처리되고 있는 유형의 모든 대기 시그널의 처리를 막는다.
    • 명시적 블록 방법 : 응용 프로그램들은 sigprocmask 함수와 이들의 도움 함수를 이용해서 시그널들을 명시적으로 블록하거나 블록해제 할 수 있다.

C++

RAII와 동적메모리 자원 관리

  • RAII(Resource Acuquisition Is Intialization)는 C++에서 강조되는 디자인 테크닉 중 하나이다.
  • C++ 프로그래밍에서 자원의 획득과 해제를 객체의 생명 주기와 결합하여 자동으로 관리하는 기법이다.
  • 이 기법은 메모리, 파일 핸들, 소켓 등 다양한 자원을 효율적으로 관리하고, 자원 누수(Resource Leak)를 방지하는 데 매우 유용하다.

기본 개념

  • RAII의 핵심 아이디어는 객체가 생성될 때 자원을 획득하고, 객체가 소멸될 때 자원을 해제하는 것이다.
  • 이 방식은 C++ 소멸자(destructor) 호출 메커니즘을 활용하여 자원을 자동으로 정리해준다.

장점

  1. 자동 자원 해제
    • 객체의 생명 주기가 끝날 때 자동으로 자원을 해제하여 자원 누수를 방지한다.
  2. 예외 안정성
    • 예외가 발생하더라도 소멸자가 호출되어 자원이 확실히 해제되므로, 예외 안정성을 높인다.
  3. 코드 간결성
    • 명시적으로 자원을 해제하는 코드를 줄여 코드가 간결해진다.

가상함수

추상 클래스(abstract)

  • 추상 클래스(Abstract Class)는 C++에서 하나 이상의 순수 가상 함수(Pure Virtual Function)을 포함 하는 클래스로, 객체를 직접 생성할 수 없으며, 상속을 통해서만 사용할 수 있다.
  • 추상 클래스는 공통 인터페이스를 정의하고, 이를 상속받는 클래스들이 인터페이스의 구체적인 동작을 구현하도록 강제한다.

특징

  1. 순수 가상 함수(Pure Virtual Function)
    • 추상 클래스는 최소한 하나의 순수 가상 함수를 포함해야 한다. 순수 가상 함수는 클래스 내에서 구현되지 않고, 파생 클래스에서 반드시 구현해야 하는 함수이다. 순수 가상 함수는 함수 선언 뒤에 ‘= 0’을 붙여서 정의한다.
  2. 객체 생성 불가
    • 추상 클래스는 객체를 직접 생성할 수 없습니다. 객체를 생성하려고 하면 컴파일 오류가 발생한다.
  3. 상속을 통해 구현
    • 추상 클래스를 상속받는 파생 클래스는 순수 가상 함수를 모두 구현해야 객체를 생성할 수 있다. 그렇지 않으면, 파생 클래스도 추상 클래스가 된다.

Virtual 키워드

  • virtual 키워드는 메서드, 속성, 인덱서 또는 이벤트 선언을 수정하고 파생 클래스에서 재정의하도록 허용하는 데 사용된다.
  • 상속 관계에서 자식이 해당 함수를 오버라이딩 하기 위해 사용한다.
728x90

'Study > TIL(Today I Learned)' 카테고리의 다른 글

24.09.09 코테, CS  (1) 2024.09.09
24.09.06 CSAPP, CS  (4) 2024.09.06
24.09.04 CS, C++  (0) 2024.09.04
24.09.03 CS, C++  (0) 2024.09.03
24.09.02 CS, 언리얼  (1) 2024.09.02