✌️
Studylog
See More
Booklog
Booklog
  • 객체지향의 사실과 오해
    • 1장. 협력하는 객체들의 공동체
    • 2장. 이상한 나라의 객체
  • DevOps와 SE를 위한 리눅스 커널이야기
    • 1장. 시스템 구성 정보 확인하기
    • 2장. top을 통해 살펴보는 프로세스 정보들
    • 3장. Load Average와 시스템 부하
    • 4장. free 명령이 숨기고 있는 것들
Powered by GitBook
On this page
  • 가장 눈에 띄는 메모리 정보
  • 프로세스의 상태들
  • 프로세스의 우선순위
  • 요약
  1. DevOps와 SE를 위한 리눅스 커널이야기

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

Previous1장. 시스템 구성 정보 확인하기Next3장. Load Average와 시스템 부하

Last updated 22 days ago

리눅스에는 시스템의 상태를 살펴볼 수 있는 명령어들이 존재한다.

그 중에서 이번 챕터에서 사용되는 top 명령어는 시스템의 상태를 전반적으로 가장 빠르게 확인할 수 있는 명령어이다.

top

Interval 간격(기본 3초)으로 화면을 갱신하며 정보를 보여준다.

이 때, 순간의 정보를 보기 위해선 "-b" 옵션을 사용하면 순간의 상태가 캡쳐된 내용을 볼 수 있다.

top 정보
  • top - 01:23:32 up 45 days: 현재 서버의 시간과 얼마나 구동 되었는지에 대한 시간

  • Load Average: 현재 시스템이 얼마나 많은 일을 하고 있는지

  • Tasks: 현재 프로세스의 수

  • Mem, Swap: 메모리 정보

  • PR: 프로세스의 우선 실행 순위

  • NI: 프로세스 실행 순위에 영향을 줄 수 있는 값

    • 기본 PR + NI = 실제 PR

  • VIRT: 프로세스(task)의 virtual memory 전체 용량

  • RES: 현재 프로세스(task)의 물리 메모리의 양

  • SHR: 다른 프로세스와 공유하고 있는 shared memory의 양

  • S: 프로세스의 상태

man top 에서 제공하는 프로세스의 상태들

   29. S  --  Process Status
       The status of the task which can be one of:
           D = uninterruptible sleep
           I = idle
           R = running
           S = sleeping
           T = stopped by job control signal
           t = stopped by debugger during trace
           Z = zombie

가장 눈에 띄는 메모리 정보

VIRT

  • Task, 즉 프로세스에 할당된 가상 메모리 전체의 크기

RES

  • VIRT 중 실제 메모리에 올려서 사용하고 있는 물리 메모리의 크기

SHR

  • 다른 프로세스와 공유하고 있는 메모리의 크기

VIRT 는 실제로 할당되지 않은 "가상의 공간" 이기 때문에 해당 값이 크다고 해서 문제가 되진 않는다.

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

"SHR의 구체적인 예는 어떤 것이 있을까?"

대표적으로 라이브러리가 SHR 영역에 포함될 수 있다.

대부분의 리눅스 프로세스들은 glibc 라이브러리를 참조하기 때문에 사용하는 프로세스마다 glibc 의 내용을 메모리에 올려서 사용하는 것은 공간 낭비일 것이다.

커널은 이럴 경우를 대비하여 공유 메모리 라는 개념을 도입 했고, 다수 프로세스가 함께 사용하는 라이브러리는 공유 메모리 영역에 올려서 함께 사용하도록 구현되었다.

"왜 메모리는 VIRT 와 RES 로 구분되어 있을까?"

Memory Commit

VIRT 로 표현되는 가상 메모리는 프로세스가 커널로부터 사용을 예약 받은 메모리라고 생각할 수 있다.

그렇기 때문에 프로세스는 malloc() 같은 시스템 콜로 자신이 필요로 하는 메모리의 영역을 할당 해달라고 요청한다.

이에 커널은 가용 가능한 공간이 있다면 성공 메시지와 함께 해당 프로세스가 사용될 수 있도록 가상의 메모리 주소를 전달한다.

기억해야 할 것은, 이 가상의 메모리 주소를 할당 받았다는 것일 뿐 물리 메모리에 해당 영역이 할당된 상태가 아니라는 것이다.

  • Page fault: 프로세스가 할당받은 메모리 영역에 실제로 쓰기 작업을 하는 것

  • Page Table: 커널이 실제 물리 프로세스의 가상 메모리 공간을 매핑하는 것

실제 메모리 영역에 쓰기 작업을 하면 커널은 물리 메모리에 프로세스의 가상 메모리 공간을 매핑하는데, 이렇게 물리 메모리에 바인딩된 영역이 RES 로 계산된다.

  • 실제 팀 회식을 하기 위해 우리 팀원 전체 인원수를 예약했다. (10명)

  • 그 중 몇 몇의 팀원이 사정이 생겨 일부만 참석했다. (7명)

  • VIRT: 10, RES: 7 으로 계산할 수 있는것이다.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.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); -> RES 영역에 할당, 없는 경우 VIRT만 증가


        sleep(1);
    }

    exit(0);
}
  • 가상 메모리 공간을 요청하는 시스템 콜 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와 같은 기술을 사용할 수 없었을 것이다.

부모 프로세스가 3GB 메모리 사용 중
→ fork() 실행
→ 자식 프로세스도 3GB 메모리 할당 (복사됨)
→ 총 6GB 사용되는 것처럼 보임

하지만 대부분의 경우, 자식 프로세스는 곧 exec()를 호출해서 완전히 다른 프로그램으로 변하게 되며, 이 복사된 메모리는 전혀 사용되지 않을 수 있음

🔹 원리

  • fork() 호출 시, 부모 프로세스의 메모리를 진짜로 복사하지는 않고, 부모와 자식이 동일한 메모리 페이지를 공유하도록 설정

  • 이 메모리는 읽기 전용(Read-only)으로 표시

  • 부모 또는 자식 중 누군가 이 메모리에 쓰기(write) 시도를 하면, 그때 비로소 해당 페이지를 복사해서 독립적인 공간에 할당

  • 즉, 실제로 변경되는 메모리만 추가로 메모리 사용이 발생함.

🔹 예시

  • 부모 프로세스가 3GB 메모리 사용

  • fork() 호출

  • 자식은 부모의 메모리를 그대로 공유 (추가 메모리 소모 거의 없음)

  • 자식이 메모리 일부를 수정 → 해당 페이지만 복사됨 (예: 4KB)

  • 결과적으로 메모리 사용량은 6GB까지 늘어나지 않음

🔹 결론

항목
COW 없음
COW 있음

fork() 시 메모리

전체 메모리 즉시 복사 (예: 3GB → 6GB)

복사 없음, 페이지 공유

물리 메모리 사용량

부모 + 자식 = 6GB

부모와 자식이 공유 (대부분 3GB 유지)

성능

느림 (전체 복사)

빠름 (실제 복사는 나중에 필요할 때만)

메모리 낭비

많음

적음

실제 시스템에서 Memroy Commit 상태를 확인할 수 있는데 이는 sar 이라는 모니터링 툴로 확인할 수 있으며 %commit 의 숫자가 시스템의 메모리 커밋 비율을 나타낸다.

이 커밋된 메모리의 비율이 높다면 시스템에 부하를 일으키거나 최악의 경우 응답 불가 현상을 일으킬 수 있는데, 그렇기 때문에 리눅스에서는 메모리 커밋에 대한 동작 방식을 vm.overcommit_memory 라는 파라미터로 제어할 수 있다.

vm.overcommit_memory 이 파라미터별로 처리하는 방법

  • 0: 커널에서 사용하고 있는 기본 값이며 overcommit 할 수 있는 최댓값은 page cache + swap 영역 + slab reclaimable 이며, 현재 메모리에 가용 공간이 얼마인지 고려하지 않고 오버커밋 가능한 최댓값 보다 낮다면 계속 커밋할 수 있다.

  • 1: 무조건 커밋을 진행한다. 요청 온 모든 메모리에 대해 커밋이 일어나며 가용 메모리보다 훨씬 큰 메모리를 요청하는 일이 발생하여 시스템 응답 불가 현상을 일으킬 수 있다.

  • 2: 제한적 커밋을 진행한다. 값이 0일 때와 같이 계산식이 있고 vm.overcommit_ratio에 설정된 비율과 swap 영역의 크기를 기반으로 계산된다.

위 동작 방식을 살펴보았을 때 Swap 영역은 커밋 메모리를 결정하는 데 중요한 개념이며 시스템의 안정성을 유지하는 데 큰 역할을 한다고 볼 수 있다.

프로세스의 상태들


   29. S  --  Process Status
       The status of the task which can be one of:
           D = uninterruptible sleep
           I = idle
           R = running
           S = sleeping
           T = stopped by job control signal
           t = stopped by debugger during trace
           Z = zombie

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를 할당하지 못하는 고갈을 일으킨다.

[ec2-user@ip-172-31-7-39 chapter2]$ sudo sysctl -a | grep -i pid_max
> kernel.pid_max = 419430

이 시스템에서 생성되는 프로세스가 가질 수 있는 PID의 최대값은 419430이다. 그래서, 이 시스템에서 생성되는 모든 프로세스는 1 ~ 419430 사이의 임의의 값을 PID로 배정받는다.

프로세스의 우선순위

top 에서 얻을 수 있는 항목 중 PR과 NI 값에 대해 살펴본다.

PR과 NI는 커널이 프로세스를 스케줄링할 때 사용하는 우선수위를 나타내는 값

  • PR — Priority The priority of the task: 커널에서 인식하는 해당 프로세스의 실제 우선순위 값

  • NI — Nice value: nice 값이라고 부르며, 명령어를 통해서 우선순위를 낮출 때 사용되는 값으로 값이 낮을수록 우선순위가 높은 것이며, nice 명령을 이용하여 PR 값을 낮출 수 있다. 그럼 더 높은 우선순위를 가지기 때문에 더 많은 스케줄링이 될 수 있는 기회를 얻게 된다.

CPU 마다 Run Queue 라는 것이 존재하며, Run Queue 에는 우선순위 별로 프로세스가 연결되어 있다.

스케줄러는 유휴 상태에 있던 프로세스가 깨어나거나 특정 프로세스가 스케줄링을 양보하는 등의 경우에 현재 Run Queue 에 있는 프로세스들 중 가장 우선순위가 높은 프로세스를 꺼내서 디스패처에게 넘겨준다.

디스패처는 현재 실행중인 프로세스의 정보를 다른 곳에 저장한 후 넘겨받은 프로세스의 정보를 가지고 다시 연산을 하도록 요청한다.

기본적으로 모든 프로세스들은 20의 우선순위 값을 갖는데 여기에 nice 값을 주면 우선순위 값이 바뀐다.

PR이 16인 auditd 처럼 20에 -4가 적용되어 20 우선순위를 갖는 프로세스 보다 더 자주 실행된다.

요약


  1. top 명령으로 현재 시스템의 CPU, Memory, Swap의 사용량 및 각 프로세스들의 상태와 메모리 점유 상태를 확인할 수 있음

  2. top 명령의 결과 중 VIRT 는 프로세스에게 할당된 가상 메모리 전체의 크기를 가리킴.

    1. RES 는 그 중에서도 실제로 메모리에 올려서 사용하고 있는 물리 메모리의 크기

    2. SHR 은 다른 프로세스와 공유하고 있는 메모리의 크기를 의미

  3. 커널은 프로세스가 메모리를 요청할 때 그에 맞는 크기를 할당해 주지만 해당 영역을 물리 메모리에 바로 할당하지 않음. 프로세스가 할당 받은 메모리 영역을 사용할 때가 되어서야 비로소 물리 메모리를 할당하기 시작하며 이를 Memory Commit 이라고 부름

  4. vm.overcommit_memory 는 커널의 Memory Commit 동작 방식을 변경할 수 있는 커널 파라미터

    1. 이 값이 0이면 최댓값을 바탕으로 Memory Commit을 진행

    2. 1이면 무조건 overcommit

    3. 2이면 현재 시스템의 정보와 vm.overcommit_ratio 에 설정된 비율을 바탕으로 제한적 Memory Commit을 진행

  5. top 으로 볼 수 있는 프로세스의 상태

    1. D는 I/O 대기중인 프로세스

    2. R은 실제 실행 중인 프로세스

    3. S는 sleep 상태의 프로세스

    4. Z는 좀비 프로세스

    5. T는 tracing 중인 프로세스

  6. 프로세스에는 우선순위라는 것이 있어서 우선순위가 낮을수록 더 빨리 실행되며, 우선순위는 nice 명령을 통해 조절 가능

문제 상황

⚠️