IP와 TCP

  • IP(Internet Protocol)
    • 인터넷에서 데이터를 전송하기 위한 기본 프로토콜
    • 데이터를 작은 조각인 패킷으로 나누어 네트워크를 통해 전송
    • 특징:
      • 비신뢰적(Unreliable): 데이터가 손실되거나 순서가 뒤바뀔 수 있다.
      • 최선의 노력(Best-Effort): 데이터를 가능한 빨리 전송하지만, 성공 보장 X.
      • 경로 설정: 패킷이 목적지까지 도달할 최적 경로를 찾음.
  • TCP(Transmission Control Protocol)
    • IP 위에서 동작하는 프로토콜
    • 신뢰적인 데이터 전송 서비스 제공
    • 특징:
      • 데이터 손실 방지: 손실된 데이터를 재전송합니다.
      • 데이터 순서 보장: 원래 순서대로 데이터를 정렬합니다.
      • 무결성 확인: 데이터가 손상되지 않았는지 확인합니다.

예를 들면,

IP가 배달원이라면 TCP는 매니저 역할이다.

  • IP는 "주소만 보고" 패킷(데이터 조각)을 전달 → 잘못된 경로로 가거나 손실될 수 있음.
  • TCP는 패킷이 제대로 도착했는지 확인, 문제가 생기면 수정. 모든 패킷을 순서대로 정리하여 완전한 데이터로 조립.

TCP가 신뢰적인 데이터 전송을 제공하는 방법

  • 기다리는 순서 번호를 가진 순서에 맞는 세그먼트의 도착. 그 이전 순서까지의 모든 데이터들은 이미 확인 응답 된 상태라면?
    • 지연 ACK. 추가 세그먼트를 위해 대기. 만약 다른 세그먼트가 오지 않는다면 ACK 보냄. 온다면 두개를 합쳐서 ACK 보냄.
  • 순서에 맞는 세그먼트의 도착. 이미 하나의 순서에 맞는 세그먼트가 있다면?
    • 하나의 누적된 ACK를 보냄
  • 지연 ACK:
    • ACK 전송을 최대 500ms까지 지연하여 추가 데이터를 기다리는 것.
    • TCP에서는 네트워크 트래픽을 줄이기 위해 지연 ACK 메커니즘을 사용한다.

예시

  • 송신자가 100바이트 크기의 데이터를 두 번에 나누어 보낸다고 가정하자
    • 첫 번째 세그먼트(Sequence Number = 1~100) 도착.
    • 두 번째 세그먼트(Sequence Number = 101~200) 아직 도착하지 않음.
  • TCP Receiver의 행동:
    • 첫 번째 세그먼트에 대해 즉시 ACK를 보내지 않음.
    • 대신 500ms 동안 대기하며, 두 번째 세그먼트가 도착하는지 확인한다.
      • 두 번째 세그먼트가 500ms 내에 도착: 두 세그먼트에 대해 한 번의 ACK를 보냄.
      • 두 번째 세그먼트가 500ms 내에 도착하지 않음: 첫 번째 세그먼트에 대해 ACK를 보냄.

TCP와 GBN 프로토콜, SR 프로토콜

  • Go-Back-N(GBN)의 특징
    • 누적 ACK:
      • 송신자는 수신자로부터 가장 최근에 성공적으로 수신된 순서대로 된 데이터에 대한 ACK만 받음.
      • 예:
        • 세그먼트 1, 2, 3, 4, 5를 보냈을 때, 세그먼트 3까지 성공적으로 수신되었으면, 수신자는 "세그먼트 3까지 잘 받았음"이라는 ACK를 보낸다.
        • 송신자는 ACK 3을 기준으로 4, 5 이후의 데이터를 전송 상태로 간주.
    • 손실 시 전체 재전송:
      • 만약 세그먼트 4가 손실되었다면, 세그먼트 4 이후의 모든 데이터를 재전송. (비효율적)
  • Selective Repeat(SR)의 특징
    • 개별 ACK:
      • 수신자는 각 세그먼트마다 별도의 ACK를 보냄.
      • 예:
        • 세그먼트 1, 2, 3, 4, 5 중 세그먼트 4가 손실되었을 때, 수신자는 1, 2, 3, 5에 대해 ACK를 보낸다.
        • 송신자는 손실된 세그먼트(4)만 재전송.
    • 손실된 데이터만 재전송:
      • 개별 ACK 덕분에 손실된 데이터만 재전송 가능하며, 네트워크 리소스를 절약할 수 있음.

TCP에서의 혼합 동작

TCP는 위의 두 방식에서 효율적인 요소를 가져와 혼합하여 동작한다.

 

TCP의 기본 동작 (GBN 기반)

  • TCP는 기본적으로 누적 ACK를 사용.
    • 수신자는 순서대로 도착한 가장 마지막 세그먼트에 대한 ACK만 송신자에게 보냄
    • 순서가 틀린 세그먼트가 도착하면, 즉시 중복 ACK(Duplicate ACK) 전송(다음에 기대하는 데이터의 순서 번호가 포함됨)

TCP의 효율화 (SR 요소 추가)

  • TCP는 일부 구현에서 순서가 틀린 데이터도 임시로 저장 가능.
    • 송신자는 손실된 데이터만 재전송 → 효율 개선.

Selective Acknowledgment (SACK) 옵션

  • 수신자는 누락된 데이터 외에도, 어떤 데이터가 성공적으로 수신되었는지 세부적으로 표시
  • 송신자는 이 정보를 기반으로 손실된 데이터만 재전송

HTTP와 비지속적 연결

특징)

  • 각 요청-응답 쌍을 개별 TCP 연결에서 처리.
  • 요청된 객체를 서버가 전송한 후 연결이 종료됨.
  • HTML 파일, 이미지 등 여러 객체를 다운로드하려면 객체마다 새로운 TCP 연결을 생성.

예시)

  1. HTTP 클라이언트 프로세스가 서버80번 포트로 TCP 연결
  2. 소켓을 통해 경로를 포함한 HTTP 요청 메시지를 서버로 전달
  3. HTTP 서버 프로세스가 메시지 수신, 경로를 HTTP 응답 메시지에 캡슐화하여 클라이언트로 전송
  4. 클라이언트가 응답 메시지 수신
  5. TCP 연결 종료

 

  • 만약 웹사이트가 1개의 html과 10개의 이미지로 구성되어 있다면, 총 11개의 TCP 연결 생성 → 각 TCP 연결은 한 번의 요청과 응답만 처리함.

 

장단점)

  • 장점: 구현이 간단하며, 요청-응답이 독립적이라 연결 관리를 최소화.
  • 단점:
    • 각 객체마다 새로운 TCP 연결을 설정해야 하므로 추가적인 오버헤드 발생.
    • RTT(왕복 시간)가 객체마다 2번씩 필요하므로 성능이 저하됨.
    • 서버와 클라이언트 모두에서 TCP 버퍼와 변수 관리에 부담.

 

 

HTTP와 지속적 연결

특징)

  • 하나의 TCP 연결을 여러 요청-응답 쌍에서 재사용.
    • 기존 비지속적 연결에서 11개의 TCP 연결을 생성했던 것과 달리, 하나의 연결에서 연속적으로 전송(파이프라이닝).
  • 연결이 일정 시간 동안 유지되며, 동일한 서버에서 여러 객체를 전송 가능.
  • HTTP 1.1의 기본 설정.

장단점)

  • 장점:
    • TCP 연결 설정/종료에 소요되는 오버헤드 감소.
    • 객체마다 2 RTT 대신 전체 페이지에 대해 최소 2 RTT로 처리 가능.
    • 서버와 클라이언트의 자원 사용 감소.
  • 단점:
    • 장시간 연결 유지로 인해 연결 타임아웃 설정이 필요.
    • 잘못된 연결로 인해 여러 요청이 실패할 가능성.

 

HTTP 메시지 포맷

보통 HTTP 요청 메시지의 형식은 다음과 같다.

GET /somedir/page.html HTTP/1.1
Host: www.someschool.edu
Connection: close
User-agent: Mozilla/5.0
Accept-language: fr

 

요청 라인(Request Line): HTTP 요청 메시지 내 첫번째 줄

  • 메서드(보통 GET), url, http 버전으로 구성돼있음.

 

헤더 라인(Header Lines): HTTP 요청 메시지 내 요청 라인을 제외한 나머지

Web proxy caches 때문에 꼭 필요함.

  • Host: 요청 객체가 위치한 서버의 호스트 이름 지정.
  • Connection: 연결 유지 여부 (예: close는 비지속적 연결 요청).
  • User-agent: 요청을 보낸 브라우저 정보
  • Accept-language: 사용자 선호 언어 설정

 

 

 

 

 

오래된 Unix 파일 시스템

슈퍼블럭(S): 볼륨 크기, 아이노드 개수, 포인터 등

 

장점

  • 단순함.

 

단점

 

  • 낮은 성능
  • 디스크를 RAM처럼 사용: 데이터를 임의의 위치에 저장하여 잦은 헤드 이동 발생
  • 512바이트의 작은 블록 크기
  • 단편화: 빈 공간 관리가 효율적이지 못해 디스크 공간이 조각나고, 파일이 여러 조각으로 나뉘어 저장.

이러한 데이트 블럭 영역에서 B와 D를 삭제하면

 

이런 상태가 된다.

데이터가 삭제되어도 연속된 청크가 아닌 두 블럭으로 단편화되어, 후 네 블럭으로 구성된 파일 E가 들어와도

 

이런 형태를 유지하기에, 파일 E를 읽을 때 포인터의 위치를 변경해줘야하는 번거로움을 겪는다.

 

 

FFS의 핵심 아이디어: 디스크에 대한 이해

  • 디스크의 물리적인 특성을 고려하여 파일 시스템의 성능 개선.
  • 기존 오래된 Unix 파일 시스템 인터페이스와 호환성을 유지함.

 

 

FFS의 주요 개선 사항

  • 실린더 그룹
    • 디스크를 실린더 그룹으로 나누어 데이터와 메타데이터를 지역적으로 저장.
    • 동일한 디렉터리에 속한 파일을 가깝게 배치하여 디스크 헤드 이동 최소화.
  • 관련 데이터의 근접 배치
    • 디렉터리와 해당 파일은 동일한 실린더 그룹에 배치.
    • 관련 없는 파일은 서로 다른 실린더 그룹에 배치.
  • 슈퍼블록 복제
    • 각 실린더 그룹에 슈퍼블록 복제본을 저장하여 데이터 손실 방지.
  • 비트맵 사용
    • 아이노드와 데이터 블록의 할당 상태를 추적하여 단편화 감소.
  • 대용량 파일 예외 처리
    • 큰 파일을 여러 그룹에 분산 저장하여 지역성 유지 및 성능 저하 완화.
  • 서브 블록
    • 작은 파일을 저장할 때 4KB 블록 전체를 낭비하지 않고 필요한 만큼의 서브 블록만 할당.
  • 매개화된 배치
    • 디스크의 성능 매개변수를 검출하여 최적의 배치 간격을 결정하는 매개화 기법 사용.

 

FFS의 성능 원칙

  • 지역성
    • 관련 데이터가 디스크 상에서 물리적으로 가까운 위치에 배치되어야 성능이 향상.
    • 디렉터리 트리 상의 파일 접근 패턴을 분석하여 지역성을 확인.
  • 할당 정책의 합리성
    • 디렉터리 내 파일 접근의 지역성이 실제로 높음.
    • 디렉터리와 파일 간의 물리적 근접성이 성능에 기여.

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <vector>
#include <algorithm>
 
using namespace std;
typedef pair<int, int> p;
 
int n, answer;
int highest, lowest;
int qcnt[1001];
vector<int> qual;
 
void init() {
    int input;
    cin >> n;
 
    for (int i = 0; i < n; i++) {
        cin >> input;
        if (qcnt[input] == 0) qual.push_back(input);
        qcnt[input]++;
    }
 
    sort(qual.begin(), qual.end());
 
    lowest = 0;
    highest = qual.size() - 1;
}
 
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(NULL);
 
    init();
 
    while (1) {
        if (lowest > highest) break;
        int low_cnt = qcnt[qual[lowest]];
        int high_cnt = qcnt[qual[highest]];
        if (lowest == highest) {
            answer += qual[highest] * high_cnt;
            qcnt[highest] = 0;
            lowest++;
            continue;
        }
        if (low_cnt < high_cnt) {
            answer += qual[highest] * low_cnt * 2;
            qcnt[qual[highest]] -= low_cnt;
            qcnt[qual[lowest]] = 0;
            lowest++;
        }
        else {
            answer += qual[highest] * high_cnt * 2;
            qcnt[qual[lowest]] -= high_cnt;
            qcnt[qual[highest]] = 0;
            highest--;
        }
    }
 
    printf("%d\n", answer);
 
    return 0;
}

'Algorithm' 카테고리의 다른 글

[boj] 은하철도_17250 java  (1) 2025.02.24
[boj] ACM Craft_1005 java  (2) 2025.01.26
Boj_ 회의실 배정 4 java  (3) 2024.11.11
[boj] 이차원 배열과 연산17140 JAVA  (0) 2024.10.27
[boj] 연구소 3_17142 JAVA  (0) 2024.10.21

페이징, 왜 느려질까?

운영체제에서 가상 메모리를 구현하는 대표적인 방식은 페이징(Paging) 이다.

 

페이징: 프로세스의 주소 공간을 일정 크기(페이지 단위)로 나누어 물리 메모리에 매핑하는 기법

문제점: 매번 가상 주소 → 물리 주소 변환 시 페이지 테이블을 참조해야 하고, 그때마다 메모리를 추가로 읽게 되므로 성능 저하가 발생

예) 모든 메모리 접근(load/store)마다 ‘페이지 테이블’을 찾아봐야 한다면? CPU 성능이 아무리 좋아도 속도가 크게 느려질 수밖에 없습니다.


TLB(Translation Lookaside Buffer)의 등장

이에, 느려지는 페이징 주소 변환 문제를 해결하기 위해 등장한 것이 바로 TLB다.

  • TLB: CPU 내부에 있는 작은 하드웨어 캐시
    • 자주 참조되는 ‘가상 주소 ↔ 물리 주소 변환 정보’를 저장
    • 일종의 ‘주소 변환 캐시’ 역할
  • TLB 히트: 원하는 변환 정보가 TLB에 이미 있다면, 페이지 테이블을 거치지 않아도 바로 물리 주소를 얻어낼 수 있어 빠른 접근이 가능
  • TLB 미스: TLB에 변환 정보가 없으면(=미스), CPU나 운영체제가 페이지 테이블을 다시 참조하고 TLB에 정보를 추가(갱신)해야 하므로 느려짐

따라서 TLB가 자주 히트할수록 주소 변환 속도가 빨라지고, 페이징의 성능이 크게 향상된다.

예시: 배열 순차 접근

  1. 연속된 배열이 가상 메모리 상에서 ‘한 페이지’에 여러 원소가 담겨 있음
  2. 배열의 첫 원소를 참조할 때에는 TLB 미스가 발생 → 페이지 테이블 접근 후 TLB 갱신
  3. 같은 페이지 안에 있는 나머지 원소들을 계속 참조하면 TLB 히트가 연이어 발생
    • 공간 지역성(spatial locality): 배열 원소들이 페이지 내에서 인접하므로, 한 번 주소 변환 정보를 TLB에 가져오면 그 페이지 안의 다음 번지는 빠르게 접근 가능

결과적으로, 배열 전체 접근 시 처음 몇 번만 미스가 발생하고 대부분은 히트를 기록하여 성능이 크게 개선됨.


TLB 미스 처리: 하드웨어 vs 소프트웨어

TLB 미스가 발생하면 처리하는 데 크게 하드웨어소프트웨어(운영체제) 방식이 있다.

  1. 하드웨어 관리(예: CISC, x86 등)
    • CPU 내부에서 페이지 테이블 위치를 알고 있어, 미스 발생 시 직접 페이지 테이블을 참조해 TLB를 업데이트
    • 이후 명령어를 재실행해 TLB 히트를 유도
  2. 소프트웨어 관리(예: RISC 계열, MIPS 등)
    • TLB 미스가 발생하면 ‘예외(Trap)’를 발생시켜 운영체제(커널 모드) 코드가 페이지 테이블을 확인 후 TLB를 갱신
    • TLB 업데이트가 끝나면 다시 해당 명령어를 재실행하여 TLB 히트를 발생시킴

어느 방식이든 최종 결과는 같지만, 하드웨어 관리 방식은 프로세서가 직접 처리하고, 소프트웨어 관리 방식은 운영체제 트랩 핸들러가 담당한다.


문맥 교환 (Context Switch) 과 TLB

멀티프로그램 환경에서 운영체제가 문맥 교환을 할 때도 TLB 문제가 발생한다.

  • 다른 프로세스로 전환되면, 이전 프로세스의 가상 주소 ↔ 물리 주소 변환 정보가 더 이상 유효하지 않을 수 있음
  • 해결 방법:
    1. 문맥 전환 시 TLB를 전부 비워(모든 valid 비트를 0으로) 새 프로세스가 TLB를 새로 채우도록 함
    2. ASID(Address Space ID)를 TLB 엔트리에 추가하여, 프로세스 식별자(혹은 유사한 ID)와 매핑 정보를 함께 저장. 이렇게 하면 프로세스 간 TLB 항목을 구분할 수 있음

TLB 교체 정책

TLB도 한정된 크기의 캐시이므로, 새 항목을 넣어야 할 때 어느 항목을 제거할지 결정해야 한다.

  • LRU(Least Recently Used): 가장 오래 안 쓰인 항목을 제거
  • Random: 무작위로 항목을 교체 (오히려 어떤 패턴에서는 더 좋은 경우가 있음)

목표는 TLB 미스를 최소화하여 성능을 높이는 것이며, 실제 구현에서는 하드웨어 복잡성이나 응용 프로그램 특성에 따라 달라진다.


 

마무리

  • TLB는 가상 메모리 체계를 지원하는 현대 CPU 구조에서 필수적인 장치
  • 페이징의 ‘추가 메모리 접근 비용’을 억제하여 가상 메모리를 사실상 “매우 빠른” 방식으로 사용할 수 있도록 해줌
  • 프로그램 특성(지역성)과 TLB 정책이 잘 맞으면 페이징 성능이 비약적으로 향상
  • 하지만 한 번에 너무 많은 페이지를 건드리거나(TLB 범위를 넘는 경우), 문맥 교환이 잦으면 TLB 미스 오버헤드가 늘어나므로 주의가 필요

멀티프로세서 스케줄링(Multiprocessor Scheduling)


 

멀티프로세서 스케줄링(Multiprocessor Scheduling)은 여러 개의 CPU가 동시에 있는 컴퓨터에서 CPU 시간을 어떻게 효율적으로 분배하는 것에 대한 문제다. 단일 CPU만 있던 시절에는 한 번에 하나의 프로그램만 CPU를 차지할 수 있었기 때문에, 프로그램들이 차례로 CPU를 쓰도록 스케줄링하는 것이 주된 고민이었다. 하지만 요즘은 컴퓨터 안에 여러 개의 CPU 코어가 들어 있기 때문에, 동시에 여러 프로그램을 처리할 수 있다. 이때, 어떤 CPU에 어떤 프로그램을 할당할지 결정하는 것이 바로 “멀티프로세서 스케줄링” 문제다.


과거에는 데스크톱이나 노트북 컴퓨터에 CPU가 1개뿐이었고, 멀티프로세서 시스템은 서버나 슈퍼컴퓨터처럼 특수한 곳에나 있었다. 하지만 이제는 우리가 쓰는 대부분의 PC에도 여러 코어가 들어 있다. “듀얼코어”, “쿼드코어”라는 단어를 들어본 적 있을 것이다. 이는 한 CPU 칩 안에 여러 개의 처리 장치가 있어서 동시에 여러 작업을 처리할 수 있다는 뜻이다.

 

싱글코어 CPU 속도를 높이는 데 한계가 오면서, CPU 제조사들은 한 칩에 코어를 여러 개 넣는 전략을 택했다. 그 결과, 프로그램을 더 빨리 실행하고 싶다면 이제는 단순히 CPU 클록 속도를 높이기보다, 여러 코어를 활용하도록 프로그램을 만들어야 한다. 즉, “동시에 여러 일을 할 수 있는 프로그램”을 작성하는 일이 중요해졌다.


멀티프로세서 시스템에서 발생하는 문제들

    1. 캐시 일관성 문제(Cache Coherence)
      각 CPU는 빠른 처리를 위해 “캐시”라는 작은 저장소를 갖고 있다. 프로그램이 메모리 데이터를 자주 읽거나 쓸 때, 매번 느린 메모리에서 가져오지 않고 이 캐시에 올려두면 더 빠르게 처리할 수 있다.
      하지만 CPU가 여러 개일 때 문제가 생긴다. 예를 들어, CPU1이 어떤 데이터(A)를 캐시에 올린 뒤 그 값을 변경했다고 해보자. 아직 메인 메모리는 업데이트되지 않았을 수 있다. 그 시점에서 CPU2가 같은 데이터(A)를 필요로 하여 메인 메모리에서 읽어오면, CPU2는 업데이트 전 옛날 값을 가져올 수 있다. 이 문제를 해결하기 위해 하드웨어 수준의 “캐시 일관성 프로토콜”이 동작한다. 모든 CPU의 캐시가 서로 모니터링해서, 한 CPU에서 데이터가 바뀌면 다른 CPU들도 그 사실을 알도록 하고, 낡은 데이터를 쓰지 않도록 한다.

    2. 동기화(Synchronization)의 필요성
      여러 CPU가 동시에 같은 자료구조에 접근한다면, 서로 충돌해서 데이터가 꼬일 수 있다. 이를 막기 위해 락(lock)이라는 장치를 사용한다. 한 CPU가 리스트를 수정할 때는 “잠금”을 걸어두고, 다른 CPU는 그 작업이 끝날 때까지 기다린다. 그래야 리스트가 꼬이거나 같은 데이터를 두 번 삭제하는 일이 없다.

    3. 캐시 친화성(Cache Affinity)
      각 프로그램(또는 스레드)이 실행되는 동안, 해당 CPU의 캐시에 데이터가 쌓인다. 프로그램이 매번 다른 CPU에서 실행되면 캐시를 매번 새로 채워야 하는데, 이것은 시간 낭비다. 하지만 다음에 이 프로그램을 다시 실행할 때 같은 CPU에서 실행하면 캐시를 재사용할 수 있어 속도가 빨라진다. 이를 “캐시 친화성”이라고 한다. 즉, 프로그램을 가능한 한 이전에 실행되던 CPU에 다시 올리는 것이 유리하다.

멀티프로세서 스케줄링 기법

  1. 단일 큐 멀티프로세서 스케줄링(SQMS)
    : 모든 작업을 하나의 대기열(queue)에 넣어두고, 여러 CPU가 그 큐에서 작업을 하나씩 꺼내서 실행하는 방법
    • 장점: 구현이 비교적 쉽다. 원래 단일 CPU용 스케줄러에서 거의 바꾸지 않고도 여러 CPU에 적용할 수 있다.
    • 단점: 한 큐를 모든 CPU가 공유하기 때문에 “락” 경쟁이 심해진다. 프로그램이 어느 CPU에서 실행될지 매번 바뀌어 캐시 친화성도 떨어진다.
  2. 멀티 큐 멀티프로세서 스케줄링(MQMS)
    : CPU마다 자신의 큐를 하나씩 두는 방법.(CPU0는 큐0, CPU1은 큐1) 작업이 들어오면 어떤 큐에 넣을지 정하고, 그 큐의 CPU에서만 작업을 처리한다.
    • 장점: 각 CPU가 자신의 큐를 독립적으로 관리하므로 락 경쟁이 적어지고, 캐시 친화성도 좋아진다(같은 CPU에서 계속 실행)
    • 단점: 워크로드(작업량) 균형 문제
      → 작업 이주(migration): 바쁜 CPU의 큐에서 일이 많은 작업을 하나 덜어내어 한가한 CPU 큐로 옮김.(작업 훔치기(work stealing))

 

자바는 한 번 작성하면 어디서든 실행할 수 있는 "Write Once, Run Anywhere"라는 철학을 바탕으로, 플랫폼 독립성을 가진 대표적인 프로그래밍 언어다. 이러한 플랫폼 독립성은 JVM(Java Virtual Machine)이라는 가상 머신 덕분에 가능한데, 관련해서 무엇인지 작성해보겠다.


1. 자바 실행 과정의 특징

자바 프로그램의 실행 과정은 크게 다음과 같다.

 

소스 코드 작성 및 컴파일

개발자는 .java 확장자를 갖는 자바 소스 코드를 작성하면, javac 컴파일러가 이 소스 코드를 바이트코드(.class)로 컴파일한다.

  • 바이트코드: 특정 운영체제에 종속되지 않는 중간 형태의 코드

클래스 로딩 및 링크(Linking)

실행 시점에 JVM은 클래스 로더(Class Loader)를 통해 필요한 .class 파일(바이트코드 파일)을 메모리에 로드하고, 심볼릭 참조를 직접 참조로 변경하는 링크 과정을 수행한다.

바이트코드 실행

로드된 바이트코드는 실행 엔진에 의해 해석되거나 JIT(Just-In-Time) 컴파일러를 통해 기계어로 변환되어 실제 OS 환경에서 실행된다.

이 과정을 통해 자바는 특정 운영체제나 하드웨어 아키텍처에 종속되지 않고 동일한 바이트코드를 기반으로 다양한 환경에서 실행될 수 있는 것이다.


2. JVM의 구조 및 역할

JVM은 자바 프로그램이 실질적으로 동작하는 "가상 머신" 환경으로, 운영체제 위에서 자바 바이트코드를 해석하고 실행하는 역할을 한다.

클래스 로더(Class Loader)

클래스 로더는 자바 클래스(.class 파일)를 동적으로 메모리에 로드하고, 필요한 경우 클래스를 초기화하는 역할을 담당한다. 자바 프로그램 실행 중 필요한 클래스를 언제든 불러올 수 있으며, 이를 통해 유연한 모듈성 및 동적 로딩이 가능하다.

  • 로딩: .class 파일을 찾아서 JVM 메모리에 로드
  • 링크: 심볼릭 레퍼런스(클래스, 메소드, 필드 정보 등)를 다이렉트 레퍼런스로 변경하고, 정적 변수를 초기화
  • 초기화: 정적 블록 실행 및 정적 변수의 초기값 할당 등 클래스 초기화 과정 수행

실행 엔진(Execution Engine)

실행 엔진은 로딩된 바이트코드를 실제 CPU가 이해할 수 있는 기계어 수준으로 해석하고 실행한다.

  • 인터프리터: 바이트코드를 명령어 단위로 해석하며 순차적으로 실행. 초기 실행 속도가 빠르지만, 반복 실행되는 코드에서는 비효율적.
  • JIT 컴파일러: 자주 실행되는 바이트코드 영역을 선별하여 기계어로 컴파일함으로써, 반복되는 코드 실행 시 성능을 향상시킴. 실행 시간이 지날수록 최적화가 이루어져 퍼포먼스가 향상.

메모리 영역(Runtime Data Area)

JVM은 자바 프로그램 실행 중 다양한 데이터를 저장한다. 메모리 영역은 다음과 같다.

  • 메소드 영역: 클래스에 대한 메타데이터(클래스 정보, 메소드 정보, 상수 풀(Constant Pool), 정적 변수) 등을 저장.
  • : new 키워드를 통해 생성되는 객체와 배열이 저장. 가비지 컬렉터에 의해 사용되지 않는 객체 정리.
  • JVM 스택: 메소드 호출 시 스택 프레임을 생성하여 지역 변수, 파라미터, 연산 중간 결과 저장. 메소드가 종료되면 해당 스택 프레임은 제거.
  • PC(Program Counter) 레지스터: 현재 실행 중인 JVM 명령어의 주소를 저장.
  • 네이티브 메소드 스택: 자바 외부의 네이티브 코드(C, C++) 호출 시 사용되는 스택 영역.

자바의 플랫폼 독립성

JVM은 자바 바이트코드를 어떤 플랫폼(Windows, Mac OS, Linux) 위에서도 동일하게 해석하고 실행한다(플랫폼 독립성). 즉, 자바 개발자는 OS나 하드웨어 환경에 구애받지 않고 하나의 바이트코드만으로 다양한 환경에서 애플리케이션을 동작할 수 있는 것이다(이식성).

1. 프로세스(Process)와 프로그램(Program)

프로그램은 하드디스크 위에 고정되어 있는 코드와 데이터의 집합→ 죽어있는 상태

프로세스는 이 프로그램을 실제로 메모리에 올려 CPU가 명령어를 하나씩 실행하는 것 → 살아있는 존재

  • 프로그램: 하드디스크 상의 정적인 명령어와 데이터 묶음
  • 프로세스: 메모리에 로드되어 CPU가 실행 중인 “활동하는” 프로그램

쉽게 말해, 게임 실행 파일 아이콘은 그냥 프로그램이고, 그것을 더블 클릭해서 게임이 실제로 화면에 움직이는 상태가 되면 그게 프로세스입니다.

 


 

2. 왜 프로세스가 중요할까?
우리는 평소 컴퓨터로 작업할 때 여러 프로그램을 동시에 켜놓음.

여러 작업이 동시에 실행 중인 것처럼 보이는데, 사실 CPU는 한 번에 하나의 명령만 처리할 수 있다.


그렇다면 어떻게 여러 프로세스가 동시에 동작하는 것처럼 보일까요?


운영체제는 CPU를 빠르게 돌아가며 여러 프로세스에 할당하는 시분할(time sharing) 기법을 사용.

ex) 한 프로세스를 실행 가능한 시점까지 실행하고, I/O 등 CPU를 사용하지 않는 작업을 할 때는 다른 프로세스를 실행.

 


 

3. 메커니즘(mechanism)과 정책(policy)
CPU 가상화를 잘 구현하기 위해서는 두 가지가 필요함.

  • 메커니즘: 어떻게 프로세스를 멈추고 다른 프로세스로 넘어가는지, 프로세스를 만들고 종료시키는 방법은 무엇인지 등의 구체적인 수단과 방법
  • 정책: 여러 개의 프로세스가 실행 대기 중일 때 어느 프로세스를 먼저 실행할지, 어떤 기준으로 CPU 사용 순서를 정할지 등의 결정 방법

메커니즘과 정책을 독립적으로 분리해두면, 나중에 정책을 바꾸더라도 메커니즘 부분을 변경하지 않아도 되고, 반대로 메커니즘을 바꿔도 정책은 그대로 둘 수 있어 편리하다.

 


 

4. 프로세스의 구성 요소

  • 메모리(주소 공간): 코드(실행 명령)데이터(변수), 힙(동적으로 할당되는 메모리), 스택(함수 호출 정보와 지역 변수)
  • CPU 레지스터 상태: 현재 어느 명령어를 실행 중인지 알려주는 프로그램 카운터(PC), 스택 포인터(SP), 각종 일반 레지스터 값 등
  • 입출력 상태: 현재 열려있는 파일들, 표준 입력/출력(키보드, 모니터), 네트워크 소켓 등
  •  

 

5. 프로세스의 생애주기(상태 전이)

  • 생성(Create) : 프로세스가 생성되는 중
  • 실행(Running) : 프로세스가 프로세서를 차지하여 명령어들이 실행 중
  • 준비(Ready) : 프로세스가 프로세서를 언제든지 사용할 수 있는 상태로, 프로세서 할당 대기 중.
  • 대기(Waiting) : 프로세스가 입출력 완료, 시그널 수신 등 디스크나 네트워크 입출력을 기다리고 있는 상태.
  • 종료(Terminated) : 프로세스의 실행 종료.

프로세스는 필요에 따라 이 상태들을 오가게 됨.
ex) A프로세스가 실행 중(Running)이었는데, 갑자기 파일을 읽어야 한다면 디스크 입출력이 끝날 때까지 대기(Waiting) 상태로 전환되고, 이때 다른 프로세스가 CPU를 차지(Ready→Running)하게 되는 식입니다.


7. 프로세스의 생성과 종료

  • 프로세스 생성(Create): 디스크에 있는 프로그램 코드를 메모리에 올리고, 스택과 힙을 초기화한 뒤, 표준 입력/출력 설정한 후 main 함수 실행.
  • 프로세스 종료(Destroy): 프로세스가 할 일을 모두 마치면 종료. 프로그램 내부에서 종료 요청을 하거나(예: return 0;), 사용자가 강제로 종료. 프로세스가 종료되면 운영체제가 점유 중이던 메모리나 파일 등의 자원을 다시 회수.

 

 

 

+ Recent posts