Chapter 5. 메모리 관리
메모리 통계 정보
free
free 명령어로 메모리에 대한 정보를 알 수 있다. (단위 : KiB)
$ free
total used free shared buff/cache available
Mem: 7729028 1552460 4177240 536648 1999328 5375580
스왑: 2097148 0 2097148
total : 시스템의 전체 메모리 용량
free : 표기 상 이용하지 않는 메모리 (available - buff)
buff/cache : 버퍼 캐시, 페이지 캐시가 이용하는 메모리. free 필드의 메모리가 부족하면 커널이 buff/cache 메모리를 해제하고 할당해준다.
available : 실질적으로 사용할 수 있는 메모리. free + buff/cache + 다른 커널 내의 해제할 수 있는 메모리 를 의미한다.
sar -r
sar -r 명령어를 통해 메모리에 관련된 통계 정보를 얻을 수 있다.
$ sar -r 1 1 # 1초 간격(2번째 파라미터)으로 1번만(3번째 파라미터)
Linux 5.13.13-surface (ubun2-Surface-Pro-7) 2021년 09월 16일 _x86_64_(8 CPU)
00시 44분 33초 kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
00시 44분 34초 4225432 5460160 1397816 18.09 67080 1869944 6410488 65.24 722640 2019192 152
...
free 결과와의 비교
free 명령어 결과의 free 필드 : kbmemfree
buff/cache 필드 : kbbuffers + kbcached
메모리 부족
메모리 사용량이 증가하면 비어 있는 메모리(free 필드)가 줄어들고, 커널은 해제 가능한 메모리 영역(buff/cache + 다른 커널에서 해제 가능한 영역)을 해제한다.
메모리 사용량이 감당할 수 없을 정도로 계속해서 증가하면 시스템은 메모리 부족 상태OOM, Out Of Memory가 된다.
OOM Killer : 메모리 관리 시스템이 적절한 프로세스를 선택하여 강제 종료(kill) 하여 메모리를 확보하는 기능
OOM Killer는 어떤 프로세스가 강제 종료될 지 알 수 없다. 내부적으로는 우선순위나 사용한지 오래되었다거나 하는 알고리즘을 통해 선정할 것이다.
서버 컴퓨터에서 프로세스가 강제 종료되어 문제가 발생할 경우 조치가 더 힘들기 때문에, sysctl의
vm.panic_on_oom
파라미터의 기본 값을 1로 변경하여 메모리 부족 시 시스템 자체를 강제 종료하는 경우도 있다고 한다.
단순 메모리 할당
메모리 할당이 필요한 상황은 주로 프로세스를 생성하거나, 동적 메모리를 할당하는 경우이다. 프로세스를 생성하는 경우는 3장에서와 같이 fork(), execve() 등의 함수를 이용하는 경우이다.
메모리 할당은 단순히 물리 메모리에 할당하는 방식과 가상 메모리를 사용하는 방식이 있는데, 단순 메모리 할당 방식은 문제점이 있다.
동적 메모리 할당 과정
프로세스에서 동적으로 메모리가 필요한 경우, 프로세스는 커널에 메모리 확보용 시스템 콜을 호출한다. 커널은 필요한 사이즈를 메모리 영역에서 확보하고, 해당 영역의 시작 주소값을 반환한다. 실제 메모리 할당 방식은 가상 메모리를 통해 이루어진다. 단순 메모리 할당 방식을 사용하면 다음의 문제점들이 존재한다.
메모리 단편화
다른 프로세스의 메모리에 접근 가능
여러 프로세스 실행 시 문제
메모리 단편화memory fragmentation
메모리의 획득, 해제를 반복할 경우 메모리에 빈 공간이 여러 군데에 쪼개져있는 상태로 지속되고, 이 때문에 메모리가 충분함에도 큰 용량에 대한 메모리 할당을 할 수 없게된다.
물리적으로 떨어진 빈 공간 여러 개를 프로세스가 하나의 영역으로 다루기에는 다음과 같은 문제점이 존재한다.
프로세스가 메모리를 획득할 때마다 메모리의 분할 개수를 확인해야 하므로 불편하다.
하나의 영역처럼 다루더라도 배열같이 연속된 데이터를 다루기가 쉽지 않다.
다른 프로세스의 메모리에 접근 가능
단순 메모리 할당 방식에서는 프로세스가 커널이나 다른 프로세스가 사용하고 있는 주소를 직접 지정할 경우 해당 영역에 접근할 수 있기 때문에 위험하다.
여러 프로세스 실행 시 문제
3장에서 readelf 명령어로 프로그램의 구조를 봤을 때, 코드와 데이터 영역의 파일 상 오프셋 등의 정보가 존재했다.
만약 동일한 프로그램으로 여러 프로세스를 생성하는 경우, 해당 메모리 영역들은 어쩔 수 없이 메모리 상에서 서로 다른 공간에 위치하게 된다. 이는 최대 1개의 프로세스만 프로그램의 정보에 있는 메모리 주소와 일치할 수 있으므로 다른 프로세스들은 실행할 수 없게된다.
만약 단순 메모리 할당 방식을 사용하면, 각 프로그램마다 동작할 주소가 겹치지 않도록 프로그래머가 신경써야 하는데, 현실적으로 불가능하다.
가상 메모리
위의 문제점들을 해결하기 위해 요즘 나오는 CPU에는 메모리 관리 장치MMU, Memory Management Unit가 존재하며, MMU에서 가상 메모리 주소를 실제 메모리 주소로 변환한다.
가상 메모리는 프로세스가 물리 메모리에 직접 접근(위에서 말한 단순 메모리 할당 방식)하지 않고, 가상 주소를 사용하여 간접적으로 접근하도록 하는 방식이다.
프로세스가 인식하는 주소는 가상 주소이고, 이를 CPU에서 변환하여 물리 주소에 접근한다.
readelf
,cat /proc/PID/maps
의 출력 결과에 나온 주소는 모두 가상 주소이다.프로세스에서 물리 주소에 직접 접근하는 방법은 없다.
페이지 테이블
가상 주소에서 물리 주소로의 변환은 커널 내부의 페이지 테이블Page Table을 참조한다. 변환은 페이지 단위로 이루어진다.
페이지 테이블에서 한 페이지에 대한 데이터를 페이지 테이블 엔트리PTE, Page Table Entry라고 한다.
PTE에는 가상 주소와 물리 주소의 대응 정보가 들어있다.
x86_64 아키텍처의 페이지 사이즈는 4KiB이다.
PT에 물리 주소가 매핑되지 않은 가상 주소에 접근하는 경우에는 CPU에 페이지 폴트page fault 인터럽트가 발생한다. 그러니까 PTE가 존재하지 않는 가상 주소에 접근한다는 것은 허용되지 않은 메모리 영역에 접근한다는 것이다.
현재 실행 중인 명령이 중단되고, 커널 내의 페이지 폴트 핸들러page fault handler 인터럽트 핸들러가 동작한다.
커널은 메모리 접근이 잘못되었다는 내용을 페이지 폴트 핸들러에 전달하고, SIGSEGV 시그널을 프로세스에 전달하여 프로세스를 강제 종료시킨다. C언어 프로그래밍할 때 많이 봤던 segmentation fault가 그 예시이다.
segmentation fault 발생 프로그램
잘못된 주소에 접근하는 프로그램을 작성한다.
$ gcc -o output/segv src/segv.c
$ output/segv
Before invalid access
[1] 9866 segmentation fault (core dumped) output/segv
*p = 0;
을 처리하면서 segmentation fault가 발생한 것을 알 수 있다.
파이썬, javascript 등 메모리를 직접 다루지 않는 언어의 경우 직접 작성한 부분에서는 이런 에러가 발생하는 경우가 거의 없다.
메모리 할당 과정
프로세스 생성
프로세스를 생성할 때는 3장에서 본 것처럼, 프로그램의 실행 파일을 통해 여러 정보를 획득하여 다음 과정을 통해 실행된다.
코드 영역 사이즈 + 데이터 영역 사이즈 만큼의 메모리를 물리 메모리에 할당하여 필요한 데이터를 복사한다.
- 실제로는 디맨트 페이징 방식을 사용하여 할당한다.
프로세스를 위한 페이지 테이블을 생성하고, 가상 주소를 물리 주소에 매핑한다.
엔트리 포인트의 주소에서 실행을 시작한다.
추가 메모리 할당
프로세스가 동적 메모리를 요구하면 커널은 새로운 메모리를 할당하여 페이지 테이블에 저장하고, 할당한 물리 메모리 주소에 대응되는 가상 주소를 프로세스에 반환한다.
메모리 맵 출력 프로그램
과정
프로세스의 메모리 맵 정보(/proc/PID/maps)를 출력한다.
메모리를 100MiB 확보한다.
다시 메모리 맵 정보를 출력한다.
$ gcc -o output/mmap src/mmap.c
$ output/mmap
memory map before memory allocation
55cf6a754000-55cf6a755000 r--p 00000000 103:05 1443273 /home/ubun2/linux-structure-practice/chapter05/output/mmap
55cf6a755000-55cf6a756000 r-xp 00001000 103:05 1443273 /home/ubun2/linux-structure-practice/chapter05/output/mmap
55cf6a756000-55cf6a757000 r--p 00002000 103:05 1443273 /home/ubun2/linux-structure-practice/chapter05/output/mmap
55cf6a757000-55cf6a758000 r--p 00002000 103:05 1443273 /home/ubun2/linux-structure-practice/chapter05/output/mmap
55cf6a758000-55cf6a759000 rw-p 00003000 103:05 1443273 /home/ubun2/linux-structure-practice/chapter05/output/mmap
55cf6af4d000-55cf6af6e000 rw-p 00000000 00:00 0 [heap]
7f2b8f0a2000-7f2b8f0c7000 r--p 00000000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f2b8f0c7000-7f2b8f23f000 r-xp 00025000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f2b8f23f000-7f2b8f289000 r--p 0019d000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f2b8f289000-7f2b8f28a000 ---p 001e7000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f2b8f28a000-7f2b8f28d000 r--p 001e7000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f2b8f28d000-7f2b8f290000 rw-p 001ea000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f2b8f290000-7f2b8f296000 rw-p 00000000 00:00 0
7f2b8f2a8000-7f2b8f2a9000 r--p 00000000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2b8f2a9000-7f2b8f2cc000 r-xp 00001000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2b8f2cc000-7f2b8f2d4000 r--p 00024000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2b8f2d5000-7f2b8f2d6000 r--p 0002c000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2b8f2d6000-7f2b8f2d7000 rw-p 0002d000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2b8f2d7000-7f2b8f2d8000 rw-p 00000000 00:00 0
7fff2c0fc000-7fff2c11d000 rw-p 00000000 00:00 0 [stack]
7fff2c164000-7fff2c168000 r--p 00000000 00:00 0 [vvar]
7fff2c168000-7fff2c16a000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
succeeded to allocate memory : address(0x7f2b88ca2000), size(0x6400000) # 아래에서 이 주소를 찾을 수 있다. 위에는 없음
memory map after memory allocation
55cf6a754000-55cf6a755000 r--p 00000000 103:05 1443273 /home/ubun2/linux-structure-practice/chapter05/output/mmap
55cf6a755000-55cf6a756000 r-xp 00001000 103:05 1443273 /home/ubun2/linux-structure-practice/chapter05/output/mmap
55cf6a756000-55cf6a757000 r--p 00002000 103:05 1443273 /home/ubun2/linux-structure-practice/chapter05/output/mmap
55cf6a757000-55cf6a758000 r--p 00002000 103:05 1443273 /home/ubun2/linux-structure-practice/chapter05/output/mmap
55cf6a758000-55cf6a759000 rw-p 00003000 103:05 1443273 /home/ubun2/linux-structure-practice/chapter05/output/mmap
55cf6af4d000-55cf6af6e000 rw-p 00000000 00:00 0 [heap]
7f2b88ca2000-7f2b8f0a2000 rw-p 00000000 00:00 0 # 새로 할당된 부분
7f2b8f0a2000-7f2b8f0c7000 r--p 00000000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f2b8f0c7000-7f2b8f23f000 r-xp 00025000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f2b8f23f000-7f2b8f289000 r--p 0019d000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f2b8f289000-7f2b8f28a000 ---p 001e7000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f2b8f28a000-7f2b8f28d000 r--p 001e7000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f2b8f28d000-7f2b8f290000 rw-p 001ea000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f2b8f290000-7f2b8f296000 rw-p 00000000 00:00 0
7f2b8f2a8000-7f2b8f2a9000 r--p 00000000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2b8f2a9000-7f2b8f2cc000 r-xp 00001000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2b8f2cc000-7f2b8f2d4000 r--p 00024000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2b8f2d5000-7f2b8f2d6000 r--p 0002c000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2b8f2d6000-7f2b8f2d7000 rw-p 0002d000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2b8f2d7000-7f2b8f2d8000 rw-p 00000000 00:00 0
7fff2c0fc000-7fff2c11d000 rw-p 00000000 00:00 0 [stack]
7fff2c164000-7fff2c168000 r--p 00000000 00:00 0 [vvar]
7fff2c168000-7fff2c16a000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
계산을 통해 새로 할당된 메모리 영역의 크기가 100MiB임을 볼 수 있다.
$ python -c "print((0x7f2b8f0a2000 - 0x7f2b88ca2000) / 1024 / 1024, 'MiB')" 100.0 MiB
High level에서의 메모리 할당
libc에는 malloc() 함수로 메모리 확보를 하는데, 리눅스에서는 malloc() 함수 내부에서 mmap() 시스템 콜을 호출하여 메모리 할당을 구현한다.
mmap() 함수는 커널 모드에서 동작한다.
메모리 확보 단위
mmap() : 페이지 단위
malloc() : 바이트 단위
먼저 mmap() 함수에서 커다란 메모리 영역을 확보하여 메모리 풀을 만들고, malloc()이 호출되면 바이트 단위로 할당한다. 메모리 풀에 비어 있는 영역이 없을 경우 다시 mmap() 함수로 새로운 메모리 영역을 확보한다.
위의 과정은 OS가 유저 모드로 동작할 때 OS에서 제공하는 기능(glibc의 malloc())이다.
여담으로, 사용 중인 메모리의 양을 측정하는 프로그램에서는 malloc() 함수 등으로 획득한 바이트 수의 총합을 나타내는데, 실제 리눅스에서는 해당 값보다 더 많은 메모리 풀 영역을 확보하고 있기 때문에 리눅스 자체에서 사용하는 메모리 크기가 더 크다.
또한 파이썬처럼 직접 메모리 관리를 하지 않는 스크립트 언어에서도 내부적으로는 libc의 malloc() 함수를 사용한다. 이는 2장에서처럼 strace 명령어로 추적해볼 수 있다.
가상 메모리를 통한 문제점 해결
단순 메모리 할당 방식에서의 문제점들을 가상 메모리로 해결할 수 있다.
메모리 단편화
단편화되어있는 물리 메모리 주소를 페이지 테이블에서 적절하게 가상 주소로 매핑하면 해결할 수 있다.
다른 프로세스의 메모리에 접근 가능
가상 주소 공간과 페이지 테이블은 프로세스 별로 만들어지기 때문에, 구조적으로 다른 프로세스의 메모리에 접근할 수 없다.
실제로는 커널의 메모리에 대해 모든 프로세스가 가상 주소 공간에 매핑되어 있는데(페이지 테이블에 커널 영역까지 기록되어있음), 커널 영역의 PTE(커널 자체가 사용하는 메모리에 대응하는 페이지)에는 CPU가 커널 모드로 실행할 때만 접근하도록 하는 커널 모드 전용 정보가 추가되어있다. 즉 유저 모드에서 커널 메모리에 접근하는 것은 불가능하다.
여러 프로세스 실행 시 문제
각 프로세스마다 가상 주소 공간이 존재하므로, 구조적으로 다른 프로그램과 물리 주소가 겹칠 일이 없다.
가상 메모리 응용
파일 맵file map
일반적으로 프로세스가 파일에 접근할 때는 파일을 열고 read(), write(), lseek() 등의 시스템 콜을 사용한다.
mmap() 함수를 특정한 방법으로 호출하면, 파일의 내용을 메모리에 읽어서 가상 주소 공간에 매핑할 수 있다. 매핑된 파일은 메모리에 접근하는 것과 같은 방식으로 접근할 수 있고, 접근하여 수정한 영역은 나중에(6장에서 설명) 실제 파일에 기록된다.
파일 맵 실험
확인할 항목
파일이 가상 주소 공간에 매핑되는지
매핑된 영역을 읽으면 파일이 실제로 읽어지는지
매핑된 영역에 쓰기를 하면 실제 파일에 써지는지
사전 작업
hello 문자열을 파일에 저장한다.
$ echo hello > testfile $ cat testfile hello
테스트 프로그램
프로세스의 메모리 맵 정보(/proc/PID/maps)를 출력한다.
testfile을 열어둔다.
파일을 mmap()으로 메모리 공간에 매핑한다.
프로세스의 메모리 맵 정보를 다시 출력한다.
매핑 영역의 데이터를 읽어서 출력한다.
매핑 영역에 쓰기를 시도한다.
파일 확인
프로그램 실행 결과
$ gcc -o output/filemap src/filemap.c $ output/filemap memory map before memory allocation 55aeb0a27000-55aeb0a28000 r--p 00000000 103:05 1448221 /home/ubun2/linux-structure-practice/chapter05/output/filemap 55aeb0a28000-55aeb0a29000 r-xp 00001000 103:05 1448221 /home/ubun2/linux-structure-practice/chapter05/output/filemap 55aeb0a29000-55aeb0a2a000 r--p 00002000 103:05 1448221 /home/ubun2/linux-structure-practice/chapter05/output/filemap 55aeb0a2a000-55aeb0a2b000 r--p 00002000 103:05 1448221 /home/ubun2/linux-structure-practice/chapter05/output/filemap 55aeb0a2b000-55aeb0a2c000 rw-p 00003000 103:05 1448221 /home/ubun2/linux-structure-practice/chapter05/output/filemap 55aeb0a52000-55aeb0a73000 rw-p 00000000 00:00 0 [heap] 7f5e50cac000-7f5e50cd1000 r--p 00000000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f5e50cd1000-7f5e50e49000 r-xp 00025000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f5e50e49000-7f5e50e93000 r--p 0019d000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f5e50e93000-7f5e50e94000 ---p 001e7000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f5e50e94000-7f5e50e97000 r--p 001e7000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f5e50e97000-7f5e50e9a000 rw-p 001ea000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f5e50e9a000-7f5e50ea0000 rw-p 00000000 00:00 0 7f5e50eb2000-7f5e50eb3000 r--p 00000000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f5e50eb3000-7f5e50ed6000 r-xp 00001000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f5e50ed6000-7f5e50ede000 r--p 00024000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f5e50edf000-7f5e50ee0000 r--p 0002c000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f5e50ee0000-7f5e50ee1000 rw-p 0002d000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f5e50ee1000-7f5e50ee2000 rw-p 00000000 00:00 0 7ffc7424d000-7ffc7426e000 rw-p 00000000 00:00 0 [stack] 7ffc7435e000-7ffc74362000 r--p 00000000 00:00 0 [vvar] 7ffc74362000-7ffc74364000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall] succeeded to allocate memory : address(0x7f5e4a8ac000), size(0x6400000) # 이 주소를 아래에서 찾으면 됨 memory map after memory allocation 55aeb0a27000-55aeb0a28000 r--p 00000000 103:05 1448221 /home/ubun2/linux-structure-practice/chapter05/output/filemap 55aeb0a28000-55aeb0a29000 r-xp 00001000 103:05 1448221 /home/ubun2/linux-structure-practice/chapter05/output/filemap 55aeb0a29000-55aeb0a2a000 r--p 00002000 103:05 1448221 /home/ubun2/linux-structure-practice/chapter05/output/filemap 55aeb0a2a000-55aeb0a2b000 r--p 00002000 103:05 1448221 /home/ubun2/linux-structure-practice/chapter05/output/filemap 55aeb0a2b000-55aeb0a2c000 rw-p 00003000 103:05 1448221 /home/ubun2/linux-structure-practice/chapter05/output/filemap 55aeb0a52000-55aeb0a73000 rw-p 00000000 00:00 0 [heap] 7f5e4a8ac000-7f5e50cac000 rw-s 00000000 103:05 1443568 /home/ubun2/linux-structure-practice/chapter05/testfile # 여기가 testfile이 매핑된 영역 7f5e50cac000-7f5e50cd1000 r--p 00000000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f5e50cd1000-7f5e50e49000 r-xp 00025000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f5e50e49000-7f5e50e93000 r--p 0019d000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f5e50e93000-7f5e50e94000 ---p 001e7000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f5e50e94000-7f5e50e97000 r--p 001e7000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f5e50e97000-7f5e50e9a000 rw-p 001ea000 103:05 2099337 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f5e50e9a000-7f5e50ea0000 rw-p 00000000 00:00 0 7f5e50eb2000-7f5e50eb3000 r--p 00000000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f5e50eb3000-7f5e50ed6000 r-xp 00001000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f5e50ed6000-7f5e50ede000 r--p 00024000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f5e50edf000-7f5e50ee0000 r--p 0002c000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f5e50ee0000-7f5e50ee1000 rw-p 0002d000 103:05 2099333 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f5e50ee1000-7f5e50ee2000 rw-p 00000000 00:00 0 7ffc7424d000-7ffc7426e000 rw-p 00000000 00:00 0 [stack] 7ffc7435e000-7ffc74362000 r--p 00000000 00:00 0 [vvar] 7ffc74362000-7ffc74364000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall] file contents before overwrite mapped region: hello # 기존 파일 내용 overwritten mapped region: HELLO # 덮어쓰기한 내용을 출력한 결과 $ cat testfile HELLO # 실제 파일에도 적용됨
- testfile에 write() 시스템 콜이나 fprintf() 등의 함수를 실행하지 않고, 메모리에 매핑된 영역에 memcpy() 함수로 복사하는 것만으로도 파일의 내용을 변경할 수 있다는 것을 알 수 있다.
디맨드 페이징
앞에서 설명한 메모리 확보 방식(커널이 메모리를 미리 확보해놓는 방식)은 메모리의 낭비가 존재한다.
프로그램 중 실행에 사용하지 않는 기능에 대한 코드와 데이터 영역
glibc가 확보한 메모리 맵 중 malloc() 함수로 확보되지 않는 부분
이러한 문제를 해결하기 위해 리눅스에서는 디맨드 페이징demand paging 방식을 사용한다. 가상 주소 공간만 먼저 할당해두고, 물리 메모리는 말 그대로 요구(디맨드)할 떄 매핑하는(페이징) 방법이다.
PTE에서 "프로세스에는 할당되었지만 물리 메모리에는 할당되지 않음" 상태가 추가된다.
- 가상 주소 공간은 존재하지만 물리 주소 공간에 매핑되지 않은 상태를 의미한다.
가상 주소에 처음 접근할 때 CPU에 페이지 폴트가 발생하고, 해당 가상 주소 공간에 물리 주소가 매핑된다.
- 커널의 페이지 폴트 핸들러가 물리 메모리를 매핑한 다음, 페이지 폴트를 지운다. (저 위의 segmentation fault와는 다름)
유저 모드에서 프로세스 실행을 계속한다. 프로세스는 페이지 폴트가 발생한 사실을 알지 못한다.
프로세스에서 mmap() 함수를 통해 동적으로 메모리를 획득한 경우, "가상 메모리를 확보한 상태"라고 볼 수 있다. 이 가상 메모리에 접근할 때 물리 메모리를 확보하고 매핑하는 것을 "물리 메모리를 확보한 상태"라고 표현한다. 이 때 가상 메모리와는 상관없이 물리 메모리 부족이 발생할 수 있다.
디맨드 페이징 실험
확인할 항목
메모리 획득 시 가상 메모리 사용량만 증가한다. (물리 메모리 사용량은 그대로)
획득한 메모리에 접근하면 페이지 폴트가 발생하고, 물리 메모리의 사용량이 증가한다.
테스트 프로그램
이 프로그램이 실행되는 동안 sar -r 명령어를 통해 메모리 사용량을 분석한다. 이를 위해 출력 메시지에 현재 시간을 표시한다.
메모리 획득 전에 메시지를 출력한다. 그 후 사용자의 입력을 기다린다.
100MiB 메모리 획득
다시 한번 메시지를 출력하고 사용자의 입력을 기다린다.
획득한 메모리를 처음부터 끝까지 1페이지씩 접근하고, 10MiB씩 접근할 때마다 메시지 출력
사용자의 입력을 기다린다.
테스트 결과 (터미널 3개로 확인)
$ output/demand-paging Thu Sep 16 22:31:49 2021: before allocation, press Enter key Thu Sep 16 22:31:51 2021: allocated 100MiB, press Enter key # 가상 메모리 할당 Thu Sep 16 22:31:52 2021: touched 10MiB Thu Sep 16 22:31:53 2021: touched 20MiB Thu Sep 16 22:31:54 2021: touched 30MiB Thu Sep 16 22:31:55 2021: touched 40MiB Thu Sep 16 22:31:56 2021: touched 50MiB Thu Sep 16 22:31:57 2021: touched 60MiB Thu Sep 16 22:31:58 2021: touched 70MiB Thu Sep 16 22:31:59 2021: touched 80MiB Thu Sep 16 22:32:00 2021: touched 90MiB Thu Sep 16 22:32:01 2021: touched 100MiB, press Enter key
$ sar -r 1 Linux 5.13.13-surface (ubun2-Surface-Pro-7) 2021년 09월 16일 _x86_64_(8 CPU) 22시 31분 48초 kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty 22시 31분 49초 2906364 4545340 1993144 25.79 83824 2546712 9809544 99.83 823128 2930380 8 22시 31분 50초 2901536 4540512 1993376 25.79 83824 2551308 9814376 99.88 823136 2930532 12 22시 31분 51초 2935556 4574532 1992912 25.78 83824 2517752 9883228 100.58 823136 2930640 24 22시 31분 52초 2941856 4580832 1993412 25.79 83824 2510952 9876428 100.51 823136 2930748 84 22시 31분 53초 2931532 4570508 2003740 25.92 83824 2510948 9876512 100.51 823136 2941008 84 # 여기부터 물리 메모리 사용량인 kbmemused 필드가 10MiB씩 증가한다. 22시 31분 54초 2921436 4560412 2013288 26.05 83832 2511488 9877092 100.52 823136 2951348 144 22시 31분 55초 2911364 4550340 2024088 26.19 83832 2510760 9876244 100.51 823136 2961720 144 22시 31분 56초 2901032 4540008 2034388 26.32 83832 2510792 9876280 100.51 823136 2972032 148 22시 31분 57초 2890700 4529676 2044720 26.46 83832 2510792 9876256 100.51 823136 2982276 148 22시 31분 58초 2889700 4528684 2045740 26.47 83832 2510776 9835556 100.10 823200 2983708 268 22시 31분 59초 2879116 4518100 2056356 26.61 83832 2510744 9835524 100.10 823200 2993972 268 22시 32분 00초 2869036 4508020 2066436 26.74 83832 2510744 9835540 100.10 823200 3004256 8 22시 32분 01초 2858452 4497440 2076472 26.87 83840 2511284 9836044 100.10 823200 3014612 64 22시 32분 02초 2848372 4487360 2086568 27.00 83840 2511268 9836028 100.10 823200 3024932 72 # 여기까지 100MiB 할당 22시 32분 03초 2848372 4487364 2087056 27.00 83848 2510772 9835536 100.10 823208 3025080 96 22시 32분 04초 2945400 4584400 1986660 25.70 83848 2514140 9736176 99.08 823212 2922648 96 # 프로세스가 종료되어 물리 메모리가 반환됨 22시 32분 05초 2945140 4584140 1986936 25.71 83848 2514124 9736160 99.08 823212 2922648 96
$ sar -B 1 Linux 5.13.13-surface (ubun2-Surface-Pro-7) 2021년 09월 16일 _x86_64_(8 CPU) 22시 31분 48초 pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff 22시 31분 49초 0.00 392.00 554.00 0.00 1308.00 0.00 0.00 0.00 0.00 22시 31분 50초 0.00 0.00 203.00 0.00 399.00 0.00 0.00 0.00 0.00 22시 31분 51초 0.00 0.00 37.00 0.00 8816.00 0.00 0.00 0.00 0.00 22시 31분 52초 0.00 0.00 51.00 0.00 1832.00 0.00 0.00 0.00 0.00 22시 31분 53초 0.00 0.00 2600.00 0.00 359.00 0.00 0.00 0.00 0.00 # 여기부터 10초간 fault/s 필드값이 증가한 상태 22시 31분 54초 0.00 56.00 2584.00 0.00 153.00 0.00 0.00 0.00 0.00 22시 31분 55초 0.00 0.00 2588.00 0.00 344.00 0.00 0.00 0.00 0.00 22시 31분 56초 0.00 0.00 2594.00 0.00 127.00 0.00 0.00 0.00 0.00 22시 31분 57초 0.00 0.00 2590.00 0.00 194.00 0.00 0.00 0.00 0.00 22시 31분 58초 0.00 0.00 2702.00 0.00 2432.00 0.00 0.00 0.00 0.00 22시 31분 59초 0.00 0.00 2577.00 0.00 141.00 0.00 0.00 0.00 0.00 22시 32분 00초 0.00 248.00 2584.00 0.00 126.00 0.00 0.00 0.00 0.00 22시 32분 01초 0.00 44.00 2614.00 0.00 141.00 0.00 0.00 0.00 0.00 22시 32분 02초 0.00 0.00 2582.00 0.00 141.00 0.00 0.00 0.00 0.00 # 여기까지 22시 32분 03초 0.00 148.00 67.00 0.00 322.00 0.00 0.00 0.00 0.00 22시 32분 04초 0.00 0.00 5309.00 0.00 29590.00 0.00 0.00 0.00 0.00 22시 32분 05초 0.00 0.00 19.00 0.00 175.00 0.00 0.00 0.00 0.00 22시 32분 06초 0.00 0.00 1283.00 0.00 3497.00 0.00 0.00 0.00 0.00
프로세스 별 메모리 통계 확인
가상 메모리의 양, 확보된 물리 메모리의 양, 프로세스 생성 시부터 페이지 폴트의 횟수 확인
ps -eo 명령어의 vsz, rss, maj_flt, min_flt 필드로 확인할 수 있다.
페이지 폴트의 횟수는 maj_flt(Major Fault) + min_flt(Minor Fault)이다.
확인을 위한 셸 스크립트 실행 (터미널 2개)
$ output/demand-paging Thu Sep 16 22:43:32 2021: before allocation, press Enter key Thu Sep 16 22:43:35 2021: allocated 100MiB, press Enter key Thu Sep 16 22:43:37 2021: touched 10MiB Thu Sep 16 22:43:38 2021: touched 20MiB Thu Sep 16 22:43:39 2021: touched 30MiB Thu Sep 16 22:43:40 2021: touched 40MiB Thu Sep 16 22:43:41 2021: touched 50MiB Thu Sep 16 22:43:42 2021: touched 60MiB Thu Sep 16 22:43:43 2021: touched 70MiB Thu Sep 16 22:43:44 2021: touched 80MiB Thu Sep 16 22:43:45 2021: touched 90MiB Thu Sep 16 22:43:46 2021: touched 100MiB, press Enter key
$ script/vsz-rss.sh 2021. 09. 16. (목) 22:43:34 KST: 10439 demand-paging 2496 648 0 113 2021. 09. 16. (목) 22:43:35 KST: 10439 demand-paging 2496 648 0 113 2021. 09. 16. (목) 22:43:36 KST: 10439 demand-paging 104900 648 0 114 # 가상 메모리(vsz) 100MiB 할당 2021. 09. 16. (목) 22:43:37 KST: 10439 demand-paging 104900 11772 0 2675 # 여기부터 물리 메모리 10MiB씩 할당 2021. 09. 16. (목) 22:43:38 KST: 10439 demand-paging 104900 21864 0 5235 2021. 09. 16. (목) 22:43:39 KST: 10439 demand-paging 104900 32160 0 7795 2021. 09. 16. (목) 22:43:40 KST: 10439 demand-paging 104900 42456 0 10355 2021. 09. 16. (목) 22:43:41 KST: 10439 demand-paging 104900 52752 0 12915 2021. 09. 16. (목) 22:43:42 KST: 10439 demand-paging 104900 63048 0 15475 2021. 09. 16. (목) 22:43:43 KST: 10439 demand-paging 104900 73080 0 18035 2021. 09. 16. (목) 22:43:44 KST: 10439 demand-paging 104900 83376 0 20595 2021. 09. 16. (목) 22:43:45 KST: 10439 demand-paging 104900 93672 0 23155 2021. 09. 16. (목) 22:43:46 KST: 10439 demand-paging 104900 103968 0 25714 # 물리 메모리 100MiB 매핑 완료 2021. 09. 16. (목) 22:43:47 KST: 10439 demand-paging 104900 103968 0 25714 2021. 09. 16. (목) 22:43:48 KST: target process seems to be finished
가상 메모리 부족과 물리 메모리 부족
프로세스가 가상 메모리를 전부 사용한 뒤에도 가상 메모리를 더 요청할 경우, 가상 메모리 부족이 발생한다. 이는 물리 메모리가 남아있어도 발생할 수 있다.
- 32비트 프로세스에서의 가상 메모리 영역은 232Byte(=4GiB)만 사용할 수 있으며, 이 이상으로 메모리 요청을 하면 가상 메모리 부족이 발생한다.
물리 메모리 부족은 말 그대로 시스템의 물리 메모리가 전부 사용되어 더 이상 할당할 메모리가 없을 때 발생한다.
Copy on Write (CoW)
3장의 fork() 시스템 콜도 사실 가상 메모리 방식을 사용하여 고속화된다.
먼저 부모 프로세스의 페이지 테이블을 자식 프로세스에 복사한다. 메모리 영역 전체를 복사하는게 아니므로 속도는 훨씬 빠르고, 페이지 읽기의 경우 같은 영역(공유된 물리 페이지)을 읽게 되는 것이다.
복사할 때 부모 프로세스와 자식 프로세스의 PTE에 있는 쓰기 권한을 둘 다 무효화(불가능) 처리한다. 이후 페이지의 내용을 변경하려고 하면 다음과 같은 흐름이 발생한다.
페이지 쓰기 권한이 없으므로 CPU에 페이지 폴트가 발생한다.
CPU가 커널 모드로 변경되고, 커널의 페이지 폴트 핸들러가 동작한다. 접근한 페이지를 다른 메모리 영역에 복사하고, 해당 프로세스에 할당한 뒤 내용을 변경한다.
변경하려한 프로세스의 PTE에서 가상 주소 영역을 새로 복사한 물리 메모리 영역으로 매핑하고, 부모 프로세스와 자식 프로세스 PTE의 쓰기 권한을 둘 다 허용으로 변경하여 이후 자유로운 쓰기가 가능하도록 한다. (더 이상 해당 물리 메모리를 공유하지 않으므로)
즉, fork() 시스템 콜을 호출했을 때가 아닌 쓰기 작업이 발생할 때 물리 메모리를 복사하기 때문에, 이 방식을 CoWCopy on Write라고 부른다.
- fork() 시스템 콜을 호출할 시점과 상관없이 쓰기 작업이 발생한 시점에 물리 메모리에 여유 공간이 부족하다면 물리 메모리 부족이 발생한다.
CoW 실험
확인할 사항
fork() 시스템 콜을 호출했을 떄부터 쓰기 작업이 발생할 때까지는 부모 프로세스와 자식 프로세스가 물리 메모리 영역을 공유한다.
쓰기 작업이 발생할 때 페이지 폴트가 발생한다.
테스트 프로그램
100MiB 메모리를 확보하여 모든 페이지에 접근한다.
시스템 메모리 사용량 출력
fork() 시스템 콜 호출
자식 프로세스에서 시스템 메모리 사용량 및 자식 프로세스의 메모리 사용량을 출력한다.
1에서 확보한 메모리 영역에 자식 프로세스가 전부 쓰기 작업을 수행한다.
자식 프로세스에서 시스템 메모리 사용량 및 자식 프로세스의 메모리 사용량을 출력한다.
실험 결과
$ output/cow free memory info before fork(): total used free shared buff/cache available Mem: 7729028 2316096 2569044 862244 2843888 4274080 스왑: 2097148 0 2097148 child ps info before memory access: 14264 cow 104900 103320 0 24 free memory info before memory access: total used free shared buff/cache available Mem: 7729028 2317056 2568084 862244 2843888 4273120 # 위의 결과와 별 차이가 없다. (아직 메모리를 복사하지 않았기 때문) 스왑: 2097148 0 2097148 child ps info after memory access: 14264 cow 104900 103320 0 25626 # 페이지 폴트가 많이 증가함 free memory info after memory access: total used free shared buff/cache available Mem: 7729028 2420332 2464808 862244 2843888 4169844 # used 필드가 100MiB정도 늘었다.
ps 명령어로 부모 프로세스와 자식 프로세스의 메모리를 확인할 때, 공유된 부분은 두 프로세스에서 모두 표시된다는 것에 주의해야 한다. 메모리의 합을 계산할 때 그냥 더해버리면 공유된 부분이 중복으로 더해지는 것이다.
스왑swap
물리 메모리가 부족할 경우 메모리 부족OOM 상태가 되는 것을 방지하기 위해, 저장 장치(HDD, SSD 등)의 일부를 메모리 대신 사용하는 방식이다.
물리 메모리가 부족한 경우 기존에 사용하던 물리 메모리의 일부분을 저장 장치에 저장하여 빈 영역을 만들어서 할당해준다. 이 때 저장 장치에 임시로 저장한 영역을 스왑 영역이라고 부른다. (윈도우에서는 가상 메모리라고 부르고, 이 때문에 예전에 가상 메모리에 대한 개념이 헷갈렸다ㅠ)
커널이 사용 중인 물리 메모리의 일부를 스왑 영역에 임시 보관하는 것을 스왑 아웃swap out, 혹은 페이지 아웃page out이라고 한다.
스왑 영역에 임시 보관했던 데이터를 물리 메모리에 되돌리는 것을 스왑 인swap in, 혹은 페이지 인page in이라고 한다.
- 스왑 아웃, 스왑 인을 통틀어서 스와핑swapping, 혹은 페이징paging이라고 한다.
스래싱thrashing
시스템의 메모리가 계속 부족한 상태로 유지되면, 메모리에 접근할 때마다 스왑 아웃, 스왑 인이 반복되는 현상이 발생하는데 이를 스래싱이라고 한다.
저장 장치에 접근하는 속도는 메모리에 접근하는 것보다 매우 느리기 때문에, 스래싱이 발생하면 시스템이 원활하게 작동되지 않는다.
스왑 확인
시스템의 스왑 영역 확인
$ swapon --show NAME TYPE SIZE USED PRIO /swapfile file 2G 0B -2
$ free total used free shared buff/cache available Mem: 7729028 1357144 4296532 594456 2075352 5504204 스왑: 2097148 0 2097148
스와핑이 발생 중인지 확인 (sar -W 옵션)
$ sar -W 1 Linux 5.13.13-surface (ubun2-Surface-Pro-7) 2021년 09월 17일 _x86_64_(8 CPU) 16시 58분 13초 pswpin/s pswpout/s # 초당 스와핑된 페이지의 수 16시 58분 14초 0.00 0.00
스왑 영역의 사용량 추이 (sar -S 옵션)
$ sar -S 1 Linux 5.13.13-surface (ubun2-Surface-Pro-7) 2021년 09월 17일 _x86_64_(8 CPU) 17시 01분 06초 kbswpfree kbswpused %swpused kbswpcad %swpcad 17시 01분 07초 2097148 0 0.00 0 0.00 17시 01분 08초 2097148 0 0.00 0 0.00 17시 01분 09초 2097148 0 0.00 0 0.00 17시 01분 10초 2097148 0 0.00 0 0.00 17시 01분 11초 2097148 0 0.00 0 0.00
kbswpused 필드가 스왑 영역의 사용량을 나타내는데, 이 값이 점점 증가하면 주의해야 한다.
만약 이 명령어를 입력했을 때
Cannot open /var/log/sysstat/sa17: No such file or directory
등의 에러가 발생하면, /etc/default/sysstat 파일을 열어서ENABLED="true"
로 바꾸고service sysstat restart
으로 서비스를 재시작한다.
Page Fault의 종류
Major Fault : 스와핑과 같이 저장 장치에 대한 접근이 발생하는 페이지 폴트
Minor Fault : 저장 장치에 대한 접근이 발생하지 않는 페이지 폴트
계층형 페이지 테이블
페이지 테이블에는 가상 주소 공간 페이지의 전부에 대응되는 데이터가 저장된다.
1차원적으로 페이지 테이블을 구성할 경우
x86_64 아키텍처에서 보통 가상 주소 공간의 크기는 128TiB이다.
1페이지는 4KiB, PTE는 8Byte이다.
프로세스 1개당 페이지 테이블의 크기는 이론상 8Byte * 128TiB / 4KiB = 256GiB이다.
위와 같이 1차원적으로 페이지 테이블을 구성하면 크기가 엄청나게 커지므로, x86_64의 페이지 테이블은 계층형 구조로 되어있어 메모리를 절약한다.
예를 들어 1차원적으로 0부터 15까지(페이지 단위라고 생각하자)의 가상 주소에 대한 PTE 중 앞의 4개만 사용하는 페이지 테이블이라면, 4개씩 묶어서 하위 페이지 테이블의 주소를 가리키도록 한다. 0
3, 47, 811, 1215 이렇게 묶으면 4개의 PTE가 필요하고, 하위 페이지 테이블로 0부터 3까지 4개의 PTE, 즉 전체 16개의 PTE를 8개로 줄일 수 있다.물론 사용하는 가상 메모리의 양이 많아지면 필요한 페이지 테이블도 많아지고, 어느 순간부터는 1차원적인 페이지 테이블보다 용량이 더 커질 수도 있다. 하지만 일반적인 상황에서 그런 상황은 발생하지 않는다.
실제 x86_64 아키텍처에서는 페이지 테이블의 구조가 4단 구조이다.
페이지 테이블 사용량 확인 (sar -r 옵션)
물리 메모리 중에 페이지 테이블의 사용량을 확인해보자.
$ sar -r ALL 1 Linux 5.13.13-surface (ubun2-Surface-Pro-7) 2021년 09월 17일 _x86_64_(8 CPU) 19시 17분 07초 kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty kbanonpg kbslab kbkstack kbpgtbl kbvmused 19시 17분 08초 2971536 4750228 1817952 23.52 86856 2635636 9296076 94.61 903012 2847004 24 1692840 217048 13104 32928 35964
- knpgtbl 필드가 현재 페이지 테이블이 사용 중인 물리 메모리 크기이다.
Huge Page
프로세스의 가상 메모리 사용량이 늘어나면, 페이지 테이블이 사용하는 물리 메모리양도 증가한다.
- 페이지 테이블의 크기가 큰 경우, fork() 시스템 콜 호출 시 속도도 느려진다. 페이지 테이블은 CoW와 관계없이 부모의 프로세스와 같은 크기로 생성하기 때문이다.
이를 해결하기 위해 Huge Page를 사용한다.
가상 주소 뭉탱이를 물리 주소 뭉탱이에 매핑한다고 생각하면 된다. 페이지의 크기를 4KiB에서 2MiB로 크게 잡는 등의 방식을 사용한다.
이를 통해 페이지 테이블에 필요한 메모리의 양이 줄어들며, MMU의 캐시 역할을 하는 TLBtranslation lookaside buffer의 hit을 증가시켜 성능을 향상시킨다.
사용 방법
mmap() 함수의 flags 파라미터에
MAP_HUGETLB
플래그 등을 넣어서 Huge Page를 획득할 수 있다.Huge Page 참고 : https://linux.systemv.pe.kr/tag/hugepage-%EB%9E%80/
Transparent Huge Page
리눅스 환경에서, 가상 주소 공간에 연속된 4KiB의 페이지들이 특정 조건을 만족하면 자동으로 Huge Page로 묶는 기능이다.
문제점 : Huge Page로 묶거나, 다시 4KiB 페이지로 분리할 때 국소적으로 성능이 하락하는 경우가 존재한다.
활성화 여부
$ cat /sys/kernel/mm/transparent_hugepage/enabled always [madvise] never
대괄호 쳐진게 적용된 항목이다.
always : 항상 사용
madvise : madvise() 시스템 콜을 사용하여 특정 메모리 영역에만 적용할 수 있다.
never : 사용 안함
무효화
$ sudo su # echo never > /sys/kernel/mm/transparent_hugepage/enabled
참고
소스 코드
'Linux > Linux Structure' 카테고리의 다른 글
Chapter 7. 파일시스템 (0) | 2021.09.26 |
---|---|
Chapter 6. 메모리 계층 (0) | 2021.09.26 |
Chapter 4. 프로세스 스케줄러 (0) | 2021.09.26 |
Gnuplot (0) | 2021.09.26 |
Chapter 3. 프로세스 관리 (1) | 2021.09.26 |