Skip to main content

top을 통해 살펴보는 프로세스 정보들

원티드 프리온보딩 백엔드 인턴십을 진행하면서 top을 통해 살펴보는 프로세스 정보들라는 발표 주제로 만든 PPT 자료와 설명을 작성하였으며 DevOps와 SE를 위한 리눅스 커널 이야기 교재를 활용하였습니다.

devops-se-ch-2-1

devops-se-ch-2-2

devops-se-ch-2-3

devops-se-ch-2-4

  • us (User): 사용자 프로세스에 의해 사용된 CPU 시간의 백분율
  • sy (System): 커널 모드에서 시스템에 의해 사용된 CPU 시간의 백분율
  • ni (Nice): NI 값이 1에서 19사이의 사용자 프로세스에 의해 사용되는 CPU 시간 비율
  • id (Idle): 시스템이 아무것도 하지 않고 대기 중인 시간의 백분율 (CPU 휴식 시간 비율)
  • wa (Wait): 입출략(I/O) 대기 시간으로 인한 CPU 대기 시간의 백분율, 이는 시스템이 디스크 I/O 또는 네트워크 I/O와 같은 것을 기다리는 동안 CPU가 사용되지 않는 시간을 나타냄 (CPU 사용 대기 시간 비율)
  • hi (Hardware Interrupt): 하드웨어 인터럽트에 의해 사용된 CPU 시간의 백분율
  • si (Software Interrupt): 소프트웨어 인터럽트에 의해 사용된 CPU 시간의 백분율
  • st (Steal Time): 하이퍼바이저에 의해 다른 가상 머신에 사용될 수 있도록 "도난당한" 시간의 백분율, 가상화 환경에서 다른 VM이 자신의 공유된 리소스를 사용하게 되면 발생(https://www.whatap.io/ko/blog/25/)

devops-se-ch-2-5

VIRT, RES, SHR

devops-se-ch-2-6

실제 사용하고 있는 메모리는 RES 영역이기 때문에 메모리 점유율이 높은 프로세스를 찾기 위해서는 RES 영역이 높은 프로세스를 찾아야 합니다.

Memory Commit

devops-se-ch-2-7

프로세스는 하드웨어 자원인 CPU를 사용하기 위해 시스템 콜을 통해 커널에게 1GB의 메모리 사용을 요청합니다. 프로세스의 요청을 받은 커널은 1GB만큼에 해당하는 가상의 메모리 주소를 프로세스에게 전달해주지만 실제로 물리 메모리(RAM)에는 할당하지 않습니다.

devops-se-ch-2-8

프로세스가 할당받은 메모리 영역에 쓰기 작업을 해야만 Page fault가 발생하여 커널이 실제 메모리에 프로세스의 가상 메모리 공간을 매핑하게 됩니다. top 커맨드에서는 이렇게 물리 메모리에 바인딩된 영역이 RES로 계산됩니다. 여기서 두 가지 궁금증이 생깁니다. 하나는 **"왜 쓰기 작업에 메모리를 할당할까?"**입니다. 다른 하나는 아래 테스트 결과에서 확인할 수 있듯이 VIRT가 계속 늘어나게 되는데 "이 수치가 계속해서 늘어날까?" 입니다.

malloc() 테스트
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MEGABYTE 1024*1024

int main() {
void *myblock = NULL;
int count = 0;

while (1) {
myblock = (void *) malloc(MEGABYTE);
if (!myblock) {
printf("Error!");
break;
}
printf("Currently allocating %d MB\n", (++count)*MEGABYTE);
// 메모리 쓰기 작업 시 주석 해제
// memset(myblock, 1, MEGABYTE);
sleep(1);
}
exit(0);
}

단순 malloc() 작업

  • VIRT만 증가
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4044 ec2-user 20 0 8340 800 736 S 0.0 0.1 0:00.03 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4044 ec2-user 20 0 10396 800 736 S 0.0 0.1 0:00.03 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4044 ec2-user 20 0 12452 800 736 S 0.0 0.1 0:00.03 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4044 ec2-user 20 0 14508 800 736 S 0.0 0.1 0:00.03 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4044 ec2-user 20 0 17592 800 736 S 0.0 0.1 0:00.03 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4044 ec2-user 20 0 19648 800 736 S 0.0 0.1 0:00.03 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4044 ec2-user 20 0 22732 800 736 S 0.0 0.1 0:00.03 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4044 ec2-user 20 0 25816 800 736 S 0.0 0.1 0:00.04 malloc_test

malloc() 후 메모리 쓰기 작업

  • VIRT 영역의 늘어나는 비율과 비슷하게 RES 증가, 즉 실제로 메모리를 쓰고 있는 RES 영역
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4237 ec2-user 20 0 8340 5388 1412 S 0.0 0.5 0:00.04 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4237 ec2-user 20 0 10396 7500 1412 S 0.0 0.8 0:00.04 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4237 ec2-user 20 0 12452 9612 1412 S 0.0 1.0 0:00.04 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4237 ec2-user 20 0 14508 11724 1412 S 0.0 1.2 0:00.05 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4237 ec2-user 20 0 16564 13572 1412 S 0.0 1.4 0:00.05 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4237 ec2-user 20 0 18620 15684 1412 S 0.0 1.6 0:00.06 malloc_test
[ec2-user@ip-172-31-35-50 ~]$ top -b -n 1 | grep -i malloc
4237 ec2-user 20 0 21704 18852 1412 S 14.3 1.9 0:00.10 malloc_test

devops-se-ch-2-9

VIRT가 계속 증가하다가 메모리가 부족해지면 커널 파라미터 vm.overcommit_memory 값에 따라 swap 영역을 사용하게 됩니다. 해당 커널미터 값에 따라 설정할 수 있는 동작은 아래와 같습니다.

  • 0: 커널에서 사용하고 있는 기본값, overcommit할 수 있는 최댓값은 page cache와 swap 영역 그리고 slab reclaimable 이 세가지의 값을 합한 값이 됩니다. 현재 메모리의 가용 공간은 고려하지 않으며 저기서 합산된 값보다 작으면 계속해서 commit할 수 있게 됩니다.
  • 1: 무조건 commit을 진행합니다. 아무것도 계산하지 않고 요청 온 모든 메모리에 대해 commit이 일어납니다. 이렇게 되면 가용 메모리보다 훨씬 큰 메모리를 요청하는 일이 발생할 수 있으며 메모리 누수가 있는 프로세스가 있다면 시스템 응답 불가 현상을 일으킬 수도 있습니다.
  • 2: 제한적으로 commit을 진행합니다. 값이 0일 때와 같이 계산식이 있으며, vm.overcommit_ratio에 설정된 비율과 swap 영역의 크기를 토대로 계산되며 이 값은 /proc/meminfo에서도 확인할 수 있습니다.
  • 즉, swap 영역은 commit 메모리를 결정할 때와 시스템의 안정성을 유지하는 데에 큰 역할을 합니다.
# [ec2-user@ip-172-31-44-96 ~]$ sysctl -a | grep overcommit_ratio
[ec2-user@ip-172-31-44-96 ~]$ cat /proc/sys/vm/overcommit_ratio
50

[ec2-user@ip-172-31-44-96 ~]$ cat /proc/meminfo | grep SwapTotal
SwapTotal: 0 kB

overcommit_memory 설정이 2로 설정된 경우에만 overcommit_ratio가 의미있으며 이 경우 오버커밋 양은 RAM의 일부와 스왑 공간을 합찬 것에 기반하여 계산됩니다. 예를 들어 현재 RAM 용량이 4GB이고 위와 같이 overcommit_ratio가 50으로 설정되었을 경우 허용 가능한 메모리는 (4GB * 0.5) + 0로 2GB로 제한됩니다.

overcommit_ratio는 시스템의 워크로드와 사용 패턴에 따라 조절해야 하며 메모리 요청이 자주 실패하는 경우 overcommit_ratio 값을 더 늘려서 더 많은 메모리 할당을 허용하게 하며 반대로 OOM killer가 자주 발생한다면 이 값을 낮춰서 메모리 부족 상황을 덜 발생하도록 조절해야 합니다.

devops-se-ch-2-10

여기서 페이지 번호는 논리 주소이고 프레임 번호는 실제 물리 메모리의 주소를 의미하며 페이지 테이블에 매핑되어 있는 페이지 번호와 프레임 번호를 통해 실제 메모리의 주소를 알 수 있습니다. 위 그림은 fork()시에 자식 프로세스가 생성되고 이 자식 프로세스에 새로운 가상 메모리 주소를 할당하게 됩니다. 이렇게 되면 자식 프로세스 또는 부모 프로세스가 쓰기 작업을 하지 않는다면 두 메모리 주소가 가리키는 내용은 모두 같을 것입니다. 즉, 메모리를 효율적으로 사용하지 않게 되며 fork()시에도 자식 프로세스의 가상 메모리 공간을 물리 메모리에 매핑하여야 하므로 그렇지 않을 때보다 더 오랜 시간이 걸리게 됩니다. 이 말은 프로세스 생성 시에 시간이 지연된다는 것과 동일합니다.

devops-se-ch-2-11

쓰기시 복사를 하게 된다면 자식 프로세스를 생성할 때 부모 프로세스와 자식 프로세스는 동일한 물리 메모리 주소를 갖게 됩니다. 쓰기 작업 이전에는 두 프로세스 내용이 동일할 것이므로 동일한 메모리 주소를 갖게 하여 메모리를 보다 더 효율적으로 사용하게 되고 프로세스 생성시 자식 프로세스의 가상 메모리 공간을 물리 메모리에 매핑하지 않아도 되므로 더 빨리 생성할 수 있게 됩니다.

devops-se-ch-2-12

만약 부모 프로세스 또는 자식 프로세스가 쓰기 작업을 한다면 물리 메모리에는 쓰기 작업을 하는 물리 메모리 공간을 복사하여 새로운 메모리 주소로 할당하게 됩니다. 여기까지 메모리 커밋 시에 메모리 공간을 바로 할당하지 않고 쓰기 작업시에 메모리 공간을 할당하는 이유에 대해서 알아보았습니다.

커널이 프로세스의 메모리 요청에 따라 즉시 할당하지 않고 Memory Commit과 같은 기술을 써서 요청을 지연시키는 이유는 fork()와 같은 새로운 프로세스를 만들기 위한 콜을 처리해야 하기 때문입니다. fork() 시스템 콜을 사용하면 커널은 현재 실행 중인 프로세스와 똑같은 프로세스를 하나 더 만들게 되는데, 대부분은 fork() 후 exec() 시스템 콜을 통해서 전혀 다른 프로세스로 변합니다. 따라서 이때 확보한 메모리 영역이 대부분 쓸모 없어질 수도 있습니다. 그래서 COW(Copy-On-Write)라는 기법을 통해서 복사된 메모리 영역에 실제 쓰기 작업이 발생한 후에야 실질적인 메모리 할당을 시작합니다. 그리고 이러한 작업을 지원하기 위해 Memory Commit이 필요합니다. 만약 Memory Commit을 하지 않고 바로 할당한다면 cow와 같은 기술도 사용할 수 없기 때문입니다.

  • MEMORY 커밋 상태는 sar이라는 도구로 모니터링할 수 있습니다.
[root@ip-172-31-35-50 ~]$ sar -r
Linux 5.10.109-104.500.amzn2.x86_64 (ip-172-31-35-50.ap-northeast-2.compute.internal) 10/06/2023 _x86_64_ (1 CPU)

12:00:02 AM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
12:10:02 AM 654140 333560 33.77 0 235608 203912 20.65 223900 40624 256
12:20:02 AM 654400 333300 33.75 0 235620 203912 20.65 223908 40600 232
12:30:02 AM 654140 333560 33.77 0 235620 203912 20.65 223896 40616 264
12:40:01 AM 653384 334316 33.85 0 236784 203912 20.65 223908 41764 268
12:50:02 AM 653376 334324 33.85 0 236804 203912 20.65 223908 41788 256
01:00:02 AM 652880 334820 33.90 0 236828 203912 20.65 223896 41788 236
01:10:02 AM 653132 334568 33.87 0 236848 203912 20.65 223912 41808 268

# (%commit) 할당만 해주고 실제 사용하지 않는 메모리의 양이 전체 메모리의 20.65%

프로세스의 상태

devops-se-ch-2-13

S 상태의 프로세스가 많은 것은 시스템에 큰 영향을 끼치지 않지만 D 상태의 프로세스가 많으면 특정 요청이 끝나기를 기다리고 있는 프로세스가 많다는 뜻이고, 이 프로세스들은 요청이 끝나면 R 상태로 다시 돌아가야 하기 때문에 시스템의 부하를 계산하는 데 포함됩니다.

devops-se-ch-2-14

위 그림은 프로세스 상태 다이어그램이며 D 상태(Uninterruptible sleep)인 프로세스는 위 그림과 같이 대기 큐에서 I/O 작업이 완료될 때까지 대기하는 상태이며 I/O 입출력 완료가 끝나게 되면 준비 상태로 돌아가게 됩니다.

좀비 프로세스

devops-se-ch-2-15

왼쪽 그림과 같이 정상적인 종료인 경우 자식 프로세스의 exit()후에 부모 프로세스가 자식 프로세스에 할당된 정보를 초기화 후 종료하지만 오른쪽 그림과 같이 부모 프로세스가 자식 프로세스에 할당된 정보를 초기화하기도 전에 먼저 exit()하여 자식 프로세스가 exit()되었다는 것을 알려줄 곳이 없어 이때의 자식 프로세스는 좀비 프로세스로 남게 됩니다.

부모 프로세스가 먼저 종료될 경우에는 init 프로세스가 그 자식 프로세스의 부모가 되며 init은 정기적으로 wait()을 호출하여 종료된 자식의 상태를 회수하기 때문에 부모 프로세스가 먼저 종료되더라도 자식 프로세스는 영원히 좀비 상태에 머무르지 않게 됩니다.

devops-se-ch-2-16

좀비 프로세스는 시스템의 리소스를 차지하지 않기 때문에 그 존재 자체는 큰 문제가 되지 않습니다. 스케줄러에 의해 선택되지 않기 때문에 CPU를 사용하지 않고 이미 사용이 중지된 프로세스이기 때문에 메모리를 사용하지도 않습니다. 하지만 PID를 고갈시키는 문제가 발생할 수도 있게 됩니다.

생성가능한 최대 PID 확인하기 (sysctl -a | grep -i pid_max)

# 프로세스의 최대 개수 32768개, 프로세스가 가질 수 있는 PID: 1 ~ 32768
[root@ip-172-31-35-50 ~]# sysctl -a | grep -i pid_max
kernel.pid_max = 32768

pid_max의 최댓값으로 가질 수 있는 최대 크기는 65536이며 이때 프로세스 개수는 65536개 이상 존재할 수 없습니다.

프로세스의 우선순위

devops-se-ch-2-17

CPU마다 Run Queue라는 것이 존재하며, Run Queue에는 우선순위 별로 프로세스가 연결되어 있습니다. 스케줄러는 유휴 상태에 있던 프로세스가 깨어나거나 특정 프로세스가 스케줄링을 양보하는 등의 경우에 현재 Run Queue에 있는 프로세스들 중 가장 우선순위가 높은 프로세스를 꺼내서 디스패처에 넘겨줍니다. 디스패처는 현재 실행 중인 프로세스의 정보를 다른 곳에 저장한 후 넘겨받은 프로세스의 정보를 가지고 다시 연산을 하도록 요청합니다.

devops-se-ch-2-18

모든 프로세스들은 20의 기본 우선순위 값을 갖는데 여기에 nice 값을 줘서 우선순위를 바꿀 수 있고 우선순위가 더 높은 프로세스는 더 많은 스케줄링이 될 수 있는 기회를 얻게 됩니다.

왼쪽 케이스의 경우 nice(nice -n -10)로 Task B의 우선순위를 낮춰도 CPU 경합을 벌일 필요가 없어 비슷한 시간에 끝나게 됩니다. 하지만 오른쪽 케이스의 경우 Task A와 Task B 모두 CPU1에서 경합을 벌이고 있으므로 우선순위에 따라 Task B가 먼저 끝나게 됩니다.

devops-se-ch-2-19

**RT**는 "Real Time"을 의미합니다. 실시간 우선순위는 실시간 작업을 위한 것으로, 일반 작업보다 높은 우선순위를 가집니다. 리눅스에서는 실시간 작업을 위해 SCHED_FIFO와 SCHED_RR과 같은 두 가지 실시간 스케쥴러가 제공됩니다.

실시간 우선순위를 가진 프로세스(RT)는 CFS로 스케줄링되는 일반 프로세스보다 더 높은 우선순위를 가집니다. 따라서 RT 우선순위를 가진 프로세스는 CPU 사용을 요청하면 CFS로 스케줄링되는 다른 프로세스보다 먼저 실행됩니다.