본문 바로가기

OS

Memory

메모리

  • 휘발성이지 않은 데이터(외움)
  • 사라질 수도 있는 데이터(망각)
  • 저장할 수 있으면 메모리다

 

bit

  • 메모리를 사용하기 위해 도입한 방법
  • 8bit = 1byte
  • 4byte = 32bit
  • 8byte = 64bit
  • bit는 컴퓨터에서 사용할 수 있는 슬롯수(2^n)만큼 사용가능, 즉 술롯을 하나만 늘려도 2^n-1의 두배만큼 늘어난다.

 

진수

  • 수가 그 진수만큼 나오면 옆으로 넘김
  • 4bit = 2^4 = 16 = 0xF
  • 8bit = 2^8 = 256 = 0xFF
  • 12bit = 2^12 = 4096 = 0xFFF
  • 16bit = 2^16 = 65536 = 0xFFFF
  • bit를 16진수로 표현하면 보기도 편하고 외우기도 쉽다.

 

메모리 종류와 캐싱

  • 용량단위로 크기도 작고 비싸기 때문에, 그래서 필요한게 캐싱
  • 많이 쓰일수록 상위로 올리자

 

캐싱

  • 데이터는 전체를 옮기는 것이 이닌 일정부분만 상위로 올림
  • 게임 CD를 넣으면 CD ROM 디바이스를 통해 감지해서 가상볼륨을 만듬
  • 가상볼륨을 HDD로 옮김
  • HDD에 있는 내용을 RAM에 옮김
  • RAM에서 게임에 실행시키기에 최소한의 값들을 CPU에 올림
  • CPU는 그 값들을 빠르게 처리
  • L2와 L2에서는 그 메모리를 저장해서 추후에 다시 사용할때 캐싱된 데이터를 사용해서 빠르게 처리(=캐시 히트)

 

코드단위 메모리 구분법

  • al Memory : 함수 안에서 사용되는 메모리
  • Global Memory : 함수 밖에서 사용되는 메모리
  • Static Memory : 프로그램 단위를 나타내는 메모리
  • External Memory : 라이브러리 / 모듈 단위 나누는 메모리

 

쓰레드 프로레스 단위 메모리 구분법

  • 쓰레드의 TLB
  • 프로세스

 

프로레스 단위 메모리 구분법

  • 데이타섹션
  • 코드섹션
  • BSS
  • Static

 

리틀엔디안 VS 빅엔디안

  • 1234를 12를 넣고, 34를 넣으면 이것이 빅엔디안(큰값부터 넣는다)
  • 1245를 34를 넣고, 12를 넣으면 이것이 리틀엔디안(작은값부터 넣는다)
  • 1234를 보낼때 리틀(34, 12) -> 리틀(12, 34) => 34, 12로 보냈는데 12, 34로 읽게되어버림
  • 그래서 통신을 할때는 빅엔디안을 사용한다.
  • 빅 -> 빅 => 빅
  • 빅엔디안을 쓰는 이유는 쉽고 바뀌지 않기 떄문
  • 리틀엔디안을 쓰는 이유는 다운캐스팅을 할때 0x12345678(int) -> 0x5678(short) 할때 리틀엔디안을 쓰면 그냥 짜르면 그만 즉 메모리 접근의 효율이 좋다.
short value = 0x1234;
short* pv = &value;
printf(*pv); //1234
char* cpv = pv;
printf(*cpv); //34

 

포인터

  • 점을 찍는 행위자
  • 주소값을 가리킨다.

  • 포인터의 증감연산자는 다음 섹션(=스크린)을 가리킨다. 즉, 포인터가 지정할 수 있는 크기의 다음 부분을 가리킨다.

  • p는 0번지 주소값을 가리키고, *p를 해줘야 역참조해서 0123이 출력된다.
  • s는 0번지 주소값을 가리키고, *s로 역참조를 해줄경우 01이 출력된다.
  • p포인터의 ++증감 연산은 다음 스크린으로 넘겨 4567데이터의 주소를 가리킨다.
  • s포인터의 ++증감 연산은 다음 스크린으로 넘겨 23데이터의 주소를 가리킨다.

  • 위의 그림은 위가 리틀엔디안 아래가 빅엔디안인 경우 입니다.
  • 리틀엔디안의 경우 int* p 에서 short* s로 다운캐스팅을 할경우 그대로 7856을 사용할 수 있습니다.
  • 빅엔디안의 경우 int* p 에서 short* s로 다운캐스팅을 할경우 1234가 나오게 되어 원하는 값을 얻기 위해서는 s++를 코드상 추가적으로 작업을 해줘야 합니다.
  • 즉 최적화 측면에서 리틀엔디안을 사용한다.

 

페이지 레이아웃

char* a = "test"
a++;
  • 위 코드에서 char* a = 은 코드영역
  • 위 코드에서 test는 데이터영역
  • 전역변수는 bss / static 영역에 저장된다.
  • 스택메모리는 리턴가능한 공간에서만 쓸 수있다. (=로컬메모리)

  • a는 스택영역에 int형이기 때문에 리틀엔디안 방식으로 23 01 00 00이 입력됨
  • a를 가르키는 지시자 역할은 SP(=Stack Point)가 전담
  • SP의 ++의 기준은 Register 0x86 32bit 4byte만큼 움직인다.
  • 만약 Short일경우 23 01만 쓰게 되고 나머지 영역은 비어있게 된다.
  • OS는 23 01을 사용하고 -2를 시켜서 남은공간을 쓰는 것보다 4바이트를 넘겨서 다음 영역에 쓰는것이 더 효율적이라고 판단했기 때문이다.

 

SP(Stack Pointer)

void main(){
    int a = 123;
    int b = 123;
    f()
    int c = 123;
    return;
}
void f(){
    int a = 123;
    return;
}
  • 스택메모리의 크기를 12byte라고 가정하고 위의 코드를 실행한다.
  • 스택메모리 0x0에 a를 저장한다.
  • 스택메모리 0x4에 b를 저장한다.
  • f()를 불러 0x8에 a를 저장하면 스택메모리의 공간이 꽉차게 된다.
  • f() 함수에서 return을 만나면 쓰레드가 현재 실행한 코드 수만큼 SP를 --한다.
  • 함수에서 리턴을 만나 --된 SP의 현재 주소값은 0x4를 가리킨다.
  • c를 0x4 덮어 씌워 저장한다.

 

IP(instrution Pointer)

  • IP는 함수를 만나면 함수의 주소값을 얻는다. 즉 현재함수와 호출함수의 오프셋을 받아 오프셋만큼 더한다.
  • IP는 함수를 만나면 현재 주소값 0x0 + 명령어의 크기만큼 더해서 스택메모리에 저장해서 넘어간다.

 

BP

  • IP를 SP에 넣을당시의 주소를 저장하는 레지스터
  • return을 만나면 SP - BP + 메모리 크기 => 함수 호출하고 실행한크기를 알아냄
  • 실행한 크기만큼 SP--시킴

 

RET

  • Return문을 만나면 수행하는 어셈블리 명령어
  • 현재 SP를 읽어서 IP에 집어넣는 역할

 

Heap 영역

  • new 연산자로 만들어지는 변수들
  • 개발자가 할당 / 초기화 / 삭제를 모두 해줘야되는 영역
  • new 연산자가 포인터를 돌려주는데 그걸로 SP나 IP의 역할들을 개발자가 다루는 형태
  • 제대로 소멸시키지 않으면 메모리 누수

 

세그먼테이션

  • 메모리를 어떻게 나눌지
  • 코드영역에 실제 주소가 0x0이고, 스택영역이 0x10이면, 스택영역을 항상 0x0으로 볼수있게 하는 역할
  • 스택영역의 실제주소는 DS에 저장됨
  • 코드영역의 실제주소는 CS에 저장됨
  • 스택영역의 a라는 데이터가 0x1000에 위치한다면 그 값은 스택영역의 0부터의 오프셋이라는 크기이다.
  • 즉 스택영역의 실제 주소가 0x10이라고 가정하면 0x10(DS) + 0x1000(Offset)을 해줘야 실제 주소가 나오게 된다.

 

정리

void main()
{
1.    int a = 123;
2.    F();
7.    int c = 456;
8.    return;
}


void F()
{
3.
4.    int a = 123;
5.    int c = 123;
6.    return;
}

 

SP BP IP Stack
0x0   1 123
  • SP는 0x0을 가리키며, SP를 통해서 123을 스택에 집어넣는다.
  • 그 후 SP는 ++되어 0x4가 된다.
SP BP IP Stack
0x4   2 [123, 7(IP + 명령어길이)]
  • 함수를 만나고 SP에 함수가 끝났을때의 주소값을 넣어준다. (현재 IP + 명령어길이)
  • 그 후 SP는 ++되어 0x8이 된다.
SP BP IP Stack
0x8 0x8 3 [123, 7(IP + 명령어길이)]
  • 함수 내부로 들어와서 BP에 SP의 주소값을 넣어준다. (함수프롤로그)
  • 그 후 SP는 ++되어 0xC이 된다.
SP BP IP Stack
0xC 0x8 4 [123, 7(IP + 명령어길이), 123
  • 변수 123을 현재 SP에 넣는다.
  • 그 후 SP는 ++되어 0x10이 된다.
SP BP IP Stack
0x10 0xC 5 [123, 7(IP + 명령어길이), 123, 123]
  • 리턴을 만난뒤 SP에 BP의 주소값을 넣는다. (0x8)
  • 그 후 SP는 --되어 0x4가 된다.
  • ret을 만나서 IP에 SP가 가리키는 주소값(7)을 넣는다.
  • SP가 올라가서 할당이 해제되었지만, 데이터는 안지워진상태
SP BP IP Stack
0x8 0x8 7 [123, 7(IP + 명령어길이), 456, 123
  • 변수 456을 현재 SP에 넣는다.
  • 기존에 있던 123이 456으로 덮어씌워진다
SP BP IP Stack
0x0 0x8 7 [123, 7(IP + 명령어길이), 456, 123]
  • 마지막으로 호출규약(c++, window)에 의해 sp가 0x0으로 초기화된다.

 

논리주소, 선형주소, 물리주소

논리주소 -> 선형주소 -> 물리주소로 변경이 된다.

요즘은 세그먼테이션을 잘 안쓰이기 때문에 논리주소 -> 물리주소로 변경된다.

논리주소

  • 0x1234
  • 개발자들이 쓰기 편한 주소

선형주소

  • CS:0x04
  • CS세그먼테이션에서 0x04번 오프셋

물리주소

  • 실제 물리주소
  • RAM에 3번째 칩셋에 있다.

 

외부 프래그먼트

위의 그림과 같이 메모리가 다음과 같이 사용하고 있을 때, 6바이트의 공간이 비어있지만 3바이트 크기의 메모리는 사용하지 못하는 현상

 

페이징

  • 외부 프래그먼트를 해결하기 위해 나온 방법
  • 메모리를 미리 짤라(4kb만큼) 놓는 상태
  • 데이터 가운데 할당되지 않게 하기위해 해놓은 상태
  • 하지만 4kb보다 적은 메모리가 할당되었을 경우 안쪽데이터가 남는 내부 프래그먼트가 발생한다.

 

더 깊게으로 들어와서 개발자가 사용하는 논리주소는 3가지로 나눠져 있습니다.

만약 논리주소가 0x02010100 이라면 다음과 같습니다.

  • PET는 물리주소가 매핑되어있다.
  • PDT에는 PDDT의 오프셋이 매핑되어있다.
  • PDDT는 PET의 오프셋이 매핑되어있다.
  • 02 : PDT에 2번째 오프셋 테이블 리스트
  • 01 : 2번째 PDDT에서 1번째 4byte가 매핑되어있는 PET번호를 가리킨다.
  • 01 : 해당 PET가 가리키는 물리메모리는 10번째에서 00번째 4byte를 가리킨다.
  • 00 : PET가 매핑된 물리메모리의 00번쨰 (이것이 물리주소)

결론적으로 이렇게 나눈 이유는 트리구조이기 때문에, 데이터 구분의 장점으로 데이터를 효율적으로 사용이 가능하다.

 

메모리 주소 변환

논리주소 CS:0x03 만약 세그먼테이션을 사용하게 된다면, CS offset이 0이 아니고 0x34같이 오프셋을 가지게 된다. 그래서 이값을 0x03 + 0x34 더해서 나온주소가 선형주소이다.

0x37의 비트를 쪼개서 이것을 이용해서 PDT을 접근한다.

그래서 나온 최종결과가 물리주소가 된다.

 

가상 메모리

램이 1GB 안되는데, SSD쪽에 접근할 경우 서로의 주소가 바꿔서 사용하는것이다.

 

페이지폴트

  • 만약 내가 접근하는 주소가 메모리에 올라가 있지 않을 경우, PET에 페이지폴트 플래그가 0으로 되어있음.
  • mmu(memory management unit)에 의해서 CPU에 페이지폴트 인터럽트를 날림
  • CPU는 메모리들 중 가장 안쓰는 페이지(victim page)를 선택하여 페이지 폴트 플래그를 1에서 0으로 바꾼다.
  • 내가 접근하는 주소의 페이지 폴트 플래그를 1로 바꿔 서로 스위칭한다.

 

페이지폴트 쓰레싱

  • 만약 victim page를 선택하는 알고리즘이 안좋아서 많이 쓰는 페이지를 선택해서 계속해서 퀀텀때마다 페이지가 교환될 경우 발생하는 현상
  • CPU 스케쥴링 속도보다 페이지 교체속도가 더 길어서 컴퓨터가 느려짐

 

copy on write

  • 메모장을 키고 아무것도 안하고, 또 메모장을 키면 동일한 메모리 주소값을 보고있다.
  • 그상태에서 메모장 하나에 글을 적을경우 메모리에 새롭게 적재되어 주소값이 변경된다.
  • 이러한 방식으로 최적화 된다.

 

메모리 로컬리티

  • 페이지 폴트를 적게 일으키자.
  • 메모리가 가까운곳으로 접근할 수 있게 짜자
for(int i=0; i<4; i++){
    for(int j=0; j<100; j++){
        arr[j][i]
    }
}
// arr[0][0]
// arr[1][0]
// arr[2][0]
// ..
for(int i=0; i<100; i++){
    for(int j=0; j<4; j++){
        arr[i][j]
    }
}
// arr[0][0]
// arr[0][1]
// arr[0][2]
// ...
  • 위의 두코드는 모두 400번을돌지만 아래의 코드가 훨씬 페이지폴트가 적게 발생하여 효율적으로 돌아간다.

 

Spatial locality / Temporal locality

  • Spatial locality : 우리가 짠 코드에서 자주 참조가 되는 변수여야한다.
  • Temporal locality : 그 주변 변수도 자주 참조 되어야 한다고 생각하고 코드를 짜야된다.

'OS' 카테고리의 다른 글

Thread  (0) 2022.03.17