메모리 모델과 관리 방식에 대하여

Suntory(SY Kim)·2022년 1월 19일
1

프로세스 메모리 구조

프로세스란 어떤 프로그램이 실행되기 위해 메모리에 올라간 상태이다. 각 프로세스는 독자적인 주소공간을 가지고 있다. 그 주소공간이 어떤 메모리 구조로 이루어져 있는지 학습한다.

프로세스의 주소 공간

프로세스의 주소 공간은 크게 Code, data, heap, stack 네 가지 영역으로 이루어져 있다.
순서대로 낮은 메모리 주소에서부터 높은 메모리주소로 배정된다. 이 중에서 stack은 특이하게 높은 메모리 주소로부터 낮은 메모리주소로 쌓여나간다.

Code 영역

코드 영역은 실제 소스 코드를 CPU가 실행할 수 있는 형태의 기계어로 형태로 변환하여 저장하는 공간이다.
이 영역은 컴파일 시에 결정되고 수정할 수 없도록 Read-Only의 형태를 띤다.
실제로 CPU는 Program counter가 가리키는 code 영역의 주소를 찾아가서 명령을 수행한다.

Data 영역

전역 변수나 Static 변수 등 프로그램이 사용되는 데이터가 저장되는 공간이다.
코드에 전역변수를 사용하면, 컴파일 시 이 Data 영역에 있는 주소를 가리키도록 설정된다.
전역 변수 값은 변경되는 경우가 있기 때문에 Read-Write의 형태로 작성된다.

Heap 영역

사용자가 생성한 객체들이 저장되는 공간이다. 프로그래머가 필요할 때 사용하는 공간으로서, 런타임에 결정되는 영역이다.
자바에서는 객체가 이 Heap에 생성이 되며 GC가 Heap 영역에서 쓸모없는 객체를 반환한다.

Stack 영역

사용자가 작성한 코드 중 함수 호출 등이 일어날 때 사용되는 리턴 주소, 다음 명령어의 Code 영역 주소, 매개변수, 지역변수 등이 저장되는 공간이다. 함수가 호출됨에 따라 call stack이 쌓여 나가고, return 됨에 따라 정리된다. Stack 영역은 컴파일 시 공간이 할당되기 때문에 무한정으로 call stack을 쌓을 수 없고, 재귀 함수를 무한히 호출하거나 하면 stackoverflow가 발생한다.

OS(커널 영역)의 주소 공간(참고)

우리가 사용하는 운영체제 또한 프로세스의 일종이다. 그렇기 때문에 메모리 상에 OS도 자신의 주소공간을 가지고 있다.

OS의 주소공간에는 프로세스 관리, 효율적인 자원 관리, 인터럽트 처리 등을 위한 특별한 기능들이 저장되어 있다.

Code 영역

  • 시스템 콜이나 인터럽트 처리 루틴 코드

    인터럽트 : 프로그램 실행 중 예기치 않은 상황이나 필요에 의해 실행중인 프로세스를 중단하고 OS에게 CPU 제어권을 넘겨 처리해야 하는 작업들
    시스템 콜 : 일반 프로세스가 OS의 도움이 필요한 특권 명령(ex. I/O 요청)을 요청할 때 일으키는 인터럽트

  • CPU, 메모리 등의 자원 관리를 위한 코드
  • 편리한 인터페이스 제공을 위한 코드

Data 영역

  • PCB(Process Control Block) : 프로세스의 관리를 위한 특별한 자료구조로, 현재 프로세스의 동작 상태/ 어디를 실행중인지 알 수 있는 Program counter 등이 포함되어 있다. 각 프로세스마다 운영체제가 독자적인 PCB를 관리한다.
  • CPU, 메모리 등 하드웨어 자원의 관리를 위한 자료구조

Stack 영역

  • 각 프로세스의 커널 스택을 저장
    • 커널 모드가 끝나고 원래 프로세스로 돌아가기 위한 정보의 일부를 저장한다.
    • 각 프로세스가 커널 모드에 진입하여 함수를 호출하면 커널영역의 스택에서 작업을 수행한다.

프로세스와 스레드

스레드란, 프로세스 내에서 실행되는 흐름의 단위이다. 일반적으로 단일 프로세서 구조에서는, 하나의 프로세스가 CPU를 점유하기 때문에 동시에 한 작업만 수행된다. 시분할 OS에서는 이 CPU 할당을 단기 스케줄러가Round-Robin 방식 등의 CPU 스케줄링을 활용하여 짧은 시간간격으로 CPU 제어권을 여러 프로세스에게 나누어 준다. 그래서 우리에게 동시에 프로세스들이 실행되는 듯한 착각을 준다.

그런데 프로세스가 여러 개의 스레드를 가진다면 어떻게 될까? 분명 한 프로세스이지만 동시에 여러 개의 작업을 동시에 처리할 수 있다. 예를 들어, 시간이 오래 걸리는 I/O 작업을 기다려야 할 때 단일 스레드 환경에서는 I/O를 기다리는 동안 다른 작업을 수행할 수 없다.
그러나 스레드가 여러개인 경우에, I/O 작업을 요청해놓고 기다리는 동안 다른 스레드가 무언가를 수행할 수 있을 것이다.
이를 서비스에 적용한다면 사용자로 하여금 빠른 반응성을 제공할 수 있기 때문에 유용하다.

이런 스레드는 그럼 프로세스처럼 독자적인 주소공간을 가질까?
스레드는 프로세스 내의 실행 흐름이기 때문에 프로세스의 주소공간의 일부는 공유한다.
당연히 같은 프로세스인만큼 코드 영역을 공유하고, 데이터와 힙 영역도 공유한다.
하지만, 독자적으로 함수를 호출하면서 작업을 할 수 있도록 스택 영역은 독자적으로 가진다.
추가적으로 프로세스 관리를 위해 만들어진 PCB 내에도 스레드 별로 별도의 Program counter를 가져야 하기에 TCB (Thread Control Block)의 형태로 관리를 해준다.

스레드의 장점

  • 오랜 시간이 걸리는 I/O 작업 등을 요청하는 동안 다른 작업을 할 수 있어 응답성이 빨라진다.
  • 동시에 여러 프로세스가 돌아가는 것보다 주소공간을 일부를 공유하기 때문에 메모리 공간을 절약할 수 있다.
  • 스레드끼리 Context switch를 할 경우에 프로세스끼리 하는 것보다 적은 오버헤드를 발생시킨다.
  • 멀티 프로세서를 갖는 경우 프로세서 별로 스레드를 병렬로 활용할 수 있어 병렬성이 증가한다.

스레드의 단점

  • 데이터와 힙에 동시에 여러 쓰레드가 접근이 가능해지므로 동기화(Synchronization) 문제가 발생할 수 있다.
    • Critical section 관리를 위한 장치로 인해 성능 저하가 발생할 수 있다.

리눅스 운영체제의 가상 메모리 관리 방식

  • 현대의 메모리는 페이징 기법을 통해 관리되고 있다.
    • 순차적으로 프로세스를 메모리에 올리기 시작하면, 프로세스가 내려갈 때마다 빈 공간이 생기게 된다. 그런데 프로세스마다 차지한 메모리의 크기가 다르기 때문에, 빈 공간은 있지만 프로세스가 들어가지 못하는 외부 조각들이 발생하기 시작한다.
    • 또는 프로세스마다 일정한 크기의 메모리를 할당하도록 만들 수도 있지만, 이 역시 프로세스마다 크기가 다르기 때문에 실제 프로세스보다 큰 메모리 공간을 가져 내부 조각을 만든다. 이 모든 것을 해결하기 위한 것이 페이징 기법이다.
  • 페이징 기법에서는 메모리를 일정한 크기의 프레임 단위로 자른다. 그리고 동일한 크기로 프로세스를 자르고 각각의 자른 요소를 페이지라 부른다.
  • 그래서 당장 프로세스 중 당장 실행될 부분의 페이지만 메모리 상에 올려 관리한다. 이 때, 프로세스의 논리주소(가상 메모리)에 해당하는 페이지가 실제 메모리의 어디에 위치하는 지 확인해야 하기 때문에 Page table이 사용된다.
    • 기존에 논리 주소를 물리주소로 변환하기 위해 사용되던 하드웨어인 MMU(Memory Management Unit)의 계산도 복잡해진다.
    • 즉, 이제 CPU가 메모리로부터 어떤 자료를 가져오려면 페이지 테이블 참조를 위해, 그 페이지에 접근하여 정보를 가져오기 위해 총 2번의 메모리 접근이 필요해진다.
      • 메모리 접근은 일반적으로 시간이 많이 들기 때문에 TLB(Translation Look-aside Buffer)를 두어 최근에 참조되었던 페이지의 물리 주소 정보를 가지고 있는다. 그래서 CPU가 페이지 테이블을 참조하기 이전에 먼저 TLB를 조회하여 원하는 페이지가 있다면 바로 페이지의 물리주소로 접근하여 정보를 가져올 수 있다.
  • 추가로 메모리 할당과 페이징 기법에 대한 자세한 설명은 운영체제 강의를 들으면서 정리할 필요성을 느낀다.

리눅스의 역할

Virtual Memory

  • 가상 메모리를 통해 CPU로 하여금 메모리 상에 온전한 프로세스가 다 올라와있다고 착각하게 만든다. CPU는 논리주소를 참조하여 명령을 수행하는 데 이 논리주소가 가상 메모리 상의 주소라고 볼 수 있다. 하지만 실제로 물리 주소상에 일부 페이지만 올라와 있고, Swap area에 사용하지 않는 페이지를 관리함으로써 메모리를 절약한다.

Page fault 관리

  • 만약 CPU가 페이지를 요구했는데 메모리 상에 없는 페이지라면, Page fault가 발생한다. (페이지 테이블 상 Valid bit를 통해 메모리 상 적재유무를 확인한다.) 리눅스는 CPU 제어권을 넘겨받아 Swap area로부터 없는 페이지를 가져다 메모리 상에 올리는 역할을 수행한다. 이 때, TLB에도 등록해준다.

Page Replacement 관리

  • 만약 현재 페이지 프레임 상 빈 공간이 없다면 어떻게 해야 할까? 현재 프레임에 올라온 페이지 중 하나를 Swap area로 쫓아내야 한다. 이 과정은 중기 스케줄러를 통해 이루어지는 데, 가장 효율적이라고 알려진 알고리즘은 LRU(Least Recently Used Algorithm) 이다. 페이지들 중 가장 오랫동안 사용되지 않은 페이지를 교체하는 것이다.
    • 그러나 문제가 있다. 리눅스는 Page fault가 발생했을 때만 제어권을 받기 때문에 그 동안 페이지가 어떻게 참조되었는 지는 알 수 없다. 그래서 LRU 알고리즘을 메모리 관리에서는 사용할 수 없다.
      • 대안으로 나온 것이 Clock Algorithm이다. 페이지 테이블에 Reference bit을 추가하여 페이지가 참조될 때마다 이 bit를 1로 바꾼다. 그러다 페이지 교체가 일어나면, 한 방향으로 페이지를 탐색하면서 1인 비트는 0으로, 0인 비트를 만나면 그 페이지를 교체한다.
      • 페이지를 교체할 때 변경사항이 있었는지 체크하고 변경사항을 기록하는 것도 리눅스의 역할이다. 이 때, 변경사항의 유무를 판단하는 dirty bit가 페이지 테이블에 추가된다.

Thrashing 관리

  • 실행중인 프로세스가 많아지면, 프로세스 당 사용하는 페이지 프레임의 개수가 떨어진다.

    • 이 과정에서 잦은 Page fault가 발생하고, CPU 사용률이 떨어진다.
      • 운영체제는 이를 CPU의 낭비라고 판단하여, 프로세스를 더 많이 메모리에 올린다. 이렇게 악순환이 반복되는 것을 Thrashing이라 한다.
  • Thrasing을 관리하기 위해 Working set/Page fault frequency 알고리즘을 사용한다.

    • Working set : 특정 프로세스의 최소 필요 Page frame을 계산하여 그만큼 확보되었을 때마다 메모리에 페이지를 올리고, 쫓아낼 때도 Working set 단위로 쫓아낸다.
    • PFF : Page fault rate의 상한과 하한을 정해놓고 PFF가 높아지면 지급하는 프로세스 당 프레임의 개수를 늘리고, 너무 낮으면 지급하는 프레임의 개수를 줄인다.

메모리 고갈상황과 CPU 사용률을 체크하는 이유

  • 메모리가 고갈된다면?
    • 프로세스들의 Swap이 활발해지면서 CPU 사용률이 하락
      • 쓰레싱 발생 -> 해소되지 않는 경우 Out of Memory 상태로 판단하여 중요도가 낮은 프로세스를 종료한다. 이 과정에서 서버 프로세스 등의 중요 프로세스가 종료될 수 있다.
  • CPU 사용률을 지속적으로 체크하는 이유
    • 특정 시점만 체크한 경우에는 신뢰도가 떨어진다.
    • 연속적으로 체크한 경우 급격하게 CPU 사용률이 떨어지는 구간을 발견할 가능성이 있다.
      • 이 때, 메모리 적재량을 함께 체크하면서 쓰레싱 유무를 확인할 수 있다.
      • 추가적인 서버 자원을 배치하는 등 해결방안을 마련할 수 있다.
profile
천천히, 하지만 꾸준히 그리고 열심히

0개의 댓글