728x90

반복문(Iteration)은 프로그램 내에서 특정 코드 블록을 조건이 만족하는 동안 반복적으로 실행하기 위한 프로그래밍 구조다. 반복문은 코드의 재사용을 통해 작업을 간소화하고, 프로그램의 효율성을 높이는 데 중요한 역할을 한다. 주로 데이터의 집합을 처리하거나, 특정 조건을 만족할 때까지 반복 작업을 수행할 때 사용된다.

 

반복문은 조건이 항상 참이 되어 반복문이 종료되지 않는 상황. 프로그램의 의도하지 않은 동작이나 성능 문제를 일으킬 수 있다. 그리고 break를 사용하여 반복을 즉시 종료하거나, continue를 사용하여 현재 반복을 건너뛰고 다음 반복으로 넘어갈 수 있다.

 

for 반복문 정해진 범위나 순서가 있는 데이터 집합을 순회할 때 사용된다. 반복의 시작과 끝이 명확할 때 주로 사용한다. 리스트의 각 요소에 대해 반복하거나, 정해진 횟수만큼 반복 작업을 수행한다.

 

while 반복문 조건이 참(True)인 동안 코드 블록을 반복 실행한다. 반복 횟수가 불명확하거나, 특정 조건에 따라 반복을 종료해야 할 때 유용하다. 사용자로부터 입력을 받아 처리하는 동안 계속 실행하거나, 특정 상태가 변경될 때까지 반복한다. while 반복문 내에서는 조건이 결국 거짓이 될 수 있도록 조건에 영향을 미치는 변수를 적절히 변경해야 한다.

 

재귀(Recursion)는 함수가 자기 자신을 호출하는 방식을 말한다. 재귀를 사용하면 복잡한 문제를 간단한 부분 문제로 나누어 해결할 수 있으며, 특히 반복적인 계산을 간결하게 표현할 수 있다. 재귀는 컴퓨터 과학에서 매우 중요한 개념으로, 다양한 알고리즘과 데이터 구조(예: 정렬 알고리즘, 트리와 그래프의 탐색)에서 널리 사용된다.

 

기저 조건(Base Case)은 재귀 호출을 중단하고, 함수가 자기 자신을 더 이상 호출하지 않도록 하는 조건. 이는 재귀의 종료 조건이다. 기저 조건을 제대로 설정하지 않으면 무한 재귀 호출로 이어져 스택 오버플로우(Stack Overflow)가 발생할 수 있다.

 

재귀는 문제를 더 작은 단위로 나누어 접근하기 때문에, 분할 정복(Divide and Conquer) 알고리즘을 구현할 때 자연스럽게 적용된다. 꼬리 재귀 최적화(Tail Recursion Optimization)와 같은 기법을 사용하여 재귀 함수의 성능을 개선할 수 있다.

 

꼬리 재귀 최적화(Tail Recursion Optimization, TRO)는 재귀 호출을 효율적으로 처리하는 컴파일러 또는 인터프리터의 기능이다. 이 최적화는 재귀 함수의 마지막 동작이 함수 자신의 호출일 때, 즉 재귀 호출이 꼬리 위치(tail position)에 있을 때 적용할 수 있다. 꼬리 재귀 함수는 현재의 함수 프레임을 재사용하여 새로운 함수 호출을 수행함으로써, 호출 스택의 크기를 증가시키지 않고 재귀 호출을 실행한다. 이로 인해 메모리 사용량이 감소하고, 성능이 향상될 수 있다.

 

# 꼬리 재귀를 사용하지 않는 팩토리얼 함수
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n - 1)

# 꼬리 재귀를 사용하는 팩토리얼 함수
def factorial_tail_recursive(n, accumulator=1):
    if n == 1:
        return accumulator
    else:
        return factorial_tail_recursive(n - 1, n * accumulator)

 

위 예시에서 factorial_tail_recursive 함수는 꼬리 재귀를 사용한다. 계산 결과를 누적하는 accumulator 인자를 추가하여, 마지막 호출에서 바로 결과를 반환하도록 설계되었다. 이러한 방식으로 함수가 자기 자신을 호출할 때마다 새로운 스택 프레임을 생성하지 않고 기존의 스택 프레임을 재사용할 수 있어, 메모리 사용량이 줄어든다.

 

깊은 복사(deep copy)는 객체와 그 객체가 포함하는 모든 자식 객체들까지 완전히 새로운 복사본을 생성하는 방식이다. 이는 원본 객체와 복사본 객체 사이에 서로 영향을 주지 않으려 할 때 사용한다. 반면, 얕은 복사(shallow copy)는 객체의 최상위 레벨만 복사하여 새 객체를 생성하고, 내부의 객체들은 원본 객체와 동일한 참조를 공유한다. 따라서 내부 객체가 변경되면 얕은 복사본도 영향을 받는다.

 

깊은 복사는 복잡한 객체 구조를 완전히 독립적으로 복제할 때 사용한다. 예를 들어, 객체 내의 리스트나 딕셔너리 같은 다른 객체를 포함하는 경우가 해당한다.

 

얕은 복사는 객체 구조가 간단하거나, 내부 객체들을 공유해도 문제가 없을 때 유용하다.

 

import copy

# 깊은 복사 예시
obj = [1, [2, 3], 4]
deep_copied_obj = copy.deepcopy(obj)
deep_copied_obj[1][0] = '변경'
# 원본 obj는 변경되지 않음
print(obj)

# 얕은 복사 예시
shallow_copied_obj = copy.copy(obj)
shallow_copied_obj[1][0] = '변경'
# 원본 obj의 내부 리스트도 변경됨
print(obj)

#출력 :
#[1, [2, 3], 4]
#[1, ['변경', 3], 4]

 

위 코드에서 deepcopy() 함수는 obj 객체의 깊은 복사본을 생성하여 deep_copied_obj에 할당한다. 내부 리스트를 변경해도 원본 obj는 영향을 받지 않는다. 반면, copy() 함수로 생성한 얕은 복사본 shallow_copied_obj에서 내부 리스트를 변경하면, 원본 obj의 해당 리스트도 함께 변경된다.

728x90
728x90

페이지 테이블(Page table)은 가상 메모리 시스템에서 중요한 구성 요소로, 가상 주소를 실제 물리 메모리 주소로 매핑하는 정보를 담고 있다. 컴퓨터 시스템에서 프로그램은 가상 주소 공간에서 실행되며, 이 가상 주소 공간은 실제 물리 메모리에 매핑된다. 페이지 테이블은 이러한 매핑 정보를 제공하며, 운영 체제의 메모리 관리 부분에 의해 관리된다.

  • 가상 페이지 번호(Virtual Page Number, VPN): 가상 주소를 페이지로 나누었을 때, 해당 페이지의 번호.
  • 물리 프레임 번호(Physical Frame Number, PFN): 실제 물리 메모리에서 해당 페이지가 위치하는 프레임의 번호.
  • 접근 권한: 페이지에 대한 읽기, 쓰기, 실행 권한을 정의한다.
  • 유효 비트(Valid Bit): 해당 페이지가 물리 메모리에 존재하는지 여부를 나타낸다. 유효하지 않은 경우 페이지 폴트가 발생한다.
  • 수정 비트(Modified Bit or Dirty Bit): 해당 페이지가 수정되었는지 여부를 나타내며, 페이지 교체 정책에서 중요하게 사용된다.

CPU가 발생시킨 가상 주소는 페이지 번호와 오프셋으로 구분된다. 페이지 테이블은 이 페이지 번호를 사용하여 해당 페이지가 물리 메모리의 어느 프레임에 매핑되어 있는지 확인한다. 변환된 물리 주소(물리 프레임 번호 + 오프셋)를 사용하여 실제 메모리에 접근한다. 요청된 페이지가 메모리에 없을 경우(유효 비트가 설정되지 않음), 페이지 폴트가 발생하고, 운영 체제는 해당 페이지를 디스크에서 물리 메모리로 로드한다.

 

대규모 가상 메모리를 관리하기 위해, 페이지 테이블은 종종 계층적 구조로 구성되거나, TLB(Translation Lookaside Buffer)와 같은 캐시 메커니즘을 사용하여 주소 변환 과정을 최적화한다. 이러한 최적화는 메모리 접근 시간을 줄이고, 시스템의 전반적인 성능을 향상시킨다.

 

매핑(Mapping)은 소프트웨어나 하드웨어에서 사용하는 가상 주소와 물리적 주소 사이의 관계를 설정하는 과정입니다. 가상 메모리 시스템에서, 프로그램이 사용하는 가상 주소를 실제 메모리(RAM)의 물리적 주소로 변환하는 것이 대표적인 예이다.

 

페이징(Paging)가장 흔히 메모리 관리에서 사용되는 페이징은 가상 메모리 시스템에서 실제 메모리(RAM)를 더 효율적으로 사용하기 위해 고안된 기법이다. 이는 메모리를 고정된 크기의 블록(페이지)으로 나누고, 프로그램이 사용하는 가상 주소 공간을 이 페이지에 매핑하여 관리한다. 페이징을 통해, 시스템은 필요한 페이지만 메모리에 적재하고, 나머지는 디스크에 저장함으로써 메모리 사용을 최적화할 수 있다.

 

페이지 폴트(page fault)는 가상 메모리 시스템에서 발생하는 현상이다. 프로세스가 접근하려는 페이지가 물리 메모리(RAM)에 없을 때 발생한다. 이는 해당 페이지가 디스크에 위치하거나, 아직 메모리에 할당되지 않은 상태일 수 있다. 페이지 폴트가 발생하면, 운영 체제는 필요한 페이지를 디스크에서 찾아 물리 메모리로 로드한다. 이 과정을 통해 프로그램은 계속 실행될 수 있다.

 

페이지 폴트는 모든 가상 메모리 시스템에서 자연스러운 현상이지만, 빈번하게 발생하면 시스템의 성능에 부정적인 영향을 미칠 수 있다. 페이지 폴트가 많이 발생한다는 것은 프로세스가 필요로 하는 데이터나 코드가 물리 메모리에 적절히 위치하지 않고, 디스크 접근이 잦다는 것을 의미한다. 이러한 상황을 최소화하기 위해, 운영 체제는 페이지 교체 알고리즘을 사용하여 어떤 페이지를 메모리에 유지하고 어떤 페이지를 디스크로 내보낼지 결정한다.

 

세그멘테이션 폴트(segmentation fault)는 프로그램이 자신에게 허용되지 않은 메모리 영역에 접근하려고 할 때 발생하는 오류다. 이는 프로그램의 안정성과 보안을 위협할 수 있으며, 일반적으로 운영 체제에 의해 프로그램이 비정상적으로 종료된다.

 

세그멘테이션 폴트는 기화되지 않은 포인터나 이미 해제된 메모리를 참조할 때, 배열의 범위를 넘어서는 위치에 접근하려고 할 때, 혹은 읽기 전용 메모리에 쓰기 시도나 실행할 수 없는 메모리 영역에서 코드를 실행하려고 할 때 발생한다.

 

스와핑(swapping)은 메모리 관리 기법 중 하나이다. 이 기법은 시스템의 가용 메모리가 부족할 때, 메모리에 있는 데이터나 프로세스를 하드 디스크와 같은 보조 저장 장치로 옮기는 과정을 말한다. 스와핑을 통해, 시스템은 메모리를 더 효율적으로 사용할 수 있게 된다.

 

운영 체제는 메모리가 부족하다고 판단할 때, 현재 메모리에서 사용 빈도가 낮은 데이터나 프로세스를 선정한다. 선정된 데이터나 프로세스는 보조 저장 장치로 이동된다. 이 과정을 스와핑 아웃(swap out)이라 한다. 필요할 때, 보조 저장 장치에 저장된 데이터나 프로세스를 다시 메모리로 가져온다. 이 과정을 스와핑 인(swap in)이라 한다.

 

스와핑은 시스템의 전반적인 성능에 영향을 줄 수 있다. 보조 저장 장치는 메모리에 비해 접근 속도가 느리기 때문에, 스와핑 작업이 빈번하게 발생하면 시스템의 반응 시간이 길어질 수 있다. 이를 스와핑 스트레스라고 하며, 시스템의 성능 저하의 원인이 될 수 있다.

 

스레싱(thrashing)은 운영체제에서 멀티태스킹 환경에서 발생할 수 있는 문제로, 시스템의 자원을 대부분 페이지 교체나 스왑 아웃, 스왑 인 같은 메모리 관리 작업에 사용하게 되어 실제 유용한 작업 처리량이 급격히 감소하는 현상을 말한다. 이는 시스템에 너무 많은 프로그램이 동시에 실행되어 메모리 요청이 메모리 용량을 초과할 때 주로 발생한다. 결과적으로 CPU는 대부분의 시간을 유효한 데이터를 찾아 메모리와 스왑 공간(swap space) 사이를 이동하는 데 소비하게 되며, 이로 인해 시스템의 전반적인 성능이 크게 저하되는 것을 말한다.

728x90
728x90

캐시(Cache)는 컴퓨터 아키텍처에서 데이터나 명령어를 미리 복사해 놓는 임시 저장 공간을 말한다. 이 저장 공간은 CPU 내부나 그와 매우 가까운 곳에 위치하여, CPU가 주기억장치(RAM)이나 다른 보다 느린 저장 장치보다 빠르게 데이터에 접근할 수 있도록 돕는다. 캐시의 주된 목적은 시스템의 성능을 향상시키는 것이며, 이는 데이터 접근 시간을 줄임으로써 가능해진다.

 

캐시는 매우 빠른 속도로 데이터를 제공할 수 있는데, 이는 CPU가 필요로 하는 데이터를 캐시에서 직접 가져올 수 있기 때문이다. 캐시는 주기억장치에 비해 상대적으로 작은 용량을 가지고 있다. 이는 높은 속도의 메모리가 비싸기 때문이다. 캐시는 CPU에 매우 가까이 위치하여 빠른 데이터 전송을 가능하게 한다.

 

현대의 컴퓨터 시스템에서는 보통 여러 레벨의 캐시(L1, L2, L3)를 사용한다. 이 레벨은 캐시의 속도와 크기, CPU로부터의 거리에 따라 다르다. 캐시는 자주 사용되는 데이터나 명령어를 저장함으로써, CPU가 같은 데이터나 명령어에 대해 반복적으로 느린 주기억장치에 접근하는 것을 방지한다. 이러한 방식으로 시스템의 전반적인 성능을 향상시킨다.

 

MMU(Memory Management Unit)CPU와 메모리 사이에 위치하여 물리적 및 가상 메모리 주소 간의 변환을 담당한다. 가상 메모리 주소를 물리적 메모리 주소로 변환한다. 이는 프로그램이 물리적 메모리 위치를 직접 알 필요 없이 실행될 수 있게 한다.

 

다른 프로그램의 메모리 영역에 무단으로 접근하는 것을 방지하여 시스템의 안정성과 보안을 유지한다. 가장 효율적인 메모리 접근을 위해 캐시의 사용을 관리한다.

 

TLB (Translation Lookaside Buffer)MMU의 일부로, 가장 최근에 사용된 주소 변환의 결과를 저장하는 작은 캐시다. 가상 주소를 물리적 주소로 변환하는 데 필요한 시간을 줄이기 위해, 자주 사용되는 변환 정보를 저장한다. TLB를 통해 메모리 접근 시간을 줄이고, 시스템의 전반적인 성능을 향상시킨다. TLB가 변환 정보를 가지고 있지 않은 경우에만 MMU가 전체 메모리 변환 과정을 수행한다.

 

 

TLB의 효율성은 'TLB hit rate'라는 메트릭으로 측정되는데, 이는 전체 메모리 접근 중 TLB가 변환 정보를 이미 가지고 있어 바로 제공할 수 있는 비율을 의미한다. TLB hit rate가 높을수록 시스템 성능이 더 좋다. MMU와 TLB는 함께 작동하여 메모리 접근 시간을 최적화하고, 프로그램이 효율적으로 실행되도록 한다.

 

가상 메모리는 컴퓨터 시스템에서 물리적 메모리(주기억장치) 크기를 초과하는 프로그램을 실행할 수 있게 해주는 메모리 관리 기법이다. 이 기술은 물리적 메모리와 보조기억장치(예: 하드 드라이브, SSD)의 일부를 결합해, 마치 더 큰 단일 메모리 공간이 있는 것처럼 운영 체제와 응용 프로그램에게 제공한다.

 

가상 메모리는 페이지(데이터의 작은 블록)를 기반으로 작동한다. 필요한 페이지는 물리적 메모리로 로드되고, 필요하지 않은 페이지는 보조기억장치로 이동된다. 프로그램은 가상 주소를 사용해 메모리에 접근한다. CPU 내부의 메모리 관리 장치(MMU)는 이 가상 주소를 물리적 주소로 변환한다.

 

프로그램은 실제 물리적 메모리보다 더 큰 메모리 공간을 사용할 수 있어, 복잡한 작업과 큰 데이터 집합을 처리할 수 있다. 각 프로그램은 독립적인 가상 메모리 공간을 가지므로, 한 프로그램의 오류가 다른 프로그램이나 운영 체제에 영향을 미치지 않는다.

 

가상 주소를 물리적 주소로 변환하는 과정과 페이지 교체 과정은 시간이 소요되며, 이는 시스템의 전반적인 성능 저하로 이어질 수 있다.

728x90
728x90

레지스터와 메모리는 컴퓨터 시스템에서 데이터를 저장하고 처리하는 데 필수적인 두 가지 리소스이며 휘발성이라는 공통점을 가진다. 이 두 리소스들은 용량, 용도, 속도 등 여러 면에서 차이점을 가진다.

메모리(RAM)는 레지스터와 달리 CPU의 외부에 위치하며, 레지스터에 비해 접근속도가 상대적으로 느리다. 메모리는 데이터와 프로그램 코드를 저장하는 데 사용되며 CPU와 시스템 버스를 통해 이루어진다. 그리고 물리 메모리 초과를 방지하기 위해 가상 메모리를 사용한다.

레지스터는 cache보다 CPU 코어에 근접해 있다. CPU코어 - 레지스터 - 캐시 - 메모리 순으로 위치 하여 있으며 가까울 수록 임시 저장 및 처리를 더 빠르게 하고 있는 것이다.

레지스터는 계산 중간 결과나 계산에 자주 사용되는 임시 값들을 저장한다. 운영체제는 context switching시 레지스터의 상태를 저장하고 복원하는 작업을 한다.

레지스터는 PC와 스택 포인터 등 특수목적 레지스터를 포함하여 상태 레지스터,작업 레지스터, 범용 레지스터, 명령어 레지스터를 일괄하여 말한다. 이 중 범용 레지스터는 프로그램의 실행 동안 가장 일반적이고 자주 사용되는 레지스터이다.

PC(Program Counter)는 CPU가 현재 실행하고 있는 명령어의 메모리 주소를 저장하는 레지스터이다. CPU가 다음에 실행할 명령어의 위치를 알려주며, 일반적으로 명령어가 실행될 때 마다 자동으로 증가하여 다음 명령어의 주소를 가리킨다. 프로그램의 실행 흐름을 변경하는 분기나 점프 명령어가 실행될 때, 프로그램 카운터는 새로운 주소로 업데이트되어 CPU가 새로운 위치에서 명령어를 계속해서 실행할 수 있게 한다.

명령어 레지스터는 현재 CPU가 실행중인 명령어를 저장하여 CPU가 현재 어떤 작업을 하고 있는지 알고 싶을 때 사용된다.

상태 레지스터는 보통 몇 비트로 이루어져 있으며, 각 비트는 특정 상태나 조건 (예 : 캐리 아웃, 오버플로우, 언더플로우, 제로, 음수 등)을 나타낸다. 이런 단일 연산 결과에 대한 정보를 제공하므로 상태 레지스터의 크기는 상대적으로 작다.

작업 레지스터는 특정 아키텍처에선 범용 레지스터라고 불리며, 일시적으로 데이터를 처리하기 위한 임시 저장소이다.

범용 레지스터는 데이터 연산, 임시 데이터 저장, 메모리 주소의 저장 등 다양한 목적으로 사용된다. 이들은 프로그램의 데이터를 처리하고, 메모리와의 데이터 교환을 위한 주요 저장소 역할을 한다.

 

디스크는 컴퓨터에서 데이터를 저장하는 장치를 말한다. 크게 두 종류로 나뉜다

 

HDD(Hard Disk Drive) 자기 저장 방식을 사용해 데이터를 저장하는 장치로, 내부에는 회전하는 디스크(플래터)가 있으며, 이 위에 데이터를 읽고 쓰는 헤드가 움직이며 작동한다. 대용량 데이터를 저장할 수 있고, 가격이 상대적으로 저렴하지만, 움직이는 부품이 있어 충격에 약하고, SSD에 비해 속도가 느리다.

 

SSD(Solid State Drive)반도체 칩을 사용해 데이터를 저장하며, 움직이는 부품이 없어 충격에 강하고 빠른 데이터 읽기와 쓰기 속도를 제공한다. HDD에 비해 가격이 높지만, 성능과 내구성 면에서 우수하다.

 

M.2SSD의 한 형태로, 소형화된 카드 형태의 설계로 제공된다. PCIe 또는 SATA 인터페이스를 통해 데이터 전송을 지원하며, 특히 NVMe 프로토콜을 사용하는 PCIe M.2 SSD는 매우 높은 속도를 제공한다. 노트북이나 소형 PC에서 공간 절약과 고속 데이터 전송이 필요할 때 사용된다.

 

이들은 다 보조기억 장치로 분류된다. 보조기억장치는 컴퓨터에서 데이터를 영구적으로 저장하는 역할을 하며, 주기억장치(RAM)과 달리 전원이 꺼져도 정보를 유지한다.(비휘발성) 이러한 장치들은 대용량 데이터 저장, 시스템 부팅, 소스트웨어 실행 등 다양한 목적으로 사용된다.

 

ROM(Read-Only Memory)은 읽기 전용 메모리로, 주로 컴퓨터나 전자기기의 펌웨어나 시스템 부트로더 등이 저장되는 곳이다. 전원이 꺼져도 데이터를 유지한다는 점에서 보조기억장치의 특성을 가지고 있지만, 일반적으로 사용자가 데이터를 저장하고 변경할 수 있는 HDD, SSD와 같은 저장 장치와는 구분된다. ROM은 보통 한 번 프로그래밍되면 변경이 어렵거나 불가능한 데이터를 저장하는 데 사용되므로, 전통적인 의미에서의 보조기억장치와는 조금 다른 역할을 한다. 그럼에도 불구하고 넓은 의미에서는 데이터를 저장하는 장치로서 보조기억장치의 범주에 포함시킬 수 있다.

 

728x90
728x90

프로그래밍에서 함수나 메서드를 호출할 때 인자를 전달하는 방식에는 주로 Call by Value(값에 의한 호출)와 Call by Reference(참조에 의한 호출) 두 가지 방법이 있다. 이 두 전달 방식은 함수를 호출할 때 인자가 어떻게 함수에 전달되는지, 그리고 함수 내에서 인자의 값을 변경했을 때 그 변경이 호출자에게 반영되는지 여부에 대한 차이를 나타낸다.

 

 

call by value는 함수에 인자를 전달할 때, 인자의 실제 값을 복사하여 함수에 전달하는 방식이다. 함수 내에서 인자의 값을 변경해도, 원본 인자에는 영향을 미치지 않는다. 왜냐하면 함수에는 원본 인자의 복사본이 전달되기 때문이다. 주로 기본 자료형(정수, 실수, 문자 등)을 인자로 사용할 때 보이는 특성이다.

 

void addOne(int x) {
    x = x + 1;
}

int main() {
    int num = 10;
    addOne(num);
    // 여기서 num의 값은 여전히 10이다.
}

 

call by reference는 함수에 인자를 전달할 때, 인자의 메모리 주소를 전달하는 방식이다. 함수 내에서 인자의 값을 변경하면, 그 변경이 원본 인자에도 반영된다. 함수에는 원본 인자를 가리키는 주소가 전달되기 때문이다. 주로 복잡한 자료형(객체, 배열 등)을 인자로 사용할 때 보이는 특성이다.

void addOne(int& x) {
    x = x + 1;
}

int main() {
    int num = 10;
    addOne(num);
    // 여기서 num의 값은 11이 된다.
}
public void updateArray(int[] arr) {
    arr[0] = 100;
}

public static void main(String[] args) {
    int[] myArray = {1, 2, 3};
    new Test().updateArray(myArray);
    // 여기서 myArray[0]은 100이 된다.
}

 

Python에서 가비지 컬렉션은 메모리 관리의 한 형태로, 프로그램에서 더 이상 사용되지 않는 메모리를 자동으로 회수하는 프로세스를 말한다.

 

Python의 가비지 컬렉터는 주로 참조 카운팅(reference counting) 방식을 사용하여, 어떤 객체에 대한 참조가 더 이상 존재하지 않을 때 해당 객체를 메모리에서 해제한다. 추가적으로, 순환 참조(circular references)를 탐지하고 제거하기 위해 세대별(generational) 가비지 컬렉션을 사용한다.

 

Java에서 가비지 컬렉션은 JVM이 자동으로 메모리 관리를 수행하는 과정이다. 이 과정은 사용되지 않는 객체를 식별하고, 메모리에서 제거하여 애플리케이션의 효율성을 극대화한다. 가비지 컬렉션의 핵심 알고리즘에는 마킹 및 스위핑, 복사, 마킹-콤팩트, 그리고 세대별 수집이 포함된다.

 

메타 데이터는 데이터에 대한 데이터라는 의미를 가진다. Java에서는 클래스, 메서드, 변수 등과 같은 프로그램 요소에 대한 정보를 의미한다. 예를 들어, 클래스 파일(.class)에는 클래스 이름, 메서드 시그니처, 변수 타입 등 해당 클래스에 관한 정보가 메타 데이터로 포함된다.

 

메타 스페이스는  Java 8 이전에는 클래스 로더가 로드한 클래스 메타 데이터가 퍼머넌트 제너레이션(Permanent Generation, PermGen) 영역에 저장되었다. Java 8에서는 PermGen이 제거되고, 메타 스페이스라는 새로운 메모리 영역이 도입되었다. 메타 스페이스는 네이티브 메모리(non-heap memory)를 사용하여 클래스 메타 데이터를 저장한다.

 

PermGen 영역은 고정된 크기를 가지고 있었고, 이로 인해 많은 클래스를 로드하는 애플리케이션에서는 java.lang.OutOfMemoryError: PermGen space 오류가 발생할 수 있었다. 메타 스페이스는 필요에 따라 동적으로 확장될 수 있어 이러한 문제를 해결한다.

 

메타 스페이스는 JVM 외부의 네이티브 메모리를 사용하기 때문에, JVM의 힙 메모리 영역과 독립적으로 관리된다. 따라서 메타 데이터의 크기가 JVM의 최대 힙 크기에 영향을 미치지 않으며, 더 유연한 메모리 관리가 가능해진다.

 

메타 스페이스의 크기는 JVM 시작 시 -XX:MetaspaceSize-XX:MaxMetaspaceSize 옵션을 통해 초기 크기와 최대 크기를 설정할 수 있다. 이를 통해 애플리케이션의 요구 사항에 맞게 메타 스페이스의 메모리 사용을 조절할 수 있다.

728x90
728x90

메모리 할당 방식에는 여러 가지가 있으며, 그 중 implicit(암시적), explicit(명시적), segregated list(분리된 리스트)는 메모리 블록을 관리하는 데 사용되는 대표적인 방법들이다. 각 방식은 메모리를 할당하고 해제하는 방법, 그리고 가용 메모리 블록을 추적하는 방법에서 차이를 보인다.

 

Fist Fit사용 가능한 메모리 블록 리스트를 처음부터 탐색해 요청된 크기를 수용할 수 있는 첫 번째 블록을 할당한다. 탐색 과정이 단순하며 때로 빠르다. 하지만 메모리 앞부분에 작은 블록들이 많이 남게 되어 큰 메모리 요청을 처리할 때 효율이 떨어질 수 있다. 이는 외부 단편화로 이어진다.

 

Next Fit Last Fit과 유사하되, 마지막으로 메모리를 할당한 위치부터 탐색을 시작해 전체 리스트를 순환한다. 마지막 할당 위치에서 시작하기 때문에 전체 메모리 공간의 사용이 더 균일해질 수 있다. 하지만 탐색 효율성이 낮아질 수 있으며, 큰 블록 요청 시 외부 단편화 문제가 발생할 수 있다.

 

Best Fit요청된 크기를 수용할 수 있으면서 가장 작은 블록을 선택해 할당한다. 메모리 낭비를 최소화하여 외부 단편화의 가능성을 줄인다. 하지만 전체 리스트를 탐색해야 하므로 탐색 시간이 길어질 수 있으며, 빈번한 작은 블록 할당으로 인해 내부 단편화가 발생할 수 있다.

 

implicit(암시적) 메모리 할당 방식은 free list를 사용하여 가용 메모리 블록을 추적한다. 이 리스트는 메모리 내의 가용 블록들을 순차적으로 연결한다. 메모리 할당 요청이 들어오면, 할당자는 리스트를 처음부터 탐색하여, 요청된 크기를 수용할 수 있는 첫 번째 블록을 할당한다. 이 과정을 "first-fit" 탐색이라고도 한다.

 

이 포스팅에는 implicit을 더 효율적인 next-fit으로 변형한 코드만 다루고 있다.

 

mm_init 함수는 메모리 관리 시스템을 초기화하는 데 사용된다. 이 함수는 동적 메모리 할당자를 초기화하고, 필요한 초기 메모리 구조를 설정하여 사용자가 요청하는 메모리 할당 및 해제 요청을 처리할 준비를 한다.

int mm_init(void)
{
    if ((heap_listp = mem_sbrk(4 * WSIZE)) == (void *)-1) // 초기 힙 메모리를 할당
        return -1;

    PUT(heap_listp, 0);                            // 힙의 시작 부분에 0을 저장하여 패딩으로 사용
    PUT(heap_listp + (1 * WSIZE), PACK(DSIZE, 1)); // 프롤로그 블럭의 헤더에 할당된 상태로 표시하기 위해 사이즈와 할당 비트를 설정하여 값을 저장
    PUT(heap_listp + (2 * WSIZE), PACK(DSIZE, 1)); // 프롤로그 블록의 풋터에도 마찬가지로 사이즈와 할당 비트를 설정하여 값을 저장
    PUT(heap_listp + (3 * WSIZE), PACK(0, 1));     // 에필로그 블록의 헤더를 설정하여 힙의 끝을 나타내는 데 사용
    heap_listp += (2 * WSIZE);                     // 프롤로그 블록 다음의 첫 번째 바이트를 가리키도록 포인터 조정
    find_nextp = heap_listp;                       // nextfit을 위한 변수

    if (extend_heap(CHUNKSIZE / WSIZE) == NULL) // 초기 힙을 확장하여 충분한 양의 메모리가 사용 가능하도록 chunksize를 단어 단위로 변환하여 힙 확장
        return -1;
    if (extend_heap(4) == NULL)                  //자주 사용되는 작은 블럭이 잘 처리되어 점수가 오름
        return -1;
    return 0;
}

 

mm_malloc 함수의 구현은 메모리 관리 시스템의 일부로, 요청된 크기의 메모리 블록을 동적으로 할당하는 기능을 제공한다. 이 함수는 요청된 크기를 조정하여 오버헤드와 정렬 요구 사항을 충족시키고, 적절한 크기의 블록을 찾거나 힙을 확장하여 메모리를 할당한다.

 

void *mm_malloc(size_t size)
{
    size_t asize;      /* Adjusted block size */
    size_t extendsize; /* Amount to extend heap if no fit */
    char *bp;

    /* Ignore spurious requests */
    if (size == 0)
        return NULL;

    /* Adjust block size to include overhead and alignment reqs. */
    if (size <= DSIZE)
        asize = 2 * DSIZE;
    else
        asize = DSIZE * ((size + (DSIZE) + (DSIZE - 1)) / DSIZE);

    /* Search the free list for a fit */
    if ((bp = find_fit(asize)) != NULL)
    {
        place(bp, asize);
        return bp;
    }

    /* No fit found. Get more memory and place the block */
    
    extendsize = MAX(asize, CHUNKSIZE);
    if ((bp = extend_heap(extendsize / WSIZE)) == NULL)
        return NULL;
    place(bp, asize);
    return bp;
}

 

아래는 동적 메모리 할당기의 일부로 사용되는 find_fit 함수의 구현 예시이며, "Next Fit" 메모리 할당 전략을 사용한다. 이 함수의 목적은 요청된 크기(asize) 이상의 가용 메모리 블록을 찾는 것이다.

static void *find_fit(size_t asize)
{
    /* Next-fit search */
    void *bp;
    bp = find_nextp;
    // 현재 블록이 에필로그 블록이 아닌 동안 계속 순회, 블록의 헤더 크기가 0보다 크지 않으면 에필로그 블럭
    for (; GET_SIZE(HDRP(find_nextp)) > 0; find_nextp = NEXT_BLKP(find_nextp))
    {
        // 가용 블럭의 헤더가 할당되어 있지 않고 요청된 크기보다 크거나 같은 경우 해당 가용 블록을 반환
        if (!GET_ALLOC(HDRP(find_nextp)) && (asize <= GET_SIZE(HDRP(find_nextp))))
        {
            return find_nextp;
        }
    }
    // 위의 for루프에서 가용 블럭을 찾지 못한 경우, 다시 순회
    for (find_nextp = heap_listp; find_nextp != bp; find_nextp = NEXT_BLKP(find_nextp))
    { // 이전에 탐색했던 find_nextp 위치에서부터 다시 가용 블록을 찾아서 반환
        if (!GET_ALLOC(HDRP(find_nextp)) && (asize <= GET_SIZE(HDRP(find_nextp))))
        {
            return find_nextp;
        }
    }

    return NULL;
}

 

place 함수는 메모리 할당 과정에서 효율성을 높이기 위해 설계되었다. 할당 요청에 따라 메모리 블록을 최적으로 사용할 수 있도록 하며, 할당 후 남은 공간이 충분히 크면 이를 다시 가용 블록으로 반환하여 메모리 낭비를 줄인다. 이 과정은 메모리 단편화를 방지하고 메모리 사용 효율을 높이는 데 중요하다.

static void place(void *bp, size_t asize)
{
    size_t csize = GET_SIZE(HDRP(bp)); // 현재 블록의 크기를 알아냄

    // 남은 공간이 충분히 클 경우, 즉 요청한 크기(asize)와 현재 크기(csize)의 차이가
    // 두 배의 더블 사이즈(DSIZE)보다 크거나 같으면 블록을 나눔
    if ((csize - asize) >= (2 * DSIZE))
    {
        PUT(HDRP(bp), PACK(asize, 1));         // 사용할 블록의 헤더에 크기와 할당된 상태 저장
        PUT(FTRP(bp), PACK(asize, 1));         // 사용할 블록의 푸터에도 똑같이 저장
        bp = NEXT_BLKP(bp);                    // 나머지 블록으로 포인터 이동
        PUT(HDRP(bp), PACK(csize - asize, 0)); // 나머지 블록의 헤더에 크기와 빈 상태 저장
        PUT(FTRP(bp), PACK(csize - asize, 0)); // 나머지 블록의 푸터에도 똑같이 저장
    }
    else // 남은 공간이 충분히 크지 않으면 현재 블록 전체 사용
    {
        PUT(HDRP(bp), PACK(csize, 1)); // 현재 블록의 헤더에 크기와 할당된 상태 저장
        PUT(FTRP(bp), PACK(csize, 1)); // 현재 블록의 푸터에도 똑같이 저장
    }
}

 

coalesce 함수는 메모리 해제 또는 메모리 할당 과정 중에 인접한 가용 블록들을 하나의 큰 블록으로 합치는 역할을 한다. 이 과정은 메모리 단편화를 줄이고, 효율적인 메모리 사용을 가능하게 한다. 병합 방식은 주변 블록들의 할당 상태에 따라 달라진다.

static void *coalesce(void *bp)
{
    size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(bp)));
    size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp)));
    size_t size = GET_SIZE(HDRP(bp));

    if (prev_alloc && next_alloc) /* Case 1 */
    {
        return bp;
    }

    if (prev_alloc && !next_alloc) /* Case 2 */
    {
        size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
        PUT(HDRP(bp), PACK(size, 0));
        PUT(FTRP(bp), PACK(size, 0));
    }
    else if (!prev_alloc && next_alloc) /* Case 3 */
    {
        size += GET_SIZE(HDRP(PREV_BLKP(bp)));
        PUT(FTRP(bp), PACK(size, 0));
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
        bp = PREV_BLKP(bp);
    }
    else /* Case 4 */
    {
        size += GET_SIZE(HDRP(PREV_BLKP(bp))) +
                GET_SIZE(FTRP(NEXT_BLKP(bp)));
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
        PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));
        bp = PREV_BLKP(bp);
    }
    find_nextp = bp;
    return bp;
}

 

realloc 함수는 주어진 포인터 ptr에 할당된 메모리 블록의 크기를 변경하기 위해 사용된다. 요청된 새 크기(size)에 따라 메모리 블록을 확장하거나 축소한다. 이 과정에서 필요하다면, 새로운 메모리 위치로 데이터를 복사하고 원래 메모리를 해제한다.

void *mm_realloc(void *ptr, size_t size)
{
    void *oldptr = ptr;
    void *newptr;
    size_t copySize;

    // 새로운 메모리 블록 할당
    newptr = mm_malloc(size);

    if (newptr == NULL)
        return NULL;

    // 이전 블록의 데이터 크기를 가져옴
    copySize = GET_SIZE(HDRP(oldptr));

    // 실제 복사할 데이터 크기는 이전 블록 크기와 요청된 새 블록 크기 중 작은 값
    if (size < copySize)
        copySize = size;

    // 데이터를 새 블록으로 복사
    memcpy(newptr, oldptr, copySize);

    // 이전 블록 해제
    mm_free(oldptr);

    return newptr;
}

 

implicit 리스트를 사용한 메모리 할당에서는 가용 블록을 찾기 위해 리스트의 시작부터 순차적으로 탐색해야 한다. 큰 메모리 풀에서 적합한 블록을 찾는 데 시간이 오래 걸릴 수 있으며, 이는 메모리 할당과 해제 작업의 효율성을 떨어뜨린다. 메 모리 할당과 해제가 빈번하게 이루어질 경우, 암시적 방식은 빠르게 메모리 단편화를 야기하고, 이는 시간이 지남에 따라 전반적인 시스템 성능에 부정적인 영향을 미칠 수 있다.

 

위 구현된 implicit 외의 explicit(명시적) 메모리 할당 방식에서는 가용 메모리 블록을 관리하기 위해 별도의 자료 구조(예: 이중 연결 리스트)를 사용한다. 각 가용 블록은 다음과 이전의 가용 블록을 가리키는 포인터를 포함한다. 이를 통해 할당과 해제가 훨씬 유연하게 이루어질 수 있다.

 

segregated list(분리된 리스트) 메모리 할당 방식에서는 다양한 크기 범위에 따라 여러 개의 가용 리스트를 유지한다. 각 리스트는 특정 크기 범위의 블록들만을 포함한다. 메모리 할당 요청이 들어오면, 해당 크기 범위에 맞는 리스트를 선택하여 가용 블록을 탐색한다.

 

buddy system은 가변 크기의 메모리 할당을 지원하며, 특히 시스템 프로그래밍이나 운영 체제에서 자주 사용된다. Buddy System의 핵심 아이디어는 메모리를 고정된 크기의 블록으로 분할하고, 이 블록들을 필요에 따라 합치거나 분할하여 메모리를 할당하는 것이다. 이 과정에서 각 메모리 블록의 "buddy" 또는 짝을 이용해 효율적으로 메모리를 관리한다.

 

buddy system의  메모리는 2의 거듭제곱 크기의 블록으로 분할된다. 예를 들어, 메모리 크기가 2^N인 경우, 가능한 블록 크기는 2^0, 2^1, 2^2, ..., 2^N까지 다양하다. 그리고 할당을 할 때에는 요청된 메모리 크기에 가장 잘 맞는 블록 크기를 찾는다. 이는 요청 크기보다 크거나 같은 가장 작은 2의 거듭제곱 크기의 블록을 의미한다. 만약 해당 크기의 블록이 없다면, 더 큰 블록을 분할하여 요구를 충족시킨다.

 

각 블록은 고유한 buddy 또는 짝을 가지며, 이는 같은 크기의 인접한 블록을 의미한다. Buddy는 블록의 주소와 크기를 이용하여 계산할 수 있다. 메모리 블록이 해제될 때, 해당 블록의 buddy가 현재 사용 가능한지 확인한다. 만약 buddy도 사용 가능하다면, 두 블록을 합쳐서 더 큰 블록을 만든다. 이 과정을 반복하여 가능한 한 큰 블록을 유지한다.

 

https://github.com/yunsejin/Malloc-lab

 

GitHub - yunsejin/Malloc-lab: 4교육장 5주차 (2024.2.8(목) ~ 2024.2.21(수)) 2조 공부 기록

4교육장 5주차 (2024.2.8(목) ~ 2024.2.21(수)) 2조 공부 기록. Contribute to yunsejin/Malloc-lab development by creating an account on GitHub.

github.com

 

728x90
728x90

malloc, calloc, realloc은 C언어에서 동적 메모리 할당을 위해 사용되는 함수다.

  1. malloc (Memory Allocation)
    • void* malloc(size_t size) 형태다.
    • 지정된 크기의 메모리 블록을 할당하고, 할당된 메모리의 주소를 반환한다. 초기화하지 않으므로 메모리 내용은 예측할 수 없다.
    • 예: int* arr = (int*)malloc(10 * sizeof(int)); // int형 10개를 저장할 수 있는 공간 할당
  2. calloc (Contiguous Allocation)
    • void* calloc(size_t num, size_t size) 형태다.
    • 지정된 수(num)의 요소 각각이 size 크기인 메모리 블록을 할당하고, 모든 바이트를 0으로 초기화한다.
    • 예: int* arr = (int*)calloc(10, sizeof(int)); // int형 10개를 저장할 수 있는 공간을 할당하고 0으로 초기화
  3. realloc (Re-Allocation)
    • void* realloc(void* ptr, size_t newSize) 형태다.
    • 이미 할당된 메모리 블록의 크기를 newSize로 변경한다. 필요하면 새로운 위치로 메모리 블록을 이동시키고, 원래 위치의 메모리는 해제한다.
    • 예: arr = (int*)realloc(arr, 20 * sizeof(int)); // arr의 크기를 int형 20개를 저장할 수 있도록 조정

이 함수들은 stdlib.h 헤더 파일에 정의되어 있으멀로 사용하기 위해서는 이 헤더 파일을 포함시켜야 한다. 동적 메모리 할당 후에는 free 함수를 사용하여 할당된 메모리를 해제해야 메모리 누수를 방지할 수 있다.

 

malloc 지정된 크기의 메모리 블록을 할당. 초기화되지 않음. int *a = malloc(4*sizeof(int)); [ ?, ?, ?, ? ]
calloc 지정된 수의 요소에 대해 메모리를 할당하고 0으로 초기화. int *b = calloc(4, sizeof(int)); [ 0, 0, 0, 0 ]
realloc 기존에 할당된 메모리의 크기를 조정. 새 크기가 더 클 경우 초기화되지 않은 공간 추가됨. a = realloc(a, 8*sizeof(int)); [ ?, ?, ?, ?, ?, ?, ?, ? ]
  • malloc은 할당된 메모리의 초기 내용이 무엇인지 정의하지 않으므로, 메모리 내용은 예측할 수 없는 값(?)으로 표시됨.
  • calloc은 할당된 메모리를 모두 0으로 초기화하기 때문에, 모든 요소가 0으로 표시됨.
  • realloc을 사용하여 메모리 크기를 늘릴 경우, 추가된 메모리 영역은 초기화되지 않아 예측할 수 없는 값(?)으로 표시됨.

메모리 블록에는 다양한 데이터가 담길 수 있다. 어떤 데이터가 담기는지는 메모리 블록을 사용하는 프로그램의 목적과 구현에 따라 달라진다. 일반적으로 메모리 블록에는 다음과 같은 것들이 저장될 수 있다:

 

프로그램에서 사용하는 변수의 실제 데이터가 메모리에 저장된다. 이는 기본 자료형(int, float, char 등)의 값일 수도 있고, 사용자 정의 자료형의 값일 수도 있다.

 

배열의 각 요소는 메모리 블록의 연속적인 위치에 저장된다. 배열을 통해 여러 데이터를 순차적으로 저장할 수 있다.

 

구조체는 여러 다른 자료형을 한 묶음으로 관리할 수 있는 C언어의 자료형이다. 구조체 인스턴스의 각 멤버는 메모리 블록에 연속적으로 배치된다.

 

객체 지향 프로그래밍 언어에서 객체의 속성과 메서드에 대한 정보가 메모리에 저장된다. C언어에서는 객체 지향 프로그래밍을 직접적으로 지원하지 않지만, 구조체와 함수 포인터를 사용하여 비슷하게 구현할 수 있다.

 

함수의 주소를 저장하여, 나중에 해당 주소를 통해 함수를 호출할 수 있다. 이를 통해 동적으로 함수를 호출하거나 콜백 함수를 구현할 수 있다.

728x90
728x90

힙(Heap)과 스택(Stack)은 프로그램이 메모리를 사용하는 두 가지 주요 영역이다.

 

: 동적으로 할당되는 메모리 영역. 프로그래머가 직접 관리하며, 필요할 때 메모리를 할당하고 사용이 끝나면 해제한다. 힙은 메모리 할당과 해제가 자유롭기 때문에 스택보다 유연하지만, 메모리 누수나 단편화 같은 문제가 발생할 수 있다. 힙은 크기가 고정되어 있지 않고 프로그램 실행 동안 확장될 수 있다.

 

스택: 자동으로 할당 및 해제되는 메모리 영역. 함수 호출 시 생성되는 지역 변수와 함수 매개변수가 저장된다. 스택은 LIFO(Last In, First Out) 방식으로 작동한다. 함수가 호출되면 스택 프레임이 스택에 푸시되고, 함수가 종료되면 스택 프레임이 팝되어 메모리에서 제거된다. 스택은 크기가 고정되어 있으며, 스택 오버플로우가 발생할 수 있다.

 

주요 차이점:

  • 스택은 컴파일러에 의해 자동으로 관리되며, 힙은 개발자가 직접 관리해야 한다.
  • 스택은 정적/자동 할당이고, 힙은 동적 할당이다.
  • 스택에 할당된 메모리는 함수 호출이 끝나면 자동으로 해제되지만, 힙에 할당된 메모리는 명시적으로 해제하지 않으면 프로그램 종료 시까지 남아 있다.
  • 스택은 제한적인 반면, 힙은 더 크고 유연하다.
  • 스택은 힙에 비해 더 빠른 할당과 해제가 가능하다.

 

코드 영역: 프로그램의 실행 가능한 기계어 코드가 저장되는 곳이다. 이 영역은 읽기 전용으로 설정되어 있어, 프로그램 실행 중에는 변경되지 않는다. 함수, 루프, 조건문 등의 실제 프로그램 코드가 여기에 위치한다.

 

데이터 영역: 프로그램의 전역 변수와 정적 변수(static variables)가 저장되는 영역이다. 이 영역에 저장되는 변수들은 프로그램의 생명 주기 동안 초기화된 값으로 유지되며, 프로그램 실행 도중에도 값이 변경될 수 있다. 데이터 영역은 두 부분으로 나뉘는데, 초기화된 데이터를 위한 영역과 초기화되지 않은 데이터를 위한 BSS 영역이 그것이다.

 

BSS 영역: 초기화되지 않은 전역 변수와 정적 변수가 저장되는 영역이다. BSS 영역에 있는 변수들은 프로그램이 시작할 때 0 또는 null 값으로 초기화된다. 이 영역의 목적은 메모리 사용을 최적화하기 위한 것으로, 실제로 프로그램에 필요한 메모리 공간만 할당하면서, 초기값이 필요 없는 변수들을 효율적으로 관리한다.

 

프로세스와 스레드는 메모리에 적재된다. 프로세스는 실행 중인 프로그램의 인스턴스로, 자신만의 메모리 공간(코드, 데이터, 힙, 스택 등)을 가진다. 스레드는 프로세스 내에서 실행되는 실행 단위로, 프로세스의 메모리 공간을 공유하지만 자신만의 스택을 가진다. 이를 통해 스레드 간의 데이터 공유와 통신이 용이하다.

 

프로세스간 통신(IPC, Inter-Process Communication)은 독립적인 프로세스들이 데이터를 주고받거나 동기화하는 방법이다.

  1. 파이프(Pipes): 단방향 통신 채널. 데이터는 한쪽 끝으로 들어가 다른 쪽으로 나온다.
  2. 명명된 파이프(Named Pipes): 이름을 가진 파이프로, 두 프로세스가 서로 다른 컴퓨터에 있어도 통신 가능.
  3. 메시지 큐(Message Queues): 메시지 기반의 통신 방식. 프로세스는 메시지 큐에 데이터를 쓰거나 읽음.
  4. 세마포어(Semaphores): 프로세스 동기화에 사용. 공유 자원에 대한 접근을 제어.
  5. 공유 메모리(Shared Memory): 프로세스들이 데이터를 공유하기 위해 메모리의 동일한 부분을 사용.
  6. 소켓(Sockets): 네트워크를 통한 프로세스 간 통신을 가능하게 함. 서버와 클라이언트 모델을 사용.

각 기법은 사용 상황과 요구 사항에 따라 선택된다. 예를 들어, 실시간 데이터 교환은 소켓이나 메시지 큐를 사용하며, 큰 데이터 블록의 공유는 공유 메모리가 적합하다. 동기화와 순서 보장이 중요할 때는 세마포어나 메시지 큐를 고려한다.

 

yunsejin 폴더에 stack와 queue 구현 코드가 있음 

 

GitHub - yunsejin/Data-Structure-C-Language-Exercises: NANYANG UNIVERSITY Data-Structure-Exercises

NANYANG UNIVERSITY Data-Structure-Exercises. Contribute to yunsejin/Data-Structure-C-Language-Exercises development by creating an account on GitHub.

github.com

 

728x90

+ Recent posts