2장. top을 통해 살펴보는 프로세스 정보들
Last updated
Last updated
VIRT
Task, 즉 프로세스에 할당된 가상 메모리 전체의 크기
RES
VIRT 중 실제 메모리에 올려서 사용하고 있는 물리 메모리의 크기
SHR
다른 프로세스와 공유하고 있는 메모리의 크기
VIRT 는 실제로 할당되지 않은 "가상의 공간" 이기 때문에 해당 값이 크다고 해서 문제가 되진 않는다.
반면, 실제 사용하고 있는 메모리는 RES 영역이기 때문에 메모리 점유율이 높은 프로세스를 찾기 위해서는 이 RES 영역이 높은 프로세스를 찾아야한다.
"SHR의 구체적인 예는 어떤 것이 있을까?"
대표적으로 라이브러리가 SHR 영역에 포함될 수 있다.
대부분의 리눅스 프로세스들은 glibc
라이브러리를 참조하기 때문에 사용하는 프로세스마다 glibc
의 내용을 메모리에 올려서 사용하는 것은 공간 낭비일 것이다.
커널은 이럴 경우를 대비하여 공유 메모리 라는 개념을 도입 했고, 다수 프로세스가 함께 사용하는 라이브러리는 공유 메모리 영역에 올려서 함께 사용하도록 구현되었다.
"왜 메모리는 VIRT 와 RES 로 구분되어 있을까?"
기억해야 할 것은, 이 가상의 메모리 주소를 할당 받았다는 것일 뿐 물리 메모리에 해당 영역이 할당된 상태가 아니라는 것이다.
Page fault: 프로세스가 할당받은 메모리 영역에 실제로 쓰기 작업을 하는 것
Page Table: 커널이 실제 물리 프로세스의 가상 메모리 공간을 매핑하는 것
실제 메모리 영역에 쓰기 작업을 하면 커널은 물리 메모리에 프로세스의 가상 메모리 공간을 매핑하는데, 이렇게 물리 메모리에 바인딩된 영역이 RES 로 계산된다.
실제 팀 회식을 하기 위해 우리 팀원 전체 인원수를 예약했다. (10명)
그 중 몇 몇의 팀원이 사정이 생겨 일부만 참석했다. (7명)
VIRT: 10, RES: 7 으로 계산할 수 있는것이다.
가상 메모리 공간을 요청하는 시스템 콜 API 인 malloc
을 통해 실제 VIRT 가 증가하는 것을 볼 수 있다.
이후 메모리 영역에 실제로 쓰기 작업을 하는 API 인 memset
을 활성화 하면 VIRT 와 RES 가 동시에 같은 비율로 증가한다.
코드를 통해 이론에 대한 내용을 직접 확인한 결과 메모리 사용과 관련해서 중요한 부분은 VIRT 가 아니라 실제 메모리를 쓰고 있는 RES 임을 알 수 있다.
"그렇다면 VIRT 는malloc
같은 시스템 콜을 사용 하면 늘어나게 되는데, 무한으로 늘어날까?"
더 이상 줄 수 있는 메모리 영역이 없다면 SWAP 을 사용하거나, OOM으로 프로세스를 죽이는 등의 방법으로 메모리를 확보하게 된다.
결론적으로는 무한대로 할당 받을 수 있게도 가능하고, 그렇게 하지 못하게 막는 것도 가능하다.
"그럼 왜 커널은 프로세스의 메모리 요청에 따라 즉시 할당하지 않고 Memory Commit 기술을 이용하여 요청을 지연시킬까?"
fork()
와 같은 새로운 프로세스를 만들기 위한 콜을 처리해야 하기 때문이다.
fork()
시스템 콜을 사용하면 커널은 현재 실행중인 프로세스와 같은 프로세스를 하나 더 만들게 되는데, 대부분은 fork()
후 exec()
시스템 콜을 통해 전혀 다른 프로세스로 변하기 때문에, 이 때 확보해 둔 메모리 영역이 쓸모 없어질 수도 있다.
그래서, COW(Copy-On-Write)
라는 기법을 통해서 복사된 메모리 영역에 실제 쓰기 작업이 발생한 후에야 실질적인 메모리 할당을 시작하는 것이다.
이 작업을 지원하기 위해 필요했던 기술이 바로 Memory Commit 이며, 만약 Memory Commit을 하지 않고 바로 할당한다면 COW
와 같은 기술을 사용할 수 없었을 것이다.
하지만 대부분의 경우, 자식 프로세스는 곧
exec()
를 호출해서 완전히 다른 프로그램으로 변하게 되며, 이 복사된 메모리는 전혀 사용되지 않을 수 있음
🔹 원리
fork()
호출 시, 부모 프로세스의 메모리를 진짜로 복사하지는 않고, 부모와 자식이 동일한 메모리 페이지를 공유하도록 설정
이 메모리는 읽기 전용(Read-only)으로 표시
부모 또는 자식 중 누군가 이 메모리에 쓰기(write) 시도를 하면, 그때 비로소 해당 페이지를 복사해서 독립적인 공간에 할당
즉, 실제로 변경되는 메모리만 추가로 메모리 사용이 발생함.
🔹 예시
부모 프로세스가 3GB 메모리 사용
fork()
호출
자식은 부모의 메모리를 그대로 공유 (추가 메모리 소모 거의 없음)
자식이 메모리 일부를 수정 → 해당 페이지만 복사됨 (예: 4KB)
결과적으로 메모리 사용량은 6GB까지 늘어나지 않음
🔹 결론
fork() 시 메모리
전체 메모리 즉시 복사 (예: 3GB → 6GB)
복사 없음, 페이지 공유
물리 메모리 사용량
부모 + 자식 = 6GB
부모와 자식이 공유 (대부분 3GB 유지)
성능
느림 (전체 복사)
빠름 (실제 복사는 나중에 필요할 때만)
메모리 낭비
많음
적음
실제 시스템에서 Memroy Commit 상태를 확인할 수 있는데 이는 sar
이라는 모니터링 툴로 확인할 수 있으며 %commit
의 숫자가 시스템의 메모리 커밋 비율을 나타낸다.
이 커밋된 메모리의 비율이 높다면 시스템에 부하를 일으키거나 최악의 경우 응답 불가 현상을 일으킬 수 있는데, 그렇기 때문에 리눅스에서는 메모리 커밋에 대한 동작 방식을 vm.overcommit_memory
라는 파라미터로 제어할 수 있다.
위 동작 방식을 살펴보았을 때 Swap 영역은 커밋 메모리를 결정하는 데 중요한 개념이며 시스템의 안정성을 유지하는 데 큰 역할을 한다고 볼 수 있다.
D: 디스크 혹은 네트워크 I/O 를 대기하고 있는 프로세스로 대기하는 동안 Run Queue 에서 빠져나와 Wait Queue 에 들어감
프로세스가 디스크 혹은 네트워크 작업을 하게 되면 디스크 디바이스 혹은 네트워크 디바이스에 요청을 보내게 되는데, 만약 디스크 요청을 받아 어느 블록에 있는 어느 데이터를 읽어달라고 한다면 프로세스의 입장에선 보낸 요청이 도착할 때 까지 아무것도 할 수 없기 때문에 CPU에 대한 사용권을 다른 프로세스에게 넘기고 대기 상태로 접어든다.
R: 실행중인 프로세스, 실제 CPU 자원을 소모중인 프로세스
D 상태의 프로세스가 많으면 특정 요청이 끝나기를 기다리고 있는 프로세스가 많다는 뜻이고, 이 프로세스들은 요청이 끝나면 R 상태로 돌아가야 하기 때문에 시스템의 부하를 계산하는데 포함된다.
S: D 상태와의 차이점은 요청한 "즉시" 리소스를 사용할 수 있는지 여부에 따라 다름
D 상태는 특정 프로세스가 I/O 작업을 요청하고 기다렸던 상태라면, S는 sleep() 시스템 콜 등을 호출해서 타이머를 작동시키거나, 콘솔 입력을 기다리는 프로세스로 Interruptible sleep 상태에 접어든다.
이 상태는 특정 요청에 대한 응답을 기다리지 않기 때문에 언제 어떻게 들어올지 모르는 시그널을 받아서 처리할 수 있는 상태이다.
Z: 부모 프로세스가 죽은 자식 프로세스
모든 프로세스는 fork() 를 통해 만들어지기 때문에 부모-자식 관계가 되고, 보통 부모 프로세스는 자식이 완료될 때 까지 기다리게 된다.
하지만 그렇지 못한 경우 즉, 부모 프로세스가 죽었는데 자식 프로세스가 남아 있거나 자식 프로세스가 죽기 전 비정상적인 동작으로 부모 프로세스가 죽는 경우에 좀비 프로세스가 만들어진다.
좀비 프로세스는 시스템의 리소스를 차지 하지 않기 때문에 문제가 되지 않는다. 스케줄러에 의해 선택되지 않기 때문에 당연히 CPU를 사용하지 않고, 이미 사용이 중지된 프로세스이기 때문에 메모리도 쓰지 않는다.
바로 좀비 프로세스가 점유하고 있는 PID 때문이다.
좀비 프로세스가 사용한 PID가 정리되지 않고 쌓이면 새로운 프로세스에 할당할 PID가 모자르게 되고, 결국 PID를 할당하지 못하는 고갈을 일으킨다.
이 시스템에서 생성되는 프로세스가 가질 수 있는 PID의 최대값은 419430이다. 그래서, 이 시스템에서 생성되는 모든 프로세스는 1 ~ 419430 사이의 임의의 값을 PID로 배정받는다.
top 에서 얻을 수 있는 항목 중 PR과 NI 값에 대해 살펴본다.
CPU 마다 Run Queue 라는 것이 존재하며, Run Queue 에는 우선순위 별로 프로세스가 연결되어 있다.
스케줄러는 유휴 상태에 있던 프로세스가 깨어나거나 특정 프로세스가 스케줄링을 양보하는 등의 경우에 현재 Run Queue 에 있는 프로세스들 중 가장 우선순위가 높은 프로세스를 꺼내서 디스패처에게 넘겨준다.
디스패처는 현재 실행중인 프로세스의 정보를 다른 곳에 저장한 후 넘겨받은 프로세스의 정보를 가지고 다시 연산을 하도록 요청한다.
기본적으로 모든 프로세스들은 20의 우선순위 값을 갖는데 여기에 nice 값을 주면 우선순위 값이 바뀐다.
PR이 16인 auditd 처럼 20에 -4가 적용되어 20 우선순위를 갖는 프로세스 보다 더 자주 실행된다.
top 명령으로 현재 시스템의 CPU, Memory, Swap의 사용량 및 각 프로세스들의 상태와 메모리 점유 상태를 확인할 수 있음
top 명령의 결과 중 VIRT 는 프로세스에게 할당된 가상 메모리 전체의 크기를 가리킴.
RES 는 그 중에서도 실제로 메모리에 올려서 사용하고 있는 물리 메모리의 크기
SHR 은 다른 프로세스와 공유하고 있는 메모리의 크기를 의미
커널은 프로세스가 메모리를 요청할 때 그에 맞는 크기를 할당해 주지만 해당 영역을 물리 메모리에 바로 할당하지 않음. 프로세스가 할당 받은 메모리 영역을 사용할 때가 되어서야 비로소 물리 메모리를 할당하기 시작하며 이를 Memory Commit 이라고 부름
vm.overcommit_memory 는 커널의 Memory Commit 동작 방식을 변경할 수 있는 커널 파라미터
이 값이 0이면 최댓값을 바탕으로 Memory Commit을 진행
1이면 무조건 overcommit
2이면 현재 시스템의 정보와 vm.overcommit_ratio 에 설정된 비율을 바탕으로 제한적 Memory Commit을 진행
top 으로 볼 수 있는 프로세스의 상태
D는 I/O 대기중인 프로세스
R은 실제 실행 중인 프로세스
S는 sleep 상태의 프로세스
Z는 좀비 프로세스
T는 tracing 중인 프로세스
프로세스에는 우선순위라는 것이 있어서 우선순위가 낮을수록 더 빨리 실행되며, 우선순위는 nice 명령을 통해 조절 가능
문제 상황