WIL WEEK7

정범진·2026년 4월 14일

이번주 공부한 내용

동적메모리 사용 이유

자료 구조의 크기를 알 수 없는 경우(프로그램 실행중에 결정되는 경우) 효율적으로 메모리를 관리하기 위해 사용

32B 만큼 할당해도 4096B로 할당되는 이유

OS가 페이지 단위(4096B)로 메모리를 할당하기 때문
왜? -> 가상 메모리 시스템 구조 때문
1. 작은 요청마다 OS 호출하면 성능 폭망 -> 미리 크게 받아놓고 나눠씀
2. 실제 32만큼 할당해도 추가로 사용해야되는 데이터가 있음 -> 청크헤더, 정렬패딩
1. 청크헤더 -> 블록의 크기, 할당되어 사용중인지(allocated), 비어있는지(free)를 나타낸느 상태 정보를 저장하는 영역
2. 정렬패딩 -> 패딩은 할당받은 영역에서 사용되지 않는 여분의 메모리 공간을 뜻함. 컴퓨터는 데이터가 특정 바이트의 배수 주소에 위치하도록 요구함. -> 64비트에서 malloc은 항상 16바이트의 배수 주소를 반환 만약 사용자가 요청한 크기가 이 조건에 맞지 않으면 남은 공간을 패딩으로 체워 정렬 조건에 맞게 올림함. -> 외부 단편화를 줄이기 위해 여분의 패딩을 추가할 수 있음.

실제로는 4096B 아닐수도 있음.

vscode 디스어셈블리에서 보여주는 영역은 코드영역

코드영역은 리눅스 x86_64기준 0x400000에서 시작되지만 보안을 위해 ASLR(주소 공간 배치 무작위화)를 사용해서 랜덤 적용됨

0x0부터 시작되지 않는 이유는 NULL포인터 역참조 문제 때문임. 만약 NULL주소의 값을 변경하려고 하면 0x0을 참조해서 변경하려고 할거임. 근데 0x0부터 시작할 경우 그 주소에 다른 데이터가 있을 수 있음

공유 라이브러리 매핑 이유

C 표준 라이브러리 코드를 실행파일(.exe)안에 전부 넣는다면 디스크 공간 낭비임.
그래서 프로그램이 실행될 때 임의의 메모리 주소에 동적으로 로드되고 연결

메모리 매핑

여러 프로그램이 실행되고 있더라도 운영체제는 공유 라이브러리 코드를 단 한 사본만 로드함.

각 프로세스의 가상 메모리 공간 중간에 공유 라이브러리 매핑 영역이 물리적 메모리의 단일 사본을 가리키도록 연결

각 프로그램은 자신만의 독립적인 가상 주소 공간을 가지면서 내부적으로 하나의 물리적 라이브러리 코드를 안전하게 공유하여 사용

공유 라이브러리는 읽기 전용으로 로드되지만 각 프로세스별로 다른 값을 가져야하는 변수나 상태등은 수정이 필요할 때 해당 프로세스의 가상메모리에서 (Uninitialized data (.bss) Initialized data (.data))영역에 복사되어 수정 가능하게 함
ㄴ 예시 : 라이브러리 함수의 호출 횟수 등을 기록하는 추적용 전역변수(addcnt, multcnt)등, 동적 메모리 할당기의 내부 상태 변수 (heap_listp) 등

void *sbrk(intptr_t increment);

힙 메모리 영역을 늘리거나 줄일 때 사용
brk 포인터(힙의 맨 위쪽 끝을 가리키는 포인터. 스택에서 top과 동일한 역할)에 인자로 전달받은 incr를 더하여 힙의 크기를 변경

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)

새로운 가상 메모리 영역을 생성하고 디스크의 파일이나 익명 객체를 해당 영역에 매핑
ㄴ 익명 객체는 뭐지?
ㄴ 디스크의 파일, 익명 객체를 왜 매핑하는거지?

int munmap(void *addr, size_t length)

mmap함수로 할당된 가상 메모리 영역을 해제함
시작 가상주소(start) ~ 지정한 길이(length)만큼의 영역을 삭제

단편화

내부 단편화

할당된 블록이 사용할 영역보다 큰 경우

외부 단편화

힙 전체에 남아있는 가용 메모리의 총합이 사용할 영역보다 크지만 공간이 쪼개져 있어서 이 조각이 사용할 영역보다 작은 경우 발생 -> 새로운 영역 할당 받아야 함(sbrk) -> 미래에 어떤 데이터가 할당될지 모르기 때문에 예측 불가능.

할당기를 어떻게 구현해야 할까?
  1. 가용 블록 구성: 어떻게 가용 블록을 지속적으로 추적할지
  2. 배치: 새롭게 할당된 블록을 배치하기 위한 가용 블록을 어떻게 선택할지
  3. 분할: 새롭게 할당한 블록을 가용 블록에 배치한 후 가용 블록의 나머지 부분들로 무엇을 할 지(내부 단편화 관련)
  4. 연결: 방금 반환된 블록으로 무엇을 할 것인지(외부 단편화 관련)

묵시적 가용 리스트

힙 시작점에 4바이트 unused패딩 있음 -> 페이로드 시작점이 8의 배수가 되도록 하기 위함(unused + header = 8) (이는 더블 워드 정렬 조건일 때, 만약 쿼드 워드 정렬 조건이면 16배수임)
블록은 (헤더 + 페이로드 + 패딩)으로 구성됨

헤더: 0~3비트에는 할당되었는지 표시하는 플래그 있음 이후 4~31비트는 블록 사이즈가 적혀있음
ㄴ 크기가 8바이트 단위면 앞에 0~3바이트는 바뀌지 않는 값으로 이 구역을 플래그 표시하는 용도로 사용

묵시적 가용 리스트의 장점은 단순성. 단점은 가용 리스트를 탐색해야 하는 연산들의 비용이 힙에 있는 전체 할당된 블록과 가용 블록의 수에 비례 -> 무슨말이지? -> 가용 블럭을 찾을 때 리스트 탐색 연산을 해야된다는 것 같음

경계 태그로 연결하기

헤더를 사용해서 다음블록 상태를 확인하고, 풋터를 사용해서 이전블록 상태를 확인하고 병합
ㄴ 헤더는 현재 블록의 크기만큼 더하면 다음 블록의 헤더를 읽어서 상태를 확인할 수 있음.
ㄴ 푸터는 모든 블록의 맨 끝에 헤더와 동일한 정보를 가진 푸터를 달아둠. 이렇게 하면 현재 블록의 헤더 바로 앞(1워드) 앞에 항상 이전 블록의 푸터가 맞닿아 있음. 이 푸터를 읽으면 이전 블록의 할당 상태와 크기를 바로 알 수 있음.

프롤로그 블록도 다른 블록처럼 헤더와 풋터 둘 다 있음. 힙의맨 앞에서 병합 과정이 일어나지 않도록 방어하는 역할을 함.
프롤로그 블록을 8바이트로 두는 이유는 완벽한 구조적 통일성 보장을 위해서 -> 만약 4바이트 한다면 매번 if문으로 힙의 맨처음 블록인지를 검사해야 함

에필로그 블록이 4바이트인 이유(헤더만 가지고 있는 이유)
헤더만 확인하면 되기 때문 이것도 프롤로그 블록처럼 경계선을 지키는 용도임

명시적 가용 리스트

포인터(pred: 이전 블록 주소, succ: 다음 블록 주소) 참고로 앞 블록, 뒷 블록 아니라 논리적으로 떨어져있는 블록임
포인터의 용도: 메모리 할당할 때 할당된 블록을 건너뛰고 비어있는 블록을 빠르게 탐색하여 할당 속도를 높이는 용도로 사용

비어있는 블록 주소를 알아내는 방법:
  1. 사용자가 free할 때 그 리턴값을 가용 리스트 맨 앞에 삽입함(LIFO).
  2. 가용 블록이 분할될 때
  3. sbrk 함수로 힙 메모리를 추가로 확보할 때
    2, 3도 모두 LIFO 정책을 따름

분리 가용 리스트

여러 개의 가용 블록 리스트를 포인터로 연결
ㄴ 명시적 가용 리스트랑 무슨 차이지?
ㄴ 명시적 가용 리스트는 힙 첫번째 블럭부터 시작해서 탐색중 첫번째 가용 리스트를 만났을 때 해당 리스트는 모든 가용 리스트와 연결되어있는거고 분리 가용 리스트는 방식에 따라 다른데 예를 들어 단순 분리 저장같은 경우 동일한 크기의 블록끼리만 연결

demand-zero memory(지연할당)

할당할 때 바로 0으로 초기화하는게 아니라 접근시 0으로 초기화 -> 실제로 쓰는 메모리만 RAM을 차지

DMA(Direct Memory Access, 직접 메모리 접근)

디스크에서 CPU를 거치지 않고 바로 메모리에 올려주는 장치

추가로 LAN카드, 사운드카드, 오인페, 글카 등에도 있음 -> 그냥 데이터를 빠르게 주고받을 수 있게 하는 장치로 디스크에만 있지 않음. 핵심은 CPU를 거치지 않는다 -> 작업 완료시 딱 한번 인터럽트 발생(보고! 데이터 다 옮겼습니다!)

페이징

페이지 테이블은 DRAM에 저장되어있음
MMU내부에 TLB라는 작은 캐시를 두어 자주 사용되는 페이지 테이블 항목(PTE)을 저장

가상메모리에서 Cached는 실제 메모리에 올라와있는 프로그램 일부분을 말함
Uncached는 가상메모리상에서 할당받았지만, 실제 메모리에 올라와있지 않은 부분을 말함

VP(Vitual Page) 가상 페이지 <=> PP(Physical Frame) 물리 프레임

가상 주소 공간을 페이지 단위로 나눈 하나의 블록

시스템 콜

사용자 프로그램이 커널에게 운영체제 서비스를 요청하는 인터페이스

유저 프로그램은 커널이 보호하는 자원, 특권 작업을 하지 못함. 그래서 컴퓨터 자원을 다뤄야 할 때 시스템 콜을 통해 커널에게 요청을 함. 그러면 커널은 그 요청을 실행함

커널 모드 전환시 일어나는 일들
  1. 유저 공간에서 시스템 콜 번호와 인자를 준비
    ㄴ 시스템 콜 번호, 인자
    ㄴ write(fd, buf, count)에서는 fd, buf, count, write에 해당하는 시스템 콜 번호

  2. CPU에 커널 진입 명령 실행
    ㄴ x86-64: syscall 실행, aarch64: svc #0 시작

  3. CPU가 유저 모드에서 커널 모드로 전환
    ㄴ 권한 상승, 커널 코드가 실행 가능한 상태로 진입

  4. 커널이 커널 스택에서 실행 시작
    ㄴ 유저 스택 사용x -> 유저 스택은 믿을 수 없음 + 커널은 전용 스택이 따로 있음
    ㄴ PC, 스택 포인터, 상태 레지스터 등 저장

  5. 유저가 준 인자와 상태 검사
    ㄴ 인자 값 검사, 유효성, 권한 등

  6. 해당 시스템 콜 핸들러 실행
    ㄴ 시스템 콜 번호를 읽고 그 번호에 맞는 함수로 보냄

  7. 결과값 준비

  8. 다시 유저 모드로 복귀
    ㄴ 이전에 저장해두었던 유저 상태 복원

0개의 댓글