1바이트 = 8비트
32비트시스템의 int 형 크기 : 32비트 = 4바이트
64비트시스템 int 형 크기 : 32비트 , 64비트 형 2가지로 나뉘어져있음
메모리주소에 변수를 저장할 때는 변수명, 해당 메모리 주소, 값 이렇게 3가지가 원래 필요함.
0x400000 으로 시작하는 이유 : 16진수 400000은 약 400만정도이고, 0x0~단에서는 운영체제커널, 등의 시스템영역이 있는데, 이 때 보안을 위해서 주소공간배치 무작위화(ASLR)를 하게 된다.
그랬을 때 0x400000은 충분히 큰 값이기 때문에 과거에 기본주소로 사용해왔었고 이게 호환성,유지보수 측면에서 유리하기 떄문에 계속 기본으로 사용되어짐.
어셈블리어 수준에서는 포인터를 이용해서 메모리주소를 추상화
포인터 ) a = b 를 하고 싶을 때 메모리에서 a에 b를 할당하기 위해 b의 크기만큼의 메모리주소공간을 할당해줘야 한다. 그런데 이렇게하면 메모리 낭비
-> a에 b의 값이 들어있는 메모리 주소를 가리키도록(b의 메모리주소를 저장) 하면 b변수가 저장된 위치를 알 수 있다. (간접주소지정)
고급언어(파이썬,자바)등에서는 포인터개념이 없다. 이 포인터를 추상화해서 참조로 사용한다.메모리 주소 대신 변수나 객체를 가리킴
ex)
a = [1, 2, 3]
b = a
a가 참조하는 객체를 b도 참조하게 만듬 (파이썬은 모든게 객체). a와 b는 동일한 메모리주소를 가리키게되고, a나 b를 수정할 시 둘다 같이 변경됨.
-> 포인터 쓰면 메모리 조작할 수 있어 좋지만 실수했을 때 너무 리스크가 커서 더 높은수준의 추상화개념인 참조를 사용하게됨
0x47f62d8 같은 주소는 실제 물리메모리 주소가 아니라 가상메모리 주소이다.
실제 물리메모리에는 프로세스가 동일한 크기의 여러조각(chunk)로 나뉘어서 저장되어있기 때문에 복잡하다. -> 가상메모리에서 이 메모리 주소들을 영역별로 나눠서 매핑해놓았기 때문에 신경쓸 필요 없음.
이런 매핑관계를 유지하는것 = 페이지테이블
서로 다른 프로세스에서 같은 메모리주소를 사용해도 괜찮은 이유가 여기서 나온다.
각 프로세스마다 하나의 페이지테이블이 있고 해당 메모리주소가 가리키는 물리메모리주소가 각각 다르기 때문에 상관없다. (물리메모리 공간은 하나이고 여기서 chunk되어서 하나씩 각 프로세스의 가상메모리 주소로 매핑됨)
후입선출(LIFO) 순서.
함수 실행 될 때 저장된 정보 구역 -> 스택프레임 or 호출스택(call stack)
먼저 실행 된 함수가 높은주소에 있고 스택영역은 낮은 주소 방향으로 커짐
스택영역이 차지하는 메모리는 함소 호출 깊이에 따라 결정된다.
이후 완료될수록 감소
함수a에서 함수b로 들어갈 때 함수a에서 다음 메모리주소를 미리 저장해놓고 있다가 함수b가 끝나면 그 다음을 명령어를 읽는다.
-> 함수a에서 함수b로 cpu의 제어권이 넘어갔다
반환 : 어디서왔는지 정보
점프 : 어디로 갈지 정보
매개변수, 지역변수는 레지스터가 아니라 해당 함수의 스택프레임에 저장된다.(레지스터의 수는 제한되어있기 때문에)
여러함수에서 같은 레지스터를 사용하면 지역변수가 덮어씌워질 수 있지않을까?
-> 레지스터에 지역변수를 저장하기 전 초기값을 저장해놓고, 함수실행 끝나면 초기값으로 복원하면는 방식을 사용. = 함수의 스택프레임에 레지스터 초기값이 들어가야하는 이유
스택영역의 크기는 제한되어있다. 그런데 함수호출을 많이하게 되면 스택영역이 더 많은 메모리를 차지하게 되고 stack overflow 발생하게됨
-> 너무 큰 지역변수x . 함수 호출 단계 너무많이 x
스택영역 밑에 유휴영역(free segment)가 있다.
스택영역 커지면 유휴영역 점유하게됨(동적라이브러리가 적재되는 영역)
특정 데이터를 여러 함수에 사용하고 싶을 때?
1. 전역변수로 사용
2. 힙영역에서 동적메모리 할당 및 해제 (일시적으로 사용이 필요할 때)
힙영역은 프로그래머가 직접 할당/해제 를 통해 수명 주기를 결정할 수 있는 영역을 제공한다.
c에서는 malloc으로 할당, free로 해제.
파이썬은 자동으로 해줌 (가비지컬렉션, 참조카운팅)
import sys
# 사용자 정의 클래스
class MyClass:
def __init__(self, value):
self.value = value
# 객체 생성
obj1 = MyClass(10)
obj2 = MyClass(20)
print(sys.getrefcount(obj1)) # 참조 횟수 확인
# 객체 사용
print(obj1.value)
print(obj2.value)
# 객체가 더 이상 필요 없을 때, Python의 가비지 컬렉터가 자동으로 메모리를 해제합니다.
del obj1
del obj2
이 코드에서 obj1, obj2는 pymalloc을 통해 메모리가 할당되고 del로 객체에 대한 참조가 삭제되면 가비지컬렉터가 자동으로 참조횟수를 감소시키고 참조횟수=0 이 되면 pymalloc을 통해 할당된 메모리를 해제한다.
힙영역은 커다란 배열형태로 되어있고 비어있는 공간을 찾아서 그 메모리를 반환해서 할당시킨다.
-> 그런데 메모리 크기는 각각 다르고 최대한 효율적으로 많은 데이터를 저장하면서 동시에 빠르게 빈 공간을 찾고싶음.
이 때 어느곳이 비어있고 그곳에 무엇이 할당 되어있는지 확인하려면?
-> 메모리 정보 확인 (메모리 조각 조직화)
사용가능한 메모리 조각인지 아닌지 등등 관리하는법
-> 연결리스트 사용. (메모리 사용정보를 저장)
메모리 사용정보 : 헤더(조각 크기. 32비트 고정), 페이로드(할당 가능한 메모리조각)
정보주소를 안다면 다음 노드의 시작주소를 알 수 있음
헤더에는 메모리 조각크기 + 메모리 조각이 비어있는지 알려주는 설정값(flag)
2가지를 함께 저장해야한다. 헤더에 조각의 크기가 나와있기 때문에 다음 노드의 시작주소를 알 수 있게 됨.
페이로드 = 할당 가능한 메모리 조각. malloc 호출시 반환되는 메모리주소가 시작되는 곳
메모리의 마지막 조각에는 끝을 알려주는 특수표시가 있다. 현재 힙 영역이 여유가 있는지, 아닌지를 알 수 있음.
메모리 요청시 메모리 할당자는 적절한 크기의 여유 메모리 조각을 찾아야함.
ex) 요청 : 4바이트
실제 여유메모리 : 8비트, 32비트 하나씩 존재.
이 때 뭘 선택해야할까?
이에 여러 전략이 존재한다.
최초 적합 방식(first fit)
-> 위에서부터 차례대로 탐색하다가 가장 먼저 여유공간 되는곳으로 반환
(완전 최적화가 안되고 작은 메모리 조각이 남을 가능성이 높음)
다음 적합 방식(next fit)
-> 최초적합방식처럼 차례대로 탐색하는데, 대신 시작위치가 여유메모리조각이 마지막으로 발견된 위치에서 시작. (앞부분이 꽉 채워져있다면 처음부터 탐색하는것 보다는 이 방식이 당연히 더 빠를것). 하지만 이 방식은 최초적합방식 보다 메모리 사용률이 떨어진다고함(이유는모름 연구했는데 그렇다고한다)
최적 적합 방식(best fit)
사용 가능한 메모리 조각 모두 찾은 후 그 중 요구사항을 만족하면서 가장 작은 메모리조각 반환.
메모리를 잘 활용할 수 있지만 모든 메모리조각 탐색해야하므로 위의 2가지 방식보다 느리다.
여유 공간 찾았다고 치고 어떻게 할당할것이냐?
헤더 말고 페이로드 주소부분을 반환하면된다.
만약에 12바이트 요청했는데 찾아낸 조각이 32비트다? 이 때 남는 공간을 전부 할당할것이냐?의 문제
-> 그냥 싹 다 할당해버리면 빠르지만 메모리 낭비. 이렇게 사용되지 않는 부분이 생기는 현상 = 내부단편화(fragmentation)라고 한다.
12바이트 만큼은 할당해서 반환하고, 남은 뒷부분(20바이트)는 작은 크기의 새로운 여유메모리 조각으로 만든다.
예를들어 32바이트였던거를 16바이트로 나누고싶다
-> 헤더 4바이트 제외하고 12바이트 할당됨
c언어에서는 free같은 해제함수에 메모리 주소를 전달하면 헤더 정보크기인 4바이트를 빼는 것으로 해당 메모리 조각의 헤더 정보를 얻을 수 있다. 이 값을 여유메모리로 바꾸면 해제가 완료된다.
그런데 이렇게되면 인접한 여유 메모리 조각들이 둘 다 비어있지만 헤더정보때문에 큰 메모리의요청을 만족시키지 못할 수가 있다. 그래서 인접한 여유메모리조각은 병합하는것이 좋다.
-> 메모리 해제될 때마다 병합시키게되면 부담 발생.
그럼 어떻게 효율적으로 병합할 수 있을까?
헤더말고 꼬리(footer)에도 정보를 넣어주면 앞의 메모리 상태를 알 수 있다.
이런식으로 만들면 사실상 양방향 연결리스트가 되는 셈.
메모리 할당자 설계 완료
x86 cpu는 4가지 특권단계를 제공.
0, 1, 2, 3 숫자가 작을수록 cpu의 특권(privilege)는 커짐 (0단계에선 모든기계명령어 실행가능)
특권 : 일부 명령어를 실행할 수 있는 권한
일반적으로 시스템은 0, 3 단계만 사용한다.
3단계 : 사용자상태(user mode)
0단계 : 커널상태(kernel mode)
cpu가 운영체제의 코드를 실행 -> 커널상태
여기서는 cpu가 모든 기계명령어를 실행할 수 있고 모든 주소공간에 접근가능, 제한없이 하드웨어 접근가능.
반대로 프로그래머가 작성한 일반적인 코드를 cpu가 실행할 때 -> 사용자상태
이 상태에서는 특정 주소공간에 접근할 수 없음(실수로 운영체제를 죽이면 안되니까= segmentation fault)
일반적인 응용프로그램 = 사용자상태
cpu는 커널상태에서는 응용프로그램 실행 불가
사용자 상태에서는 운영체제 코드 실행 불가
파일 읽기/쓰기, 데이터 송수신 등 요청이 온다면 어떻게하지?
-> 시스템 호출(system call)을 통해 프로그래머는 운영체제에 서비스를 요청해서 이러한 작업들을 응용프로그램 대신에 처리해줌.
시스템호출 : x86의 INT 명령어(인터럽트)처럼 특정 기계명령어로 구현됨.
시스템콜이 들어오면 커널상태로 전환 -> 운영체제 코드 실행 -> 프로세스로 반환
서로다른 운영체제에서 실행 가능하도록 저수준 계층 간 차이를 감춤 = 표준 라이브러리
표준라이브러리는 사용자상태에서도 실행됨. 실행중인 운영체제에 따라 대응되는 시스템 콜 선택
운영체제와 소통
malloc은 표준라이브러리의 일부다
유휴영역을 사용해서 늘어나게된다. 어떻게 늘릴수 있나?
-> 원래는 운영체제에 메모리 요청해야하지만 리눅스에서는 brk, mmap과 같은 전용 시스템 콜을 이용해서 힙영역 늘리거나 줄일 수 있다.
이 종류가 중요한게 아님. 힙영역 부족하면 시스템콜을 통해 운영체제에 힙영역 늘려달라고 요청해서 늘릴 수 있다는게 중요
과정 예시
ex) 프로그램이 malloc으로 메모리할당 요청
-> malloc이 여유메모리조각 탐색, 적절한크기 찾아서 할당(여기까지 사용자상태)
-> 만약 여유메모리조각 찾지 못할 시, brk 시스템콜 통해 운영체제에 힙영역 늘려달라고 요청(brk는 운영체제 일부분이기때문에 여긴 커널상태)
-> 힙영역 늘어나면 amlloc이 다시 적절한 여유 메모리조각 찾아서 할당
힙영역을 포함한 전체 프로세스 주소 공간은 실제 물리 메모리주소가 아니다.
malloc의 호출이 반환되면 그 메모리는 가상메모리다. (실제 물리메모리는 전혀 할당되지 않은 상태일 수도 있음.)
-> 그렇다면 실제 물리메모리는 언제 할당되냐?
실제 할당한 메모리가 사용되는 순간 물리 메모리를 할당한다.
가상메모리가 아직 물리메모리와 연결 안되어있으면 페이지 오류(page fault) 발생
운영체제가 이 오류 감지하면 페이지 테이블 수정해서 가상메모리와 물리메모리 매핑 제대로해서 물리메모리 할당.
이 과정이 완료되면 프로그램은 할당된 메모리 사용 가능.
malloc으로 요청하는건 복잡하고 빈번한 할당해체 요청떄문에 시스템 성능 영향갈수 있음.
그래서 이 대신 사용 가능한 전략중 하나가 메모리풀
malloc은 표준라이브러리, 메모리풀은 응용프로그램의 일부(메모리풀이 더 고수준 계층)
메모리풀은 특정 상황에서만 적용 가능하고 최적화 되기때문에 범용성은 떨어짐
메모리 할당,해제 속도가 중요할 때(ex임베디드), 여러 객체를 생성하고 해제해야하는 상황(게임개발), 장시간 실행되는 서버(메모리 단편화로 인해 성능저하 일어날 가능성), 메모리 누수 방지, 메모리 사용을 제한하거나 제어해야할 때.
-> 메모리풀은 고정크기 블록을 사용하기 때문에 할당, 해제가 매우 빠르다.
메모리풀은 표준라이브러리, 운영체제 거치지 않고 메모리 할당한다.
메모리풀이 시작될 때 큰 덩어리의 메모리를 할당하고 이를 여러개의 고정크기의 블록으로 나눈다. 이 블록들은 할당/해제 준비가 되어있음.
사용가능한 블록을 추적하기 위해 자유리스트(free list)를 유지한다.
자유리스트에서 블록을 가져오는 방식으로 메모리 할당이 이루어지며, 해제할 시 블록을 자유리스트에 다시 추가하는 방식으로 이루어지며 이 과정은 매우 빠르다.(시간복잡도 O(1). malloc, free보다 효율적).
고정크기 블록 쓰므로 메모리 내부단편화 문제 발생x
생길 수 있는 문제 및 단점)
하지만 고정된 크기를 사용한다는것은 큰 메모리가 요구될때는 처리가 힘들다는 단점이 있음
혹은 필요 메모리 크기가 고정된 블록 크기보다 작으면 메모리 낭비가 될수도 있다.
그리고 메모리풀은 사전에 메모리를 할당해 두기 떄문에 필요 이상으로 메모리 차지할 수도 있다.
그리고 미리 할당할 때 초기화시간이 소요됨 + 이런 문제때문에 다양한 크기블록 쓰거나 동적 크기조절 등의 기능을 추가시 복잡해짐.
메모리가 부족해지면 malloc에 새로운 메모리조각 요청해야하는데 이 때 이전 메모리조각의 2배로 요청한다.
ex) 동적 배열
배열 크기 10, 20, 30, 40 이렇게 늘어나면 재할당,복사 소요시간 많아짐
배열 크기 10, 20, 40, 80 이렇게 꽉찰때 마다 2배씩 늘리면 재할당, 복사 횟수가 줄어들어서 성능 좋아짐.
파이썬의 리스트와 같은 경우가 동적배열에 해당하는데, 처음에는 메모리 사용량이 작다가, 특정 요소 수를 넘어가면 메모리 크기가 급격히 증가한다. 이는 리스트 크기 부족시 파이썬 인터프리터가 내부적으로 더 큰 메모리 블록을 할당하고 기존 요소들을 복사해서 새로운 블록으로 옮기는 과정이 있기 때문.
ex) 처음에 8개 담을 수 있는 크기 -> 9번째 요소 append -> 16개 요소를 수용할 수 있는 메모리 블록 만든 후 요소 복사
메모리풀이 싱글 스레드에서는 잘 작동한다. 그런데 멀티스레드 환경에서는?
메모리풀에 잠금/보호 설정하게 되면 대량의 스레드에서 동시에 할당,해제 요청이 들어올 시 성능저하.
-> 각 스레드마다 메모리 풀을 유지하면 됨. 어떻게? = 스레드 전용 저장소(thread local storage)에 스레드 풀 넣어서 사용하면 각 스레드가 자신에 속한 스레드풀만 사용가능
문제) 스레드a에서 사용한 메모리가 수명주기가 끝났는데 스레드b에서 이걸 사용해야될때?
-> 1. 중앙화된 메모리 관리 시스템 도입.(특정 스레드가 메모리 할당해도 다른 스레드가 해당 메모리에 자유롭게 접근,해제할 수 있도록함). 전역 힙 사용하거나 메모리할당,해제 작업을 중앙화된 관리자에게 위임
어떤 함수의 변수를 가리키는 포인터의 값을 수정했을 때 다른 함수의 스택프레임이 파괴될 수 있다.
포인터는 데이터 형식의 크기를 단위로 움직이므로 데이터 형식의 크기는 신경쓸 필요 없다.
파이썬에서는 mutable/immutable , shallow/deep copy 개념으로 인한 버그가 c언어의 포인터개념과 비슷하다고 할 수 있다.
포인터가 어떤 영역의 변수를 가리키냐에 따라서 프로세스 종료(버그찾기쉬움) or 해당 영역 스택프레임 파괴 (버그찾기 어려움) 등의 문제 발생.
malloc이 여유메모리 찾아서 거기를 사용하려고 할 때 해당메모리는 이전 사용정보가 남아있어서 0이 아닐 수 있다.
-> free함수가 메모리블록을 해제하면 공간은 비워지지만 초기화되진 않기 때문에 내용은 지워지지 않는다.(다시 새로운 할당이 이루어지기 전까지 저장하고있음)
free를 실행 시 바로 모든 데이터가 날라가는게 아니라, 현재 할당 가능한 상태라고 알려주는 것.
초기화하려면 따로 0으로 초기화하는 함수 필요
스택버퍼오버플로우 : 버퍼에 할당된 메모리보다 더 많은 데이터를 쓰려고 할 때 발생.
ex) 길이 10인 배열에 15개의 데이터를 쓰려고 할 때
버퍼 : 데이터 일시 저장을 위한 메모리공간. 데이터의 일시적 저장, 전송속도 향상을 위해사용
메모리풀 : 동적 메모리 할당 성능 올리기위한 메모리 관리 기술(할당/해제 오버헤드 줄이기)
동적 메모리 할당 성능 최적화, 메모리 내부 단편화 줄이기위해 사용
메모리 요청해서 할당 후 바로 반환하면, 해당메모리는 프로세스 종료 전까지 해제할 수 없다.
-> 메모리 누수
특히 가비지컬렉션을 제공하지 않는 언어에서 주로 발생.
메모리 할당만하고 해제를 따로 안해주면 힙영역이 계속 늘어나다가 결국 프로세스 강제종료됨.
(= out of memory killer)
이러한 문제들을 분석할 수 있는 tcmalloc같은 할당자 존재.
Solid State Drive. ssd는 읽기 속도가 매우 빠르다.
그런데 왜 쓸수없을까?
빠르긴 하지만 실제 ddr 메모리보다는 몇십배 느린 수준이기 때문.(단순 속도만 봤을때)
여러가지 관점에서 차이가 있다.
메모리는 바이트 단위로 주소 지정 되지만, 디스크는 조각 단위로 주소가 지정됨.
-> ssd는 cpu가 저장된 데이터에 직접 접근할 수 없기 때문에 직접 프로그램 실행 불가
최신 운영체제는 가상메모리 기반인데 32비트시스템 최대 주소지정범위 = 4gb
-> 1tb를 가진 ssd여도 4gb이상 메모리를 못씀.(64비트에선 괜찮다)
ssd는 수명제한이 있음. cpu가 프로그램 실행 시 대량의 메모리 읽기/쓰기 작업을 하므로 ssd를 메모리로 쓰면 수명때문에 병목현상 생길 수 있다.
왜 ssd는 수명문제가 있고 ram은 괜찮지?
-> ssd의 플래시 메모리 셀은 전자를 저장하고 방출하는 식으로 데이터저장하는데, 이 방식에서 전자를 저장하는 절연체의 물리적 마모가 발생함. 이 때문에 셀의 전기적 특성이 변하고 셀의 수명이 줄어듬
반면에 ram 칩은 트랜지스터, 커패시터로 구성되어있어서 전기적으로 데이터 저장,접근 하기 때문에 물리적 마모가 없어서 수명이 길다.