운영체제(이론) - 핀토스-2(기본)

연도·2024년 5월 31일
0

User Stack

프로세스의 사용자 모드에서 사용되는 스택 영역.

스택은 함수 호출, 로컬 변수 저장, 함수 호출 간의 컨텍스트유지하는 데 사용되는 메모리 구조. 유저 스택은 프로세스의 주소 공간에 할당되며, 운영체제의 커널 모드에서 사용하는 커널 스택과 구분된다.

주요 기능 and 특징

  1. 함수 호출 관리
  • 함수 호출 시 각 함수의 호출 정보 저장.
  • 함수가 호출될 때마다 함수의 반환 주소, 매개변수, 로컬 변수가 스택에 푸시(push) 된다.
  • 함수가 종료되면 반환 주소로 돌아가고, 스택에서 해당 함수의 데이터를 팝(pop)한다.
  1. 로컬 변수 저장
  • 함수 내에서 선언된 로컬 변수는 유저 스택에 저장.
  • 함수가 호출될 때마다 새로운 스택 프레임이 생성되고, 여기에 로컬 변수가 포함.
  1. 재귀 함수 지원
  • 재귀 함수 호출 시 각 재귀 호출마다 새로운 스택 프레임 생성
  • 이는 각 호출의 컨텍스트를 유지하는데 필요.
  1. 주소 공간
  • 유저 스택은 프로세스의 가상 주소 공간의 일부
  • 일반적으로 스택은 높은 주소에서 낮은 주소 방향으로 성장.

스택 구조

유저 스택은 LIFO 구조를 따른다.

  1. 반환주소

함수 호출이 완료되면 돌아가야 할 주소를 저장.

  1. 매개변수

함수 호출 시 전달된 인수들이 저장.

  1. 프레임 포인터

이전 스택 프레임의 시작 주소를 가리킨다.

  1. 로컬 변수

함수 내에서 선언된 변수들이 저장.

파일 디스크립터

운영체제가 파일 or 입출력 자원(파이프, 소켓, 터미널 등)을 추적하기 위해 사용되는 추상적 식별자.

파일 디스크립터는 프로세스가 열어놓은 모든 파일에 대해 고유한 비정수(정수형) 값을 반환하여 이를 통해 파일에 대한 읽기, 쓰기, 닫기 등의 작업을 수행할 수 있다.

주요 특징

  1. 정수형 식별자
  • 파일 디스크립트는 프로세스 내에서 고유한 작은 정수로 표현
  • 일반적으로 파일 디스크립터 값은 0, 1, 2부터 시작하여 순차적 할당
  1. 운영체제 관리
  • 운영체제가 파일 디스크립터를 통해 파일 테이블 관리
  • 파일 테이블은 파일 디스크립터와 파일의 메타데이터(파일 상태, 위치, 권한 등)을 매핑
  1. 유니버셜 입출력 인터페이스
  • 모든 파일, 소켓, 파이프, 장치 등의 입출력 작업은 파일 디스크립터를 통해 수행.
  • 이러한 통일된 인터페이스 덕분에 다양한 입출력 장치와의 상호작용이 간편해진다.
  1. 표준 입력 (Standard Input, stdin): (0)
    • 기본적으로 키보드 입력을 읽습니다.
    • 예: 터미널에서 사용자 입력을 받음.
  2. 표준 출력 (Standard Output, stdout): (1)
    • 기본적으로 터미널에 출력합니다.
    • 예: 프로그램의 결과를 화면에 출력.
  3. 표준 오류 (Standard Error, stderr): (2)
    • 기본적으로 터미널에 오류 메시지를 출력합니다.
    • 예: 프로그램 오류를 화면에 출력.

파일 디스크립터는 ‘open()’, ‘read()’, ‘write()’, ‘close()’ 등의 시스템 콜을 통해 사용된다.

이를 통해 프로세스는 파일 및 다른 입출력 자원과 상호 작용o

Cache

데이터의 임시 저장소. 주로 데이터 접근 속도를 향상 시키기 위해 사용. 메모리 계층 구조에서 CPU와 메인 메모리(RAM) 사이에 위치 하여, 자주 사용되는 데이터를 보다 빠르게 접근할 수 있도록 한다.

CPU가 데이터를 처리하는 속도를 최대로 끌어올리는 것입니다.

주요 특징

속도 - 매우 빠른 속도로 동작하며, 일반적으로 SRAM 기술 사용

크기 - 용량이 작지만 매우 빠른 메모리

계층 구조

  • L1 캐시: 가장 빠르고, CPU 코어 내부에 위치하며, 용량이 가장 작습니다.
  • L2 캐시: L1 캐시보다 크고 느리며, CPU 코어 내부 또는 외부에 위치할 수 있습니다.
  • L3 캐시: L2 캐시보다 크고 느리며, 여러 CPU 코어가 공유할 수 있는 경우가 많습니다.

종류

  1. CPU 캐시:
    • CPU 내부 또는 근처에 위치하며, 명령어와 데이터를 빠르게 공급합니다.
    • L1, L2, L3 캐시로 구성됩니다.
  2. 디스크 캐시:
    • 디스크 접근 속도를 높이기 위해 메모리의 일부를 디스크 캐시로 사용합니다.
    • 파일 시스템의 데이터 블록을 캐싱하여 디스크 I/O 성능을 향상시킵니다.
  3. 웹 캐시:
    • 웹 서버나 브라우저가 자주 접근하는 웹 페이지나 데이터를 캐싱하여 웹 페이지 로딩 시간을 줄입니다.
    • 프록시 서버 캐시, 브라우저 캐시 등이 있습니다.

원자적 연산

중단되지 않고 완전하게 실행되는 연산. 즉, 다른 연산이 개입 하거나 중간에 상태를 볼 수 없도록 보장된 연산. 원자적 연산은 주로 다중 스레드 환경에서 동기화 문제를 해결하기 위해 사용된다.

주요 특징

  1. 불가분성
  • 원자적 연산은 실행이 시작되면 다른 어떤 연산도 개입x. 연산이 끝날 때까지 완전하게 독립적 수행.
  1. 동기화
  • 다중 스레드가 공유 자원에 접근할 때, 원자적 연산은 데이터의 일관성과 무결성 유지
  • 교착 상태나 경쟁 상태 방지.
  1. 하드웨어 지원:
  • 원자적 연산은 하드웨어 수준에서 지원되는 경우가 많다. 많은 CPU는 원자적 명령어를 제공하여 이러한 연산 보장.

원자적 연산의 예

단일 연산의 예

  1. 정수 증가 연산 (increment):
    • x++ 연산이 원자적으로 수행되면, 다른 스레드가 x의 값을 읽거나 변경하지 못하도록 보장됩니다.
    • 하드웨어 명령어: lock inc [address] (x86 아키텍처)
  2. 비트 단위 연산 (bitwise operation):
    • 비트 단위의 OR, AND, XOR 연산은 원자적으로 수행될 수 있습니다.

복합 연산의 예

  1. Compare-and-Swap (CAS):

    • 두 값을 비교하고, 동일하면 새로운 값으로 교체합니다.
    bool compare_and_swap(int* ptr, int old_value, int new_value) {
        if (*ptr == old_value) {
            *ptr = new_value;
            return true;
        }
        return false;
    }
  • CAS는 락프리 알고리즘의 기초가 된다.
  1. Fetch-and-Add:
  • 변수의 현재 값을 반환하고, 주어진 값을 원자적으로 더합니다.
int fetch_and_add(int* ptr, int value) {
    int old_value = *ptr;
    *ptr += value;
    return old_value;
}

원자적 연산의 구현

  1. 하드웨어 명령어
  • 많은 프로세서에서 원자적 연산을 지원하는 특수한 하드웨어 명령어를 제공합니다.
  • 예: xchg, lock cmpxchg (x86 아키텍처), ldrex/strex (ARM 아키텍처)
  1. 소프트웨어 라이브러리
  • 원자적 연산을 위한 라이브러리 함수 제공
  • 예: C++ 표준 라이브러리의 <atomic> 헤더

원자적 연산의 중요성

  1. 동기화 문제 해결
  • 다중 스레드 환경에서 데이터 경쟁을 방지하여 안전하게 공유 자원에 접근할 수 있도록 합니다.
  1. 성능 향상
  • 락을 사용하는 것보다 원자적 연산을 사용하는 것이 더 효율적일 수 있습니다. 락은 오버헤드가 크고 교착 상태를 초래할 수 있기 때문입니다.
  1. 단순화된 코드
  • 원자적 연산을 사용하면 복잡한 락 기법을 피하고, 코드를 간단하고 이해하기 쉽게 유지할 수 있습니다.

REX 레지스터

x86-64 아키텍처에서 64비트 명령어 확장을 위해 도입된 특별한 접두사입니다. REX는 "Register Extension"의 약자로, 추가적인 레지스터와 64비트 연산을 지원하기 위해 사용됩니다.

REX 접두사는 2003년에 AMD에 의해 처음 도입되었으며, 인텔의 EM64T(현재의 Intel 64) 아키텍처에서도 채택되었습니다.

주요 기능과 특징

  1. 64비트 연산 지원:
    • REX 접두사는 명령어가 64비트 연산을 수행하도록 합니다.
    • 이를 통해 레지스터와 연산이 64비트 크기로 확장됩니다.
  2. 추가 레지스터 접근:
    • 기본 x86 아키텍처에서는 8개의 일반 목적 레지스터(EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP)만 사용됩니다.
    • REX 접두사를 사용하면 8개의 추가 레지스터(R8-R15)에 접근할 수 있습니다.
  3. SIB 바이트 확장:
    • SIB(Scale, Index, Base) 주소 지정 모드에서 추가적인 인덱스 레지스터와 베이스 레지스터를 사용할 수 있도록 확장됩니다.

32/64비트 운영체제

  • 주소 공간 및 메모리 관리: 32비트는 최대 4GB 메모리, 64비트는 매우 큰 주소 공간과 더 많은 메모리를 지원합니다.

  • 성능: 64비트 운영체제가 더 많은 레지스터와 64비트 데이터 단위를 사용하여 성능이 향상됩니다.

  • 소프트웨어 호환성: 64비트 운영체제는 대부분의 32비트 애플리케이션을 실행할 수 있지만, 반대의 경우는 불가능합니다.

  • 하드웨어 요구 사항: 64비트 운영체제는 64비트 CPU가 필요하며, 더 많은 RAM을 지원합니다.

  • 보안: 64비트 운영체제는 더 강력한 보안 기능을 제공합니다.

Interrupt

컴퓨터 시스템에서 CPU가 현재 작업을 중단하고, 발생한 특정 이벤트를 처리하도록 하는 메커니즘.

인터럽트는 HW, SW의해 발생하며, 이벤트를 처리한 후 CPU 는 중단된 작업을 재개한다.

주요 특징

  1. 즉각적인 응답
  • 인터럽트는 중요한 이벤트에 대해 즉각적으로 응답할 수 있게 합니다.
  • 예: 키보드 입력, 마우스 클릭, 네트워크 패킷 도착 등
  1. 비동기적 작동
  • 인터럽트는 CPU의 명령어 실행과 독립적으로 발생할 수 있습니다.
  • CPU는 인터럽트 발생 시 현재 작업을 중단하고 인터럽트를 처리합니다
  1. 우선순위
  • 인터럽트는 우선순위를 가질 수 있으며, 높은 우선순위의 인터럽트가 먼저 처리됩니다.
  • 예: 하드웨어 인터럽트는 일반적으로 소프트웨어 인터럽트보다 높은 우선순위를 가집니다.

요약

  • 인터럽트는 CPU가 현재 작업을 중단하고 중요한 이벤트를 처리할 수 있게 하는 메커니즘입니다.

  • 유형

하드웨어 인터럽트, 소프트웨어 인터럽트, 예외.

  • 처리 과정

인터럽트 발생 -> 인터럽트 요청 확인 -> 작업 상태 저장 -> ISR 실행 -> 작업 상태 복원.

  • 중요성

효율적인 시스템 운영, 실시간 처리, 다중 작업 지원.

Segmentation Fault

프로그램이 허용되지 않은 메모리 영역에 접근하려고 할 때 발생하는 오류. 이는 주로 프로세스가 자신의 메모리 주소 공간 외부를 참조하려고 할 때 운영체제가 이를 감지하고 해당 프로세를 중단시킴으로 발생

주요 원인

  1. 널 포인터 접근
  • 초기화되지 않은 포인터를 사용하여 메모리에 접근하려 할 때 발생합니다.
#include <stdio.h>

int main() {
    int *ptr = NULL;  // 널 포인터 초기화
    *ptr = 10;        // 널 포인터에 값 할당 시도
    return 0;
}
  1. 잘못된 포인터 사용
  • 유효하지 않은 메모리 주소를 가리키는 포인터를 사용하여 메모리에 접근하려 할 때 발생
int *ptr; *ptr = 10;
  1. 버퍼 오버플로우
  • 배열이나 버퍼의 경계를 넘어서 데이터를 읽거나 쓰려고 할 때 발생
#include <stdio.h>

int main() {
    char buffer[10];
    for (int i = 0; i <= 10; i++) {
        buffer[i] = 'a';  // 버퍼 오버플로우
    }
    return 0;
}
  1. 스택 오버플로우
  • 너무 깊은 재귀 호출이나 큰 지역 변수를 선언하여 스택 공간을 초과할 때 발생합니다.
void func() { func(); }
  1. 잘못된 메모리 해제 후 접근
  • 이미 해제된 메모리 영역에 다시 접근하려고 할 때
free(ptr); *ptr = 10;

예방 및 모범 사례

  1. 포인터 초기화
  • 포인터를 선언할 때 항상 초기화하고, NULL 포인터에 접근하지 않도록 주의.
  1. 메모리 할당 검증
  • 동적 메모리 할당 시 반환 값을 항상 확인하고, 할당 실패를 처리
  1. 경계 검사
  • 배열이나 버퍼를 사용할 때 항상 경계를 검사하여 오버플로우 방지
  1. 정적 분석 도구 사용
  • 컴파일 시 정적 분석 도구를 사용하여 잠재적인 메모리 접근 오류를 사전에 발견

요약

  • 세그멘테이션 폴트는 프로그램이 허용되지 않은 메모리 영역에 접근하려 할 때 발생하는 오류입니다.

  • 주요 원인으로는 널 포인터 접근, 잘못된 포인터 사용, 버퍼 오버플로우, 스택 오버플로우, 잘못된 메모리 해제 후 접근 등이 있습니다.

  • 세그멘테이션 폴트는 런타임 오류로, 프로그램 실행 중 발생합니다.

  • GDB와 Valgrind 같은 디버깅 도구를 사용하여 세그멘테이션 폴트를 진단하고 수정할 수 있습니다.

  • 포인터 초기화, 메모리 할당 검증, 경계 검사 등 예방 방법을 통해 세그멘테이션 폴트를 방지할 수 있습니다.

0개의 댓글