Byeonguk Kim

안녕하세요. 29살의 조금은 늦은 나이로 새롭게 개발자로 시작하는 신입 개발자입니다. 포트폴리오 [https://deaguowl.github.io]

컴퓨터공학 08. 메모리&가상메모리

15 Mar 2019 » 컴퓨터공학

2019.03.16 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기.

  1. 우리가 입력한 정보는 어떻게 메모리에 저장되는가?
  2. 변수를 스택에 할당하면 왜 빠르고 힙에 할당하는 왜 느린가?
  3. 언제 힙을 사용하는가?
  4. 함수를 호출할 때 어떻게 스택프레임이 쌓이는가?
  5. 가상 메모리란 무엇인가? 페이지와 스왑은 무엇인가? 디스크와 메모리의 속도차이는 어느정도이고 그 원인은 무엇인가? SSD의 장점은 무엇인가?
  6. 10기가바이트짜리 파일을 C 드라이브에서 D 드라이브로 복사를 한다. 얼마나 걸릴까? A 폰에서 C 드라이브로 옮겨온다면? 만약 1메가바이트짜리 1만개라면 얼마나 걸릴까? 카피를 동시에 여러개 돌리면 더 빨라질까?

한번에 보기

메모리

메모리(memory)

# 미리알면 좋은 정보.

  • 메모리 저장 방식에는 빅 엔디언과 리틀 엔디언이 있다.
    • 사람은 빅 엔디언 방식으로 사용하고
    • 컴퓨터는 대부분 리틀 엔디언 방식으로 저장한다.
    • 단 네트워크를 이용할 떄는 빅 엔디언 활용.
    • 참고

메모리 계층(hierarchy)

  1. 레지스터
  2. 캐시(cache 미스와 cache hit)
  3. 메모리
  4. hard disk(기계식) / SSD (전자식)
  • 위로 올라갈수록 빠르다.
  • 위로 올라갈수록 용량이 작다.
  • 레지스터가 그렇게 좋으면 다 레지스터로 구성하면 안되냐? —> 비싸다

메모리 특징

  • 하드디스크에 있는 데이터는 바로 레지스터로 갈 수 없다. 모든 데이터 이동은 순차적으로 이루어져야 한다.
    • register —> ALU or 다른 register —-> 1cycle
    • Cache —> register —> 3 cycle
    • memory —-> register —-> 20cycle ~ 100 cycle
    • hard disk —> register —-> 50만 ~ 5000만 cycle

위와 같은 속도 차이로 인해 프로그래밍 할 때 데이터 베이스에 최소한 적게 접근해야 한다.
SQL은 데이터베이스로 접근하기 위해 쓰는데 최소한 적게 써야 한다.

Cache도 결국 속도를 빨리 하기 위한 것이다.

cpu가 3ghz짜리가 나와도 memory에서 데이터를 가지고 오는데 시간이 오래 걸리면 다 소용이 없다. 그래서 나온게. 캐시이다.

ex)

>>>li = [1,2,3,4,5]
>>>res = 0
>>>for e in li:
    	res += e

—> 위의 코드는 바로 계산 못한다 cpu로 가지고 와야 한다.
먼저 위의 값들이 어떻게 메모리로 저장되는지 알아야 하고 그리고 그 저장된 것을
어떻게 cpu로 가지고 오는지를 알아야 한다.

  • 기본적으로 300cycle이 실행된다.
    1. res에 계속 접근한다 . –> temporal locality
    2. li에 순차적으로 접근한다. —> spatial localit

위의 코드를 실행할 때 cpu는 메모리에 위와 같이 접근한다고 볼 수 있다. 따라서 여기서 지역성이라는 것이 나오게 된다.

  • locality는 2가지 개념이 있다.
    1. 시간지역성temporal locality –> 한번 접근한 메모리에 자주 접근
    2. 공간지역성spatial locality —> 접근하는 메모리가 이전에 접근한 메모리의 근처일 확률이 높다.

위에 2가지 특징을 알게 되었고 그래서 이것을 해결하기 위해 캐쉬라는 것을 설치했다.

먼저 CPU —> CASHE에게 list값이 있는지 물어보고 없으면 메모리에서 cashe에 저장한다.
list만 가져오는 것이 아니라 list 주변의 메모리 데이터를 듬뿍 가지고 온다.(대략 64byte ~ 128byte cachelines)

  1. CPU가 캐시에 데이터를 요청했는데 데이터가 없어서 메모리에 요청할 때 —-> 캐시 미스
  2. CPU가 캐시에 데이터를 요청했을 때 캐시에 데이터가 있을 때 —-> 캐시 히트

기본적으로 캐시히트가 일어날 확률이 95%이다.

캐시의 위치 cpu — ram 사이에 존재하고 있었음
but 지금은 L1캐시는 cpu안에 있고 L2, L3는 밖에 있음

가상 주소 공간

  • 하드디스크의 페이지파일을 메인 메모리 처럼 사용 => 가상 주소 공간
  • 가상 주소 공간(假想 住所 空間, 영어: Virtual Address Space; VAS)은 가상 메모리 기법으로 제공되는 주소 공간으로서, 프로세스의 관점에서 사용하는 주소이다.
  • 프로그램을 더블클릭하여 실행하면 하드디스크에 있던 프로그램이 메인 메모리로 올라오면서 프로세스가 생성되고, 32bit운영체제라면 실행되는 순간 4GB 메모리를 할당받는다.

할당 받은 램 4g (32bit 일 때)

크게 user영역과 kernel영역으로 나누어진다.

2g (user 영역)

  • 코드
  • 데이터
  • 스텍

2g ( kernel 영역 - os를 실행시킬 수 있는 데이터들이 들어간다.)

  • 코드
  • 데이터
  • 스텍

옛날은 pc가 직접 메모리 주소를 가르켰다.
–> 잘못하면 커털영역의 메모리 주소를 불러오게 할 수 있고 그러면 블루스크린이 뜨면서 시스템이 꺼진다.

지금은 os가 커널 영역을 직접 관리해서 보안이 훨씬 나아졌다.

하지만 실제로 내가 프로그램을 짤 때는 나에게 운영체제가 무조건 메모리를 4g를 준다고 생각하고 설계하면 된다. 왜 그럴까?

바로 가상 주소 공간(vas) 때문이다.

  • 가상 주소 공간은 가상 메모리 기법으로 제공되는 주소 공간이므로 가상 메모리에 대해 알아야 한다.
  • virtual address space(메모리가 아니다.) 우리는 메모리 처럼 쓰면 되지만 실제로는 메모리가 아니고 os가 우리에게 주는 가짜 메모리 공간이다.

가상 주소 공간에 대해 좀 더 명확히 알기 위해서는 메모리에 대해 좀 더 알아야 한다.

우리가 코드를 작성하고 저장하면

  • 우리가 작성한 함수나 클래스 정의 코드는 인스트럭션으로 컴파일 되어서 하드에 저장되었다가 호출할 때 코드 세그먼트로 들어가는 게 된다.
  • 데이터 세그먼트에는 전역변수가 들어간다. BSS 포함이다.(이건 뭐지?)
  • 스텍에는 지역변수가 쌓이는 공간이다. 스텍프레임이 쌓이는 공간이다.

변수를 왜 저렇게 나누어서 저장할까?

바로 생성 시기의 문제이다.

  • 예를 들어 전역변수는 프로세스가 시작 될 때 생성된다.
  • 소멸시기는 프로세스가 종료 될 때 이다.
  • 생성과 소멸에 대해 프로그래머가 할 수 있는 일이 없다.

아주 잠깐만 필요한 데이터 인데 전역 변수에 넣어 놓으면 굉장히 메모리 낭비이다.
따라서 전역변수를 되도록 쓰지 마라고 한다.!!!

이에 비해 스택프레임이 생성될 때는 함수가 호출 될 때이다.
소멸은 함수 실행이 종료될 때 이다.
즉 프로그래머가 어느정도의 통제권을 가지고 있다.

if 함수 호출이 종료되고 나서도 데이터를 가지고 있고 싶으면, call by reference를 사용한다.

>>>int a = 10; 
>>>func(num):
>>>    num += 100 이면 
>>>func(a)
>>>int a 

라고 하면 a의 값은 바뀌지 않는다. —> call by value

함수 호출이 끝난 다음에도 유지하기 위해서는 call by reference를 해주면 된다.
따라서 b = func(a)으로 해서 전역변수 b를 해주면 남길 수 있으나 이것은 전역변수를 또 생겨나게 하는 것이다.

그래서 나온 개념이 heap이다.

heap은 생성시기는 프로그래머가 원할 때 소멸시기도 프로그래머가 원할 때 이다.
함수 내부에서도 만들 수 있고 없앨 수도 있다. 엄청난 특권이다.
하지만 맹점이 있다. 생성만 해주고 소멸을 안하면 그대로 남아 있는다. (메모리 누수).
또한 할당과 해제가 잦아 메모리 빈 공간이 잘게 나누어 지면서 총합은 충분하지만 관련 데이터가 한 곳에
모이지 못하기도 한다. 이를 메모리 단편화라고하고 메모리 단편화가 발생하면 캐시 미스 확률이 높아진다.

heap의 단점

  1. heap에 할당은 했는데 지을 수 없을 때 —> 메모리 누수(memory leak)
  2. malloc이 느리다.
  3. 메모리 단편화(memory fragmentation)

stack 과 heap 비교

stack은 메모리를 할당할 때 차곡 차곡 쌓음 해제를 할 때도 위에서 부터 해제해서 메모리 레이아웃이 연속적이 된다.
—> 할당 할 주소를 찾을 필요가 없음(stack pointer) —> 스피드가 빠르다.

heap에 쌓을 때 (double linked list)

  1. malloc() 처음 쌓을 때 왼쪽위부터 그 다음에 이어서 오른쪽으로 쌓음 그리고 아래에도 이어서 막 쌓음. malloc을 가르키는 주체는 stack프레임에 있음(변수). 지우게 될 때 중간 중간에 막 지우게 된다. malloc이 쌓일 때 가장 앞쪽에는 header를 넣음(주소값 및 크기) 이 헤더가 linked list로 구현되어 있음 이 header들은 지워지지 않고 남아져있음. 따라서 os 가 이 header들을 찾아다니며 넣을 수 있는 곳을 찾음

  2. if 딱히 저장 할 곳이 없을 때(1개의 크기 문제 or 공간이 꽉 차서) 남는 곳으로 보면 충분히 넣을 수 있는데 메모리가 조각조각 비워있으니깐 (단편화가 되어있어서) 메모리를 할당할 수 없음. —> 메모리 단편화(전체로 봤을 땐 충분하지만 메모리가 쪼개져 있어서 메모리를 할당 할 수 없거나 찾는데 오래 걸리는 경우) stack에는 메모리 단편화가 생길 수 없음

메모리 단편화 해결방안

  • 제일 먼저 잡히는 힙의 공간을 default heap이라고 한다.시작을 하자마자 한방에 malloc에 1.6g정도 요청을 함. (heap은 메모리 바운더리가 없다. stack은 한계가 정해져있다)

일반적으로 user에게 2g가 할당되면

  • code - 17m
  • data - 4m
  • heap
  • stack - 1m

약 1.98g 만큼이 남는데 이만큼 heap이 커질 수 있다.
시작하자마자 malloc으로 1.98g을 받아놓고
이거를 프로그래밍을 통해 내 마음대로 쪼갤 수 있고 나만의 메모리 매니지먼트를 만들 수 있다.

예를 들어 abc.py을 실행한다고 보면 먼저 python.exe 가 실행되고 abc.py이 실행되는데
code 세그먼트에 python.exe가 실행되고 abc.py는 말그대로 editor로 문자열을 python.exe에 넘겨주는 것이다.
따라서 abc.py 그 자체는 공간을 할당 받을 수 없다.
abc를 실행하던중 나오는 것들은 모두 heap에 저장 될 수 밖에 없다.
python.exe 실행하면 저 위에 메모리 전체를 갖고 (이미 실행되어 stack에는 쌓지 못하고) heap에 쌓을 수 밖에 없다.

  • 파이썬을 실행하면 미리 malloc을 통해 일정 용량을 가지고 오고 heap을 적절하게 나누어서 활용한다.(abc 관련된 것은 heap에 쌓인다).
    이것을 dynamic heap이라고 한다.

dynamic heap을 통해 메모리 단편화를 해결할 수 있다.

파이썬에서는 파이썬이 알아서 해준다. 따라서 그냥 melloc은 os가 관리하게 때문에 파이썬에서 쓰면 터질 수 있다.
PyMem.Malloc을 써라. os가 아니라 파이썬이 주도권을 가지고 와서 할당해준다.

앞으로 heap의 관리를 python이 한다 —> dyanmic heap이라고 한다.

개발자들의 이야기 30%는 heap와 stack이야기 이다.

gabage collection :

  • 언어 차원에서 메모리 관리를 해주겠다.—> heap을 내(언어 자체)가 관리해줄게.
  • 파이썬 구현방법 —> reference counter
  • heap에 생긴 메모리의 reference counter을 확인하여 counter가 0이 되면 파이썬이 알아서 삭제 한다.
  • python은 reference counter 기능을 통해 삭제해준다.

파이썬은 heap 안에 부분을 메모리와 완전히 똑같이 만들어 놓았다.

정리하자면

  • code data heap stack은 모두 데이터를 저장하는 공간인데
    code 세그먼트는 우리가 작성한 코드가 기계어로 바뀌어 하드디스크에 저장되었다가
    우리가 호출하는 순간 코드 세그먼트에 저장되고,
  • data 세그먼트는 전역변수가 저장되는 공간으로 프로세스가 시작될 때 생성되었다가
    프로세스가 종료될 때 소멸되어 프로그래머가 할 수 있는 일이 없으며,
  • Stack 세그먼트는 지역변수가 저장되는 공간으로 함수가 호출 할 때 생성되고,
    함수가 종료될 때 소멸되어 프로그래머가 어느정도 통제권을 가지게 되지만 함수가 끝날 때 까지는,
    계속 유지해야 된다는 단점이 있다. 이를 보완하기 위해 나온 것이
  • heap 세그먼트로 heap 세그먼트는 프로그래머가 원할 때 생성하고 원할 대 소멸시킬 수 있다.
    하지만 생성만 해주고 소멸을 안해주면 그대로 남아 지속적으로 메모리를 차지한다.(메모리 누수)

스텍 세그먼트

이번에는 스택 세그먼트에 저장되는 함수의 스택 프레임이 어떻게 할당되고 해제되는지 알아보자.

먼저 2개의 용어에 대해 알아야 하는데

  1. esp(stack pointer)
    • 스택 세그먼트의 맨위를 가르키는 스택 포인터 레지스터
  2. ebp(base pointer or frame pointer)
    • 스택 프레임의 기준
    • 지역 변수에 접근 할 때 프레임 포인터를 이용하여 이동

에 대해 알아야 한다.

스크린샷 2019-03-18 오후 12 41 28

# 사진은 컴퓨터사이언스 부트캠프 with 파이썬에서 가지고 왔습니다 출처

나는 위에 사진으로 최대한 간단하게 이해를 했는데 ebp는 스택 프레임의 중간을 가르키고 있고 ebp를 활용하여 지역 변수에 접근 할 수 있고, 스택 프레임이 새롭게 생성 될 때는 esp가 이동하여 미리 지역 변수 공간을 확보하여 새로운 스택 프레임을 생성한다. 그리고 스텍 프레임이 소멸 될 때는 다시 esp의 위치를 ebp로 옮김으로서 해당 스텍 프레임을 소멸 시킨다.

더 많은 정보를 알면 좋겠지만 ㅠㅠ.. 이 정도만 하고 넘어가려고 한다.

가상 주소 공간(virtual address space)

길게 돌아서 다시 왔다 ㅠㅠ.

  1. 가상 메모리 정의
    • 메인 메모리를 확장하기 위해 페이지 파일(page file)이라고 불리는 하드디스크의 일정 부분을 메인 메모리 처럼 사용하는 것을 말한다. —> 리눅스에서는 swap이라고 부른다.
  2. 가상 주소 공간 정의
    • 가상 메모리 기법으로 메모리에 주어지는 가상 메모리 주소 공간

MMU

가상 메모리에서는 가상 메모리 주소가 주어지는데 이를 논리 주소(logical address)라고 하고 메인 메모리의 메모리 주소는 물리 주소(physical address)라고 한다. 실제로 프로세스를 실행하려면 메인 메모리의 물리 주소가 필요하다.
이를 위해 논리 주소를 물리 주소로 변환시켜주는 하드웨어가 MMU(Memory Management Unit)이다.

목표 : page와 pagenumber, offset의 개념

if 2000byte가 있다고 하면 주소 값은 0 부터 1999까지 있을 수 있다.
if page를 100byte로 잡으로 총 페이지 갯수는 20개이다.
처음 페이지 #0(넘버) ~ #19

0 번째 페이지 a0 ~ a 99.
1 번째 페이지 a100 ~ a 199 —–> 뒤에서 2번째 앞으로 와서 자르면 페이지의 넘버
if a145 —-> 1번째 페이지 + offset(특정한 값에서 떨어진 정도) – 45.
145 - page # + offset 이것을 그대로 2진수에 적용

  • page : 프로세스를 실행하며 부여 받은 메모리를 쪼개어 쪼개진 한 부분
  • pagenumber : 나누어진 페이지 순서를 나타내는 비트
  • offset : 페이지 안에서 특정 바이트를 가르키는 비트

페이지 넘버와 오프셋을 더하면 논리 주소가 된다.

페이징

우리는 프로세스를 실행하며 부여 받은 4g를 어떻게 할 것이냐? 쪼갤 것이다.
in 32bit 컴퓨터 — > 4096byte —> 4kb로 쪼갠다

쪼개어진 4096byte를 page라고 한다. 2 ** 12

4g는 2 ** 32승 / 2 ** 12 —> 총 페이지수는 2 ** 20

page #0 ~ page # 2 ** 20 - 1 까지 존재

따라서 32비트 시스템에서 메모리 주소를 표현할 때는 32비트를 활용한다. 페이지 갯수가 2 ** 20개 이므로 페이지 넘버를 나타내는 비트는 20 비트고, 페이지 크기가 4,096바이트(2 ** 12)이므로 오프셋은 12비트이다. 이 둘을 합치면 가상 주소 공간에서 하나의 주소 값이 된다.

실제 RAM

메인 메모리 역시 가상 주소 공간과 같은 크기로 쪼갠다. 4096byte크기로 1개씩 다 나누었을 때 이것을 page frame이라고 한다.(진짜 메모리)]]

frame의 갯수는 2 ** 20.
0 ~ frame의 마지막은 2 **. 20 - 1.

특정 frame의 주소를 보았더니 0x5252a/ef3 (16진법 표기 2진수로 바꾸면 20자리/12자리) 5252a가 frame 넘버를 의미하고 이 프레임의 첫번째 위치부터 떨어진 정도 ef3

VAS + Ram의 변환의 큰 그림

cpu가 pc를 보고 vas에게 요청 (0x12345abc)

—> 이것을 실제 램에서 가지고 와야 한다.

여기서 vas가 page 넘버와 오프셋을 쪼갬 12345/abc —> 8자리가 16진수를 나타내는 것이고 2** 20을 나타내려면 16진수 5자리 수로 표현

만약에 실제 ram에서는 0Xabcde/abc에 매칭되어 있다면

페이지 넘버만 12345 —> abcde로 바꿔주면 된다. ( logical address —> physical address)

page table (page fault와 연관)

페이지 테이블은 어떤 프로세스의 페이지 넘버, 상응하는 프레임 넘버, 상태 등을 저장하는 테이블이다.
모든 프로세스는 각자만의 페이지 테이블이 있고 이 페이지 테이블은 메인 메모리에 저장된다.

만약에 우리가 어떤 프로그램을 더블클릭을 하면 os가 프로세스를 시작하면서 4g를 주는 척 하면서 램에 있는 일부 공간에 페이지 테이블을 만든다.

스크린샷 2019-03-19 오후 12 06 28

os가 프로세스를 시작하면 4G를 할당해준다. —> 프로세스마다 페이지 테이블을 main memory에 만들고 pagetable의 첫 주소를 가르쳐준다.(4g를 할당해준다 라고 표현)

VAS를 구현하기 위한 구현체가 pagetable이다. mapping table을 만드는 것 자체를 4g 할당했다고 한다.

if 스타크래프트를 딱 키면 4g를 준다 pagetable은 일단 3가지만 기억(칼럼)

  1. 페이지 넘버
  2. 프레임 넘버
  3. valid bit(유효 비트)

딱 키면 pagetable을 만들고 지금 당장 필요할 것 같은 코드영역의 데이터 혹은 전역변수를 하드디스크에서 4096byte 단위로 메모리에 업로드 —-> freeparing프리페어링기법

가상의 4g에서 cpu가 요청하면 페이지 테이블에서 페이지 넘버에 vas의 페이지 넘버를 프레임 넘버에 실제 램 메모리의 프레임 넘버를 저장한다.

이것의 2가지를 바탕으로 페이지 부분을 바꾸어서 pc를 바꾸어서 MAR(memory address register)에 저장해주고

MMU —> memory management unit —> logical address 를 physical address로 바꾸어 준다.

따라서 4g를 준다는 것은 페이지 테이블을 할당해주는 것이다.

페이지 테이블의 valid bit가 0이면 (즉 프레임 넘버가 있지 않으면, 메인 메모리안에 실제 메모리가 없다. ) —> page fault 페이지 폴트 —-> 하드 디스크에서 비어있는 공간에 램에 올리고 이거를 다시 frame 테이블에 올리고 valid bit를 1로 바꾼다.

page fault가 나면 치명적이다. 하지만 한번 가지고 올 때는 한번씩 꼭 발생한다.

캐시미스와 페이지 폴트는 비교할 정도가 아니다.

다음 코드를 실행할 떄 MMU가 pagetable을 참고하여 확인해줌

내가 스타크래프트를 하다가 져서 음악을 들으려고 유튜브를 키면

process 2가 등장한다. (#페이지 테이블은 공유하지 않는다. 프로세스마다 한개씩 생긴다)

하드디스크 안에서 page file(리눅스 swap)이 있다. 1. 우리가 하드디스크를 메인 메모리로 사용하지 않고 램을 사용하는 이유는 속도 때문이다. 1. 만약에 하드디스크의 속도가 메모리 만큼 빠르면 메모리와 동일하다. 2. 하드디스크의 일정 용량을 마치 램처럼 사용하겠다 —> 이 크기를 페이지파일이라고 한다. (가상 메모리)

그럼 어떤식으로 동작하나??

유튜브도 새로운 페이지테이블이 생긴다.

스카이프를 띄움

프로세스를 한 100개 띄어서 램을 꽉 채움

만약에 스타를 다시 시작하게 되면 valid bit가 0이면 페이지폴트가 나고 하드디스크의 코드를 램에 보내야 하는데 램이 가득 찻기 때믄에 새로운 애를 올리기 위해서 누군가는 내려와야 한다. —-> 내리는 애들 pagefile로 내림 이 내려온 에를 victim page라고 부른다. 이렇게 내려가는 행위를 page out이라고 말한다.(올라가는건 page in)

만약에 cpu가 다시 프로세스 2로 와서 아까 내렸던 애를 요청하면, 램에서 다시 가장 오랫동안 사용하지 않았던 애를 또 내림 (victim page, page out) 그리고 다시 내렸던 애를 page in하여서 새로운 주소를 frame table 올려줌

따라서 frame table을 항상 일정하지 않다.

가상메모리는 ram + page file을 가상메모리라고 하고 이거를 paging 이라고 한다.

if page in , page out 보다 page fault가 더 빨리 일어나면 thrashing이라고 하여서 실행은 안되면서 미친듯이 내부적으로 작동되고 있는 상황을 thrashing이라고 한다.

페이스북 이야기

왜 데이터를 힙에 저장했을 때 페이지 폴트가 많이 일어날까??

gcc가 데이터를 저장할 때 힙에 저장함. malloc의 특성에 의해서 힙의 여러 위치에 저장됨

가상 메모리에서는 특정 단위로 나누어서 가지고 오고 불러올 때 페이지폴트가 일어날 확률이 많아진다???? —-> 이해 안됨

stack에서는 한번만 페이지폴트가 일어나면 다시 일어날 확률이 굉장히 작아짐.

CPPcon 2016 : nicholas ormrod /fbstiring cppcone/