[Linux_System_programming] 09 메모리 관리

Linux System Programming (Robert Love) chapter 09 Memry Management

프로세스 주소 공간

리눅스는 메모리를 가상화(물리주소에 직접 접근 x)

페이지와 페이징

  • 페이지 : MMU에서 관리할 수 있는 최소 단위(가상 주소 공간은 페이지로 구성, 32bit 시스템 4K, 64bit 시스템 8K)
  • 프로세스는 모든 물리 페이이제 접근하지 못하며 어떤 페이지는 유효하지 않을 수 있다.
    • 유효한 페이지 : RAM / swap파티션 / 디스크의 실제 페이지와 연관됨
    • 유효하지 않은 페이지 : 접근 시 세그멘테이션 폴트
  • 유효한 페이지가 2차 저장소(swap, 디스크)에 연결 시 접근하려고 하면 페이지 폴트 발생
  • 페이징 아웃 : 새로운 데이터 페이징을 위한 데이터 공간을 만들기 위해 RAM페이지 내용을 2차 저장소로 이동

공유와 copy-on-wirte

  • 가상 메모리의 페이지는 다른 프로세스의 가상 주소 공간에 존재할 지라도 하나의 물리페이지로 맵핑 될 수 있음
  • 상기 방식으로 물리 메모리에 있는 데이터를 다른 가상 주소 공간에서 공유(ex. 공유 C 라이브러리, 데이터 베이스)
  • 프로세스가 쓰기가 가능한 공유페이지에 데이터를 쓰면 2가중 한가지 사건 발생
    • 커널이 쓰기를 허용해 그 페이지를 공유하고 이쓴ㄴ 모든 프로세스가 쓰기 결과가 반영된 페이지 공유(여러 프로세스가 쓰려고 하면 조정/동기화 필요)
    • MMU가 쓰기 요청을 가로채서 예외를 던짐, 커널은 쓰기 요청한 프로세스를 위해 그 페이지의 복사본을 만들고 쓰기 요청 진행(COW)

메모리 영역

  • 커널은 접근 권한과 같은 특정 속성을 공유하는 블록 내부에 페이지를 배열, 이를 맵핑, 메모리영역이라고 함
    • 메모리 영역 종류 : 텍스트 세그먼트(코드), 스택영역(스택), 힙(동적할당), bss(초기화 되지 않은 전역변수)
  • 리눅스는 두 단계로 변수를 최적화
    • 초기화 하지 않은 데이터는 전용 공간인 bss 세그먼트에 할당(링커는 오브젝트 파일에 특수 값을 저장하지 않음)
    • BSS세그먼트가 메모리에 적재되면 커널은 단순히 이 세그먼트를 COW 기법을 통에 0으로 채워진 페이지에 맵핑 하여 기본 값을 설정

동적 메모리 할당하기

  • 메모리 관리 시스템의 기본은 동적 메모리의 할당과 사용, 해제를 어떻게 하느냐
  • 책에 malloc()에 대한 설명 있음
1
2
3
#include <stdlib.h>

void* malloc(size_t size);
  • 책에서 void포인터를 반환하는 함수에 대한 타입 캐스팅에 대한 위험성을 언급(책을 읽어보자)
  • malloc()이 NULL을 반환할 경우 에러메세지를 출력하고 프로그램을 종료하도록 하는 사용자 래퍼함수 xmalloc()에 대해 소개 있음

배열 할당하기

1
2
3
#include <stdlib.h>

void* calloc(size_t nr, size_t size)
  • 동적메모리 할당에서 할당하려는 크기가 유동적일 경우 calloc()사용 가능(ex. 고정 item에 대한 가변 길이 배열 할당)
  • calloc()이 성공 시 크기가 size바이트인 원소 nr개 크기의 메모리 블록에 대한 포인터 반환
  • calloc()은 메모리 영역을 모두 0으로 채움(memset 사용보다 빠른데 커널이 이미 0으로 채워진 메모리를 제공하기 때문)
  • 호출 실패 시 NULL 반환 및 errno를 ENOMEM으로 변경

할당 크기 변경

1
2
3
#include <stdlib.h>

void* realloc(void* ptr, size_t size);
  • realloc() 호출이 성공시 ptr이 가리키는 메모리 영역을 size 바이트 크기로 새로 조정 후 반환(반환 값은 ptr이 아닐 수 있음)
  • 메모리 영역을 키울 때 원래 위치에서 기존 메모리 영역을 확장 할 수 없다면 새로운 메모리를 할당하고 이전 내용을 복사 후 이전 영역을 해제(상대적으로 비용이 많이듬)
  • size가 0이면 free()와 같고 ptr이 NULL이면 malloc()과 같다
  • 호출 실패 시 NULL 반환 및 errno를 ENOMEM으로 변경

동적 메모리 해제

  • 스택을 거슬러 올라오면서 자동으로 거둬들이는 자동할당과 달리 동적 할당은 수동으로 해제될 때 까지 프로세스 주소공간에 존재
1
2
3
#include <stdlib.h>

void free(void *ptr);
  • free()를 호출하면 ptr이 가리키는 메모리 해제(ptr은 반드시 malloc(), calloc(), realloc()에서 반환된 값일 것)
  • 할당된 메모리 블록의 일부만 해제는 불가능
  • 메모리 누수나 댕글링 포인터 접근 주의

정렬

  • 데이터 정렬은 메모리에 데이터를 나열하는 방식(프로세서, 메모리 서브시스템 및 구성요소들은 특정 바이트로 정렬되길 요구)
  • 정렬에 대한 규측은 하드웨어에서 유래하므로 시스템마다 다름(엄격한것, 널널한것)

정렬된 메모리 할당하기

  • POSIX는 malloc(), calloc(), realloc()에서 반환된 메모리가 어떤 C타입을 사용하든 적절하게 정렬되어야 함
  • 리눅스는 32비트 시스템에서 8byte, 64bit 시스템은 16byte
  • 직접 블록 입출력이나 소프트웨어 하드웨어 간 통신을 위한 버퍼를 적절히 정렬해가 위해 페이지 같은 데 큰단위 정렬된 동적메모리 요구
1
2
3
4
5
6
7
/* 둘 중 아무거나 하나만 정의해도 상관없다 */
#define _XOPEN_SOURCE 600
#define _GNU_SOURCE

#include <stdlib.h>

int posix_memalign (void **memptr, size_t alignment, size_t size);
  • posix_memalign() 호출이 성공하면 동적 메모리를 size만큼 할당하고 alignment의 배수인 메모리 주세오 맞춰 정렬
  • alignment는 2의 거듭제곱이며 void포인터 크기의 배수 일 것, 할당된 메모리 주소는 memptr에 저장
  • 성공 시 0반환, 실패시 EINVAL/ENOMEM 중 반환(errno사용 안함)
  • 할당된 메모리는 free()를 이용해 해제해야 함
  • posix_memalign()정의 전에는 BSD에는 valloc(), memalign()함수를 이용

다른 정렬 고려 사항

  • 비표준 데이터 타입과 복잡한 데이터 타입의 정렬의 네가지 유용한 규칙
    1. 구조체의 정렬 요구사항은 가장 큰 멤베의 타입을 따름
    2. 구조체는 각 멤베가 그 타입의 요구사항에 맞게 적절히 정렬 될 수 있도록 패딩 필요(gcc 옵션 -Wpadded 사용 시 컴파일러가 패딩을 채워 넣을 때마다 경고를 띄움)
    3. 유니언의 정렬 요구사항은 유니언에 속한 가장 큰 타입을 따른다
    4. 배열의 정렬 요구사항은 기본 타입을 따른다(배열에는 단일 타입을 넘어서는 요구사항이 없다)
  • C++ 엄격한 앨리어싱 : 객체가 실제 그 객체의 타입, 타입 한정자, 부호, 구조체, 유니언 맴버 또는 char포인터를 통해서만 접근해야함

데이터 세그먼트 관리하기

  • 유닉스 시스템은 전통적으로 데이터 세그먼트를 직접 관리할 수 있는 인터페이스를 제공(malloc() 함수가 사용하기 쉬워서 잘 안씀)
  • 자신만의 힙 기반 할당 메커니즘을 구현하고자 하는 사람들은 아래를 이용
1
2
3
4
#include <unistd.h>

int brk(void* end);
void* sbrk(intptr_t increment);
  • 이름의 유래 : 메모리에서 힙과 스택을 나누는 경계선을 break 또는 break point라고 한다
  • brk()를 호출하면 데이터 세그먼트의 마지막 브레이크 포인터를 end로 지정한 주소로 설정
  • sbrk()를 호출하면 데이터 세그먼트를 increment만큼 늘린다(음수, 양수 가능)

익명 메모리 메핑

  • glib은 버디 메모리 할당 기법 이둉(메모리 할당은 데이터 세그먼트와 메모리 매핑 이용)
    • malloc()을 구현하는 전통적인 방법은 데이터 세그먼트를 2의 배수만큼의 구역으로 나눈 다음 요청하는 크기에 가장 가까운 크기의 구역 반환
    • 인접 구역이 비어 있을 경우 하나로 합쳐 더 큰 구역을 확보하거나 힙의 윗 부분이 완전히 비어있다면 brk()를 이용해 브레이크 포인트를 낮춰 힙을 줄임
  • 버디 메모리할당 기법은 속도와 명료함이 장점이지만 2종류의 메모리 파편화가 발생
    • 내부 파편화 : 메모리 할당 요청을 충족시키기 위해 요청보다 더 많은 메모리를 반환 할 때
    • 외부 파편화 : 요청을 만족 시킬 메모리가 남아 있지만 인접하지 않은 두 구역으로 떨어져 있을 때
  • 상기 이유로 대규모 메모리 할당에는 glib은 힙을 사용하지 않고 익명 메모리 매핑사용(파일 기반 매핑과 유사하나 파일과 연관되지는 않음)
  • 익명 메모리 맵핑을 단일 할당만을 위한 새로운 힙(힙 영역 밖에 위치)
    • 장점 : 파편화 X, 크기조정/권한설정/힌트 사용 가능, 힙관리 필요 없음
    • 단점 : 크기는 페이지 크기의 정수배로 메모리 낭비 될 수 있음, 매핑을 생성하는 부하가 힙보다 크다

익명 메모리 맵핑 생성하기

  • 익명 메모리 메핑을 사용하거나 독자적인 메모리 할당 시스템을 작성 시 수동으로 익명 메모리 매핑 생성
  • mmap()으로 메모리 매핑 생성, munmap()으로 매핑 해제
  • MAP_ANONYMOUS 플래그로 익명으로 매핑 생성, MAP_PRIVATE 플래그로 공유되지 않도록
  • 커널이 COW로 애플리케이션의 익명 페이지를 0으로 채운 페이지로 매핑하므로 0을 채울 필요가 없다.

/dev/zero 매핑하기

  • BSD 간은 다른 유닉스 시스템에는 MAP_ANOYMOUS 플래그가 없어 /dev/zero파일을 매핑하는 방식 사용

고급 메모리 할당

1
2
3
#include <malloc.h>

int mallopt(int param, int value);
  • mallopt()를 다른 메모리 할당 인터페이스를 사용 전 호출하여 할당 연산을 인자로 제약
  • mallopt()를 호출하면 param으로 지정된 메모리 관리와 관련된 인자를 value로 설정
  • 호출이 성공하면 0이 아닌 값을 반환, 실패시 0을 반환(errno는 설정하지 않음)
  • param의 목록은 다음과 같다
    • M_CHECK_ACTION : MALLOC_CHECK_ 환경변수 값
    • M_MMAP_MAX : 최대 매핑 계수 설정
    • M_MMAP_THRESHOLD : 힙 대신 익명 맵핑으로 처리할 할당요청의 임계 크기
    • M_MXFAST : 패스트 빈의 최대 크기(힙 내 특수 메모리 영역)
    • M_PERTURB : 메모리 포이즈닝 활성화(미리정의한 값으로 채워서 관리 에러 탐지)
    • M_TOP_PAD : 데이터 세그먼트 크기 조정 시 사용되는 패딩의 크기
    • M_TRIM_THRESHOLD : glib이 sbrk()를 호출해서 메모리를 커널에 반환하기 전 데이터 세그먼트의 빈 메모리의 최소 크기

malloc_usable_size()와 malloc_tirm()으로 튜닝하기

  • malloc_usable_size()는 메모리 영역의 실제 할당 크기 반환(요청보다 할당 영역이 클 수 있음)
  • malloc_tirm()은 유지되어야 하는 패딩 바이트를 제외한 가능한 많은 데이터 세그먼트를 줄이고 1반환
  • 두 함수를 쓸일은 적다(이식성도 떨어짐)

메모리 할당 디버깅

  • 프로그램은 MALLOC_CHECK_ 환경 변수를 설정해서 메모리 서브시스템의 고급 디버깅 기능을 활성 가능
  • 환경 변수로 디버깅을 제어하므로 재 컴파일은 필요 없이 실행만 하면 됨
  • 환경 변수를 1로 설정 시 유익한 정보를 담은 메세지가 stderr로 출력, 2로 설정하면 프로그램은 즉시 abort()를 호출해서 종료
1
$ MALLOC_CHECK_=1 ./rudder

통계 수집하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <malloc.h>

struct mallinfo{
  int arena;    /* malloc이 사용하는 데이터 세그먼트 크기 */
  int ordblks;  /* 비어 있는 메모리 블록 수 */
  int smblks;   /* 패스트 빈 수 */
  int hblks;    /* 익명 매핑 수 */
  int hblkhd;   /* 익명 매핑 크기 */
  int usmblks;  /* 전체 할당 최대 크기 */
  int fssmblks; /* 사용 가능한 패스트 빈의 크기 */
  int uordblks; /* 전체 할당 공간의 크기 */
  int fordblks; /* 사용 가능한 메모리 블록의 크기 */
  int keepcost; /* 정리가 가능한 공간 크기 */
}

struct mallinfo mallinfo (void);
  • mallinfo()를 호출하면 mallinfo 구조체에 통계를 담하 변환(포인터가 아니라 값으로 반환)
  • 메모리 관련 통계를 stderr로 출력하는 malloc_stats() 함수도 제공

스택기반 할당

1
2
3
#include <alloca.h>

void* alloca(size_t size);
  • alloca()은 호출이 성공시 size 바이트 만큼 할당한 메모리에 대한 포인터 반환
  • 상기 메모리는 스택에 위치하며 실행중인 함수가 반환 될때 자동으로 해제
  • 일부 구현에서는 호출이 실패할 경우 NULL반환, 대부분 실패하지 않거나 실패를 알리지 못함(실패는 스택 오버플로를 일으킴)
  • alloca()는 버그가 많고 일관성 없는 구현으로 나쁜 평판을 얻어서 호환성을 위해서는 사용하지 않는 것이 좋으나 malloc() 보다 월등히 성능이 좋다.
  • 작은 메모리 할당이 필요하고 성능향상이 필요할 경수 사용

스택에서 문자열 복사하기

  • 문자열을 임시로 복사하는 경우 alloca()을 매우 자주 사용(책에 예제 있음)
  • 리눅스 시스템은 스택에 문자열을 복사하는 stddup()함수군을 제공, 그러나 POSIX는 상기 함수를 정의하지 않으므로 이식성이 필요한 경우 주의

가변 길이 배열

  • C99는 컴파일 시점이 아니라 실행중에 배열의 크기를 결정하는 가변 길이 배열(VLA)를 도임(GNU C는 이미 지원하고 있었음)
  • 가변 길이 배열은 alloca()와 마찬가지로 동적메모리 할당이라는 부하를 없애 줌
  • alloca()로 얻은 메모리는 함수 주기동안 유지, 가변 길이 배열로 얻은 메모리는 변수가 스코프를 벗어 날때 까지만 유지
1
2
3
4
for(i = 0 ; i < n; ++i) {
  char foo[i+1];
  /* foo 사용 */
}

메모리 할당 메커니즘 선택하기

메모리 조작하기

  • C언어는 메모리 바이트 조작 함수 제공, 사용자가 제공한 버퍼 크기에 의존하며 에러 반환을 안함(잘못된 영역을 넘기면 세그멘테에션 폴트 발생)

바이트 설정하기

1
2
3
#include <string.h>

void* memset(void* s, int c, size_t n);
  • memset()을 호출하면 s에서 시작해서 n 바이트만큼 c로 채운 다음 s 반환
  • 0으로 채울 때 가장 많이 쓰이며 BSD에서 bzero()도 제공하였으나 이제는 안씀

바이트 비교하기

1
2
3
#include <string.h>

void* memcmp(const void* s1, const void* s2, size_t n);
  • memcmp()는 s1과 s2의 처음 n 파이트를 비교하고 두영역이 같으면 0, s1>s2 이면 음수, s2>s1이면 양수
  • BSD에서도 bcmp()함수를 제공하나 현재 사용안함
  • 구조체는 패딩 때문에 memcmp로 비교하는 것은 신뢰할 수 없음(각 멤버끼리 비교)

바이트 옮기기

1
2
3
4
5
6
#include <string.h>

void* memmove(void *dst, const void* src, size_t n);
void* memcpy(void* dst, const void* src, size_t n);
void* memccpy(void* dst, const void* src, int c, size_t n);
void* mempcpy(void* dst, const void* src, size_t n);
  • memmove()는 src의 처음 n 바이트를 dst로 복사하고 dst를 반환(BSD에서는 bcopy() 함수를 제공하나 사용하지 않음)
  • memmove()함수는 중첩되는 메모리 영역(dst의 일부가 src안에 존재)을 안전하게 다룸(예를 들면 메모리 바이트를 특정 영역안에서 위나 아래로 이동 가능)
  • 중첩된 메모리 영역을 지원하지 않는 memcpy() 제공(잠재적으로 좀 더 빠르게 동작)
  • memccpy()함수는 src 첫 n 바이트 내에서 c 바이트를 발견하면 복사를 멈춤, dst에서 c의 다음 바이트를 가리키는 포인터 반환, 찾지 못하면 NULL 반환
  • mempcpy()는 마지막 바이트를 복사한 후 다음 바이트를 가리키는 포인터를 반환

바이트 탐색하기

1
2
3
4
5
6
7
8
#include <string.h>

void* memchr(const void* s, int c, size_t n);

#define _GNU_SOURCE

void* memrchr(const void* s, int c, size_t n);
void* memmem(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen);
  • memchr()는 s가 가리키는 메모리의 n바이트 범위에서 unsigned char 문자 c를 탐색, c와 일치하는 첫 바이트를 가리키는 포인터를 반환, 찾지 못한 경우 NULL 반환
  • memrchr()는 시작지점부터 찾는게 아닌 s가 가리키는 메모리의 뒤에서 부터 n바이트 범위 탐색(GNU확장, C언어의 일부 아님)
  • memmem()는 길이가 haystacklen 바이트인 메모리 블록 haystack 내부에서 길이가 needlelen 바이트인 서브 블록 needle의 첫 번째 위치를 가리키는 포인터를 반환, 찾지못하면 NULL 반환

바이트 섞기

1
2
3
4
#define _GNU_SOURCE
#include <string.h>

void* memfrob(void *s, size_t n);
  • memfrob()는 s에서 시작하는 메모리의 처음 n바이트를 숫자 42와 XOR 연산 후 s를 반환
  • 반환된 메모리를 memfrob()에 다시 넘기면 아무것도 안한 것과 같으므로 암호화에 사용해서는 안됨(알아보기 어렵게 하는 용도)

메모리 락 걸기

  • 리눅스는 필요할 때 디스크에서 페이지를 읽어오거나 필요없을 때 디스크로 되돌리는 요청식 페이지 구현(메모리 가상화)
  • 보통은 유저가 커널의 페이징에 대해 신경쓰지 않아도 되나 어플리케이션이 페이징 동작에 영향을 미치기를 원하는 동작 2가지
    • 결정성 : 페이지 폴트는 디스크 입출력 연산을 발생시켜 결정성 보장 못하게 함, 필요 페이지가 항상 물리메모리에 머물게해 일관성/결정성 보장
    • 보안 : 중요 내용을 메모리에 저장했는데 이런 내용이 암호화 되지 않은 디스크로 페이징 될 수 있음(메모리의 암호화 되지 않은 개인 키가 스왑파일로 저장)

일부 주소 공간 락 걸기

1
2
3
#include <sys/mman.h>

int mlock(const void *addr, size_t len);
  • mlock()은 호출이 성공 시 메모리에서 [addr, addr+len) 영역을 포함하는 모든 물리 페이지를 잠금
  • 호출이 성공하면 0을 반환, 실패 시 -1 반환 및 errno를 설정(EINVAL, ENOMEM, EPERM)
  • POSIX 표준은 addr이 반드시 페이지 크기로 정렬되어야 한다고 정의, 리눅스는 강제하지 않으나 필요하다면 addr을 가장 근접한 페이지로 자름

전체 주소 공간 락 걸기

1
2
3
#include <sys/mman.h>

int mlockall (int flags);
  • mlockall()을 호출하면 현재 프로세스 주소 공간에 있는 모든 페이지를 물리 메모리에 잠금
  • flag는 다음 값을 OR 연산(대부분 두값을 OR연산 해서 대입)
    • MCL_CURRENT : 현재 맵핑된 페이지(스택, 데이터 세그먼트, 맵핑된 파일 등)를 프로세스 주소 공간에 잠금
    • MCL_FUTURE : 향후 주소공간에 맵핑되는 모든 페이지 역시 메모리에 잠금
  • 호출이 성공하면 0을 반환, 실패하면 -1 반환 및 errno를 설정(EINVAL, ENOMEM, EPERM)

메모리 락 해제하기

1
2
3
4
#include <sys/mman.h>

int mulock(const void* addr, size_t len);
int mulockall(void);
  • mlock()은 addr에서 시작해서 len 바이트만큼 확장된 페이지의 락을 해제(mlock에 대응)
  • 두 함수 모두 호출이 성공하면 0을 반환, 실패 시 -1 반환 및 errno를 설정(EINVAL, ENOMEM, EPERM)

락 제약

  • 리눅스는 프로세스에서 얼마나 많은 페이지를 락 걸수 있는지 제한을 둠(메모리 락은 시스템 전반적인 성능에 영향)
  • CAP_IPC_LOCK 기능을 가진 프로세스는 페이지 수에 제약 없이 락 걸 수 있음
  • 상기 기능이 없는 프로세스는 RLIMIT_MEMLOCK 바이트만 락 걸 수 있음(기본은 32KB, 6장 내용 참조)

페이지가 물리 메모리에 존재하는지 확인하기

1
2
3
4
#include <uinstd.h>
#include <sys/mman.h>

int mincore (void* start, size_t length, unsigned char *vec);
  • mincore()은 시스템 콜 호출하는 시점에 물리 메모리에 맵핑된 페이지를 기술하는 벡터 제공(디버깅과 진단목적 사용)
  • 호출이 성공하면 0 반환, 실패 시 -1 반환 및 errno 설정(EAGAIN, EFAULT, EINVAL, ENOMEM)
  • 이 시스템 콜은 MAP_SHARED로 생성한 파일 기반 매핑에 대해서만 제대로 동작(사용시 큰 걸림돌)

게으른 할당

  • 리눅스는 게으른 할당 전략 사용
  • 프로세스가 커널에 추가 메모리 요청(데이터 세그먼트 늘림, 메모리 매핑) 시 메모리 할당 약속(이 때는 저장장치 제공이 아닌 약속만)
  • 프로세스가 새로 할당된 메모리에 쓸 때 커널은 사용자에게 제공한 메모리를 물리 메모리 할당으로 변환(페이지 단위로 수행)
  • 다음 3가지 장점
    1. 메모리 할당을 지연 시켜 실제 할당이 필요한 최후의 순간까지 대부분의 작업을 미룰 수 있음
    2. 요구에 따라 페이지 단위로 요청을 처리하므로 실제 사용하는 물리 메모리만 물리 저장소 소비
    3. 할당을 약속한 메모리의 총량이 물리 메모리/스왑 공간 까지 넘어 설 수 있음(이를 오버 커밋이라 함)

오버커밋과 OOM

  • 오버커밋은 더 크고 많은 어플리케이션을 실행할 수 있도록 함
  • 오버커밋으로 인해 요청한 메모리를 충족 시킬 수 있는 메모리가 부족한 경우 OOM(out of memory) 발생
  • OOM이 발생하면 커널은 OOM 킬러를 실행 시켜 가장 덜 중요한 프로세스를 종료 시킴
  • OOM 조건은 드물게 발생하므로 초기에 오버커밋을 허용하면 활용도가 극대화 되며 OOM에 의한 프로세스 종료는 용납되지 않는 경우가 있음
  • /proc/sys/vm/overcommit_memry 파일 또는 sysctl 파라미터 vm.overcommit_memory를 통해 오버커밋 비활성화 가능
    • 0(defualt) : 합당한 오버커밋 허용
    • 1 : 모든 오버커밋 허용
    • 2 : 오버커밋 비활성화(책에 상세 내용 있음)
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy