• 서버용으로 사용되는 시스템의 관리자는 어떤 사용자가 언제 로그인했는지 확인
  • last 명령어는 로그인 로그와 로그 아웃 로그를 확인 가능

last 명령어로 알 수 있는 정보

  1. 접속계정명
  2. 접속장치명
  3. 접속한 IP주소
  4. 접속시간
  5. 시스템 재부팅 정보

last 명령어를 통해 특정 정보 추출

1. 최근 접속정보

$ last
root     pts/0        [서버 시스템 IP]    Mon Jun 21 23:11   still logged in
root     pts/0        [서버 시스템 IP]    Mon Jun 21 23:01 - 23:10  (00:08)
root     pts/0        [서버 시스템 IP]    Mon Jun 21 22:37 - 22:37  (00:00)

2. 사용자 접속 정보

# root 사용자 접속 정보 -> 개별 사용자가 있는 경우를 제외하고 root로 접속하는 기록 출력
$ last root
root     pts/0        [서버 시스템 IP]    Mon Jun 21 23:11   still logged in
root     pts/0        [서버 시스템 IP]    Mon Jun 21 23:01 - 23:10  (00:08)
root     pts/0        [서버 시스템 IP]    Mon Jun 21 22:37 - 22:37  (00:00)

# reboot 사용자 접속 정보 -> rebooting한 기록 확인
$ last reboot
reboot   system boot  3.10.0-1160.6.1. Fri Jan 15 14:10 - 12:56 (11+22:46)
reboot   system boot  3.10.0-1160.6.1. Fri Jan 15 13:58 - 14:09  (00:11)
reboot   system boot  3.10.0-1160.6.1. Fri Jan 15 13:22 - 13:56  (00:34)

3. 지정일자 이전에 접속한 정보

# 지정일자 이전에 접속한 정보
# last -t YYYYMMDDHHMMSS 형식
$ last -t 20201210000000           # 2020년 12월10일 이전의 접속 기록만 출력
root     pts/0        [서버 시스템 IP]    Wed Dec  9 17:38 - 17:43  (00:04)
root     pts/0        [서버 시스템 IP]    Wed Dec  9 16:33 - 17:36  (01:03)
root     pts/0        [서버 시스템 IP]    Tue Dec  8 22:46 - 22:51  (00:05)

4. 원하는 행의 수만큼 출력

$ last -n 3
root     pts/0        [서버 시스템 IP]    Mon Jun 21 23:24   still logged in
root     pts/0        [서버 시스템 IP]    Mon Jun 21 23:11 - 23:22  (00:11)
root     pts/0        [서버 시스템 IP]    Mon Jun 21 23:01 - 23:10  (00:08)

wtmp begins Sat Dec  5 04:17:18 2020

5. IP주소를 제외한 정보

$ last -R
root     pts/0        Mon Jun 21 23:24   still logged in
root     pts/0        Mon Jun 21 23:11 - 23:22  (00:11)
root     pts/0        Mon Jun 21 23:01 - 23:10  (00:08)
root     pts/0        Mon Jun 21 22:37 - 22:37  (00:00)

6. IP주소를 열의 마지막에 출력

$ last -a
root     pts/0        Mon Jun 21 23:24   still logged in    [서버 시스템 IP]
root     pts/0        Mon Jun 21 23:11 - 23:22  (00:11)     [서버 시스템 IP]
root     pts/0        Mon Jun 21 23:01 - 23:10  (00:08)     [서버 시스템 IP]
root     pts/0        Mon Jun 21 22:37 - 22:37  (00:00)     [서버 시스템 IP]

7. 외부 접속 정보만을 출력

$ last -d
root     pts/0        [서버 시스템 IP]    Mon Jun 21 23:24   still logged in
root     pts/0        [서버 시스템 IP]    Mon Jun 21 23:11 - 23:22  (00:11)
root     pts/0        [서버 시스템 IP]    Mon Jun 21 23:01 - 23:10  (00:08)
root     pts/0        [서버 시스템 IP]    Mon Jun 21 22:37 - 22:37  (00:00)

  • 리눅스에서는 서버에 접속실패 정보와 접속정보를 기록

접속 실패 로그 확인 → btmp

  • ssh 접속 시도 실패 로그는 /var/log/btmp 파일에 특수하게 저장

  • /var/log/btmp는 바이너리 파일로 이루짐

  • /var/log/btmp 파일을 보기 위해서는 last -f 명령을 이용하여 확인 가능

  • lastb 명령어는 last -f /var/log/btmp와 동일한 결과 출력

    $ last -f /var/log/btmp
    root     ssh:notty    [시스템 IP]    Tue May 25 03:07 - 09:57  (06:50)
    root     ssh:notty    [시스템 IP]    Tue May 25 03:07 - 03:07  (00:00)
    root     ssh:notty    [시스템 IP]    Tue May 25 00:52 - 03:07  (02:15)
    
    $ lastb
    lastb
    root     ssh:notty    [시스템 IP]    Tue May 25 03:07 - 03:07  (00:00)
    root     ssh:notty    [시스템 IP]    Tue May 25 03:07 - 03:07  (00:00)
    root     ssh:notty    [시스템 IP]    Tue May 25 00:52 - 00:52  (00:00)



접속정보 기록 확인 → wtmp

  • 성공한 로그인/로그아웃 정보 및 시스템의 boot/shutdown의 히스트리 정보를 파일로 저장
  • /var/log/wtmp는 바이너리 파일로 이루짐
  • /var/log/wtmp 파일을 보기 위해서는 last -f 명령을 이용하여 확인 가능
  • last 명령어는 last -f /var/log/wtmp와 동일한 결과 출력
    $ last -f /var/log/wtmp
    root     pts/0        [시스템 IP]    Mon Jun 21 23:11   still logged in
    root     pts/0        [시스템 IP]    Mon Jun 21 23:01 - 23:10  (00:08)
    root     pts/0        [시스템 IP]    Mon Jun 21 22:37 - 22:37  (00:00)
    root     pts/0        [시스템 IP]    Mon Jun 21 22:31 - 22:36  (00:05)


시스템에 현재 로그인한 사용자들에 대한 상태 정보 → utmp

  • 로그파일은 binary 파일로 되어 있어 직접 확인 불가능 → 아래 명령어로 확인 가능
  • 로그파일 확인 명령어 : w, who, finger

1. w 명령어

  • utmp를 참조하여 출력
  • 현재 시스템에 성공적으로 로그인한 사용자정보, 시스템로드 정보 및 uptime 정보 출력
    $ w
    23:34:07 up 145 days, 10:36,  1 user,  load average: 0.00, 0.01, 0.05
    USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
    root     pts/0      [시스템 IP]        23:24    7.00s  0.06s  0.02s w

2. who 명령어

  • utmp를 참조하여 출력
  • 현재 시스템에 성공적으로 로그인한 사용자의 정보와 접속한 Client IP를 출력
    $ who
    root     pts/0        2021-06-21 23:24 ([시스템 IP])

3. finger 명령어

  • 사용자 계정 정보와 최근 로그인 정보, 이메일, 예약 작업 정보 등을 볼 수 있는 명령어

  • yum install finger -y로 finger 명령어 설치

    # finger 패키지가 없는 경우 다운 필요
    $ yum install -y finger
    
    # root 사용자 계정 정보 확인
    $ finger root
    Login: root                               Name: root
    Directory: /root                        Shell: /bin/bash
    On since Mon Jun 21 23:24 (KST) on pts/0 from [시스템 IP]
     4 seconds idle
    New mail received Tue May 25 03:08 2021 (KST)
       Unread since Sat Jan 30 21:53 2021 (KST)
    No Plan.



lastlog 명령어

  • 각 사용자들이 언제 마지막으로 접속하였는가를 확인
  • /etc/passwd 파일에 정의되어 있는 모든 사용자들의 마지막 접속정보를 확인
  • lastlog는 /var/log/lastlog 파일의 정보를 출력

1. /etc/passwd 파일에 정의된 모든 사용자의 마지막 접속 정보 확인

$ lastlog
Username         Port     From             Latest
root               pts/0    [시스템 IP]    Mon Jun 21 23:24:25 +0900 2021
bin                                               **Never logged in**
daemon                                        **Never logged in**
[...생략...]

2. 사용자의 마지막 접속 정보

# lastlog -u userid
$ lastlog -u root
Username         Port     From             Latest
root             pts/0    [시스템 IP]    Mon Jun 21 23:48:26 +0900 2021

3. N일 이전에 접속한 정보

# lastlog -b N -> N일 이후에 접속한 기록이 있는 사용자는 제외
$ lastlog -b 10
Username         Port     From             Latest
bin                                            **Never logged in**
daemon                                     **Never logged in**
[...생략...]

4. D일 부터 현재까지 접속한 정보

# lastlog -t D
$ lastlog -t 5
Username         Port     From             Latest
root             pts/0    [시스템 IP]    Mon Jun 21 23:48:26 +0900 2021

ifconfig 명령어를 통해 network interface의 정보 확인

$ ifconfig
em1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 110.45.211.60  netmask 255.255.255.128  broadcast 110.45.211.127
        inet6 fe80::e643:4bff:fe18:b550  prefixlen 64  scopeid 0x20<link>
        ether e4:43:4b:18:b5:50  txqueuelen 1000  (Ethernet)
        RX packets 318352  bytes 159830547 (152.4 MiB)
        RX errors 309  dropped 4864  overruns 0  frame 309
        TX packets 240498  bytes 41808200 (39.8 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 246  bytes 27053 (26.4 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 246  bytes 27053 (26.4 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

ifconfig 명령어 카운터 의미

1.errors

  • 에러가 발생한 모든 패킷 카운트
  • 너무 긴(짧은) 프레임 에러, 링-버퍼 오버플로우 에러, CRC 에러, 프레임 정렬 에러, FIFO 오버런, 패킷 분실 등
  • 아래 3개 overruns, dropped, frame 등을 모두 포함한 에러 카운터

2. overruns

  • FIFO 오버런, 버퍼가 꽉차서 버린 패킷 카운트
  • 이더넷이 처리할 수 없을 정도로 빠르게 자료가 오고감으로써 손실된 패킷의 갯수

3. dropped

  • 의도하지 않는 패킷 카운트
  • linux buffers에 공간이 없어서 버려진 패킷. "no space in linux buffers" 표현
  • VLAN tags가 맞지 않거나 IPv6 설정이 없는데 IPv6 패킷이 들어왔을 때 버린 패킷들

4. frame

  • 프레임 정렬 에러, 프레임 길이가 틀린 패킷 카운트
  • 수신 프레임이 바이트 단위가 아닌 여분의 비트를 포함하는 패킷
  • frame은 정렬되지 않은 프레임만 계산하므로 길이가 8로 나눌 수 없는 프레임을 의미
  • 길이 때문에 유효한 프레임이 아니며 단순히 폐기됨

ethtool -S 명령어를 사용하여 error를 자세하게 확인

$ ethtool -S em1 | grep -i error
     rx_errors: 310
     tx_errors: 0
     rx_over_errors: 0
     rx_crc_errors: 310
     rx_frame_errors: 0
     rx_fifo_errors: 0
     rx_missed_errors: 0
     tx_aborted_errors: 0
     tx_carrier_errors: 0
     tx_fifo_errors: 0
     tx_heartbeat_errors: 0
     rx_length_errors: 0
     rx_long_length_errors: 0
     rx_short_length_errors: 0
     rx_csum_offload_errors: 1

  • 기본적으로 캐시를 생각하면, 빠르게 읽기 위해서 사용하는 저장공간으로 생각
  • 캐시는 쓰기 명령을 수행할 때도 사용 → 아래의 그림처럼 캐시에 Write Buffer라는 걸 두어서 쓰기 성능 향상에 사용
  • Write Buffer는 CPU가 쓰기 명령 수행 중에 좀 더 효율적으로 다른 일을 할 수 있도록 해줌
  • 쓰기 버퍼 방식은 크게 두 가지가 존재 → Write Through와 Write Back

Write Through

  • Write Through라는 용어는 쓰루 패스와 마찬가지로 Memory에 뭔가를 쓰기 명령을 수행할 때, Cache와 Memory 값을 일치 시켜주는 방식
  • CPU가 주기억장치(RAM) 또는 디스크(Disk)로 데이터를 기입하고자 할 때 데이터는 먼저 캐시로 기입 → 데이터가 캐시 됨과 동시에 주기억장치(RAM) 또는 디스크(Disk)로 업데이트하는 구조
  • 데이터 로스의 리스크가 있으면 안되는 상황에서는 Write Through를 사용하는 것이 바람직

Write Through 장점

  • 캐시와 메모리에 업데이트를 같이 하여, 데이터 일관성을 유지할 수 있어서 안정적
  • inconsistency(불일치)현상이 발생 X

Write Through 단점

  • 속도가 느린 주기억장치 또는 디스크로 동시에 데이터를 기록
  • 완료될 때까지 CPU가 대기하는 시간이 필요하기 때문에 성능이 떨어짐

Write Through를 사용하여 데이터를 처리하는 구조


Write Back

  • CPU 데이터를 사용할 때 데이터는 먼저 캐시로 기록되는데, 캐시 내에 일시적으로 저장된 후에 블록 단위에 캐시로부터 해제되는 때(캐시안에 있는 내용을 버릴시) 에만 주기억장치 또는 보조기억장치에 기록되는 방식
  • 데이터를 쓸 때 메모리에는 쓰지 않고 캐시에만 업데이트를 하다가 필요할 때에만 주기억장치나 보조기억장치에 기록하는 방법
  • 데이터 로스의 리스크를 조금 감수하더라도 빠른 서비스를 요하는 상황에서는 Write Back을 사용하는 것이 바람직

Write Back 장점

  • Write Through보다 훨씬 빠름

Write Back 단점

  • 속도가 빠른 대신에 캐시에 업데이트 하고 메모리에는 바로 업데이트를 하지 않기 때문에 캐시와 메모리가 서로 값이 다른 경우가 발생 가능
  • inconsistency(불일치)
  • 캐시에만 써놓고 Device에 값을 안넘기는 경우가 발생하여서 문제 발생 가능
  • Write Back 문제를 해결하기 위하여 Cache Flush 또는 Cache clean을 사용

Write Back을 사용하여 데이터를 처리하는 구조

※ 참조

  • Cache Flush는 Cache Invalidate(캐시 무효화)로 Cache 안의 내용을 마치 Reset하듯이 정리함
  • Cache Clean은 Cache에 있던 내용을 Memory에도 update해줌

Write Back과 Write Through의 성능 차이가 큼(데이터 베이스 로드 처리량 비교)


  • ACL이나 보안 그룹(security group)으로 포트가 막혀 있는지, 열려있는지 확인하는 방법
  • 보통은 ping 같은 명령어로 ICMP 패킷을 쏴보고 해당 서버가 살아있는지 먼저 확인
  • ping으로는 살아 있는데, ssh 나 http 같은 건 안 되는 경우에, TCP 포트가 열려 있는 상태 확인
  • /dev/의 built-in에 대해서 조금 더 자세한 내용 참고 URL : https://tldp.org/LDP/abs/html/devref1.html

1. tcping

  • TCP SYN 패킷을 보내서 해당 포트가 열려 있는지 확인해주는 간단한 프로그램
  • 서버의 네트웍 연결 상태를 확인할 때, ping 차단(ICMP차단)된 서버의 네트웍 상태를 TCP를 통해 특정 tcp포트 확인
  • "telnet 서버주소 포트" 시도 후 'Escape character is "]"' 문자열이 보이는지 확인하여 연결 상태를 확인 가능
  • tcping을 쓰게 되면, ping처럼 round-trip time을 출력
  • TCP 연결 속도도 확인 가능
  • TCP 연결은 최대한 빠르게 처리하기 때문에 latency를 좀 더 정확히 보려면 ping보다는 tcpping을 써야 함
  • tcpping 명령어를 사용하기 위해서 tcping 패키지 설치 필요

    $ yum install -y tcping
    
    # 설치된 tcping 명령어 위치 확인
    $ which tcping
    /usr/bin/tcping
  • tcping 명령어 사용 예시

    # Open된 포트에서 tcping을 통해 접속 확인
    $ tcping -t 5 www.daum.net 80
    www.daum.net port 80 open.
    
    # Close된 포트에서 tcping을 통해 접속 확인
    $ tcping -t 5 www.daum.net 8080
    www.daum.net port 8080 closed.



2. telnet <IP><PORT>

  • 텔넷으로 IP, port를 명시하면 해당 서버에 저 포트가 열려 있는지 간단히 확인 가능
  • 연결 → 서버 연결되면 명령어가 입력이 안되기에 Ctrl+']' 를 누르시면 텔넷 프롬프트가 출력
  • 텔넷 프롬프트가 출력될 때 quit을 입력하여 종료
  • telnet 명령어 사용 예시

    # Close된 포트에서 telnet 접속 요청
    $ telnet 127.0.0.1 10002
    Trying 127.0.0.1...
    telnet: Unable to connect to remote host: Connection refused
    
    # Open된 포트에서 telnet 접속 요청
    $ telnet 127.0.0.1 22
    Trying 127.0.0.1...
    Connected to 127.0.0.1.
    Escape character is '^]'.
    SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.1
    ^C^]
    telnet> quit
    Connection closed.



3. echo > /dev/tcp/<ip>/<port>

  • bash의 built-in 기능

  • /dev/tcp를 이용하면 wget이나 curl이 없어도 파일을 받아올 수 있음

  • 포트가 열려 있는 경우라면, 아무 메시지가 나오지 않은 상태로 끝남

  • echo $?을 통해 이전 명령어의 결과를 출력하면 0이 나옴

  • 바로 전 실행 명령이 0이면 정상적으로 끝났다는 유닉스 세계의 메시지

    $ echo > /dev/tcp/127.0.0.1/22
    $ echo $?
    0
  • 포트가 열려 있지 않은 경우에는 에러 메시지도 나오며, $?의 값이 1로 나옴 → 성공적으로 연결 X
    $ echo > /dev/tcp/127.0.0.1/10002
    bash: connect: 연결이 거부됨
    bash: /dev/tcp/127.0.0.1/10002: 연결이 거부됨
    $ echo $?
    1

1. diskpart로 접속

  • 시작 → 프로그램 및 파일 → cmd 검색
  • cmd에서 아래 명령어 입력
    > diskpart

2. 디스크 리스트 출력

  • 디스크 상태창에서 현재 본인이 사용하고 있는 하드디스크, SSD, USB 등을 출력
    > list disk

3. 디스크 선택

  • USB 디스크는 디스크 1번임으로 disk 1을 선택
    > sel disk 1

4. 선택한 디스크를 청소

  • 디스크 1을 선택한 후에 clean 명령어를 통해 디스크 용량 문제를 해결
    > clean

5. 선택한 디스크를 청소후 출력


6. 디스크 볼륨 설정

  • 시작 → 프로그램 및 파일 검색 → 컴퓨터 관리 → 디스크 관리
  • 디스크 1에 파티션이 할당 되지 않을 것을 확인 할 수 있음

7. 새 단순 볼륨을 통해 USB 디스크 파티션 할당


8. 디스크 포맷

  • 새 단순 볼륨을 클릭한 후 아래의 그림이 나올 때까지 다음(N)을 클릭해도 상관없음

9. 완료


  • 프로세스에서 중요한 요소 → VIRT, RES, SHR
  • 현재 프로세스가 사용하고 있는 메모리와 관련된 값
  • 프로세스는 task를 의미

VIRT → task가 사용하는 virtual memory의 전체 용량을 의미

  • 프로세스에 할당된 가상 메모리 전체의 크기
  • VIRT는 프로그램이 현재 얼마나 많은 메모리를 접근할 수 있는지를 출력
  • VIRT는 물리 메모리의 사용한 공간을 의미하지는 않음
  • VIRT = SWAP + RES
  • VIRT는 실제로는 할당되지 않은 가상의 공간(swap)이기 때문에 VIRT의 해당 값이 크다고 해도 문제되지는 않음

1. VIRT의 Memory Commit

  • 프로세스가 커널로부터 사용을 예약 받은 메모리 → 프로세스는 malloc()과 같은 시스템 콜로 자신이 필요로 하는 메모리의 영역을 할당해 줄것을 요청
  • 프로세스가 요청한 내용은 커널이 가용할 수 있는 공간이 있다면 성공 메시지와 함께 해당 프로세스가 사용할 수 있도록 가상의 메모리 주소를 전달
  • malloc을 통해 할당 받은 가상 메모리는 물리 메모리에 해당 영역이 할당된 상태는 아님

2. VIRT의 malloc 테스트 코드 → malloc_test.c 파일을 생성

$ vi malloc_test.c
#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);
        sleep(1);
    }
    exit(0);
}

# malloc_test 파일을 malloc_test 실행 파일로 만듦
$ gcc -o malloc_test malloc_test.c
$ ls -al
-rwxr-xr-x  1 root root 8520 Oct 11 17:53 malloc_test
-rw-r--r--  1 root root  395 Oct 11 17:47 malloc_test.c

3. VIRT의 malloc 테스트 실행 결과

  • malloc() 으로 메모리 영역을 요청한 후에 아무것도 하지 않음

  • top 명령으로 해당 프로세스의 변화 확인 → 시간이 지나면서 VIRT는 계속해서 높아지지만, RES는 늘어나지 않음

    # 위에서 생성한 malloc_test 실행파일 실행
    $ ./malloc_test
    Currently allocating 1 MB
    Currently allocating 2 MB
    Currently allocating 3 MB
    Currently allocating 4 MB
    Currently allocating 5 MB
    # [...생략...]
    
    # 다른 터미널로 해당 내용 확인
    $ top -b -n 1 | grep -i malloc
    65246 root      20   0   33000    348    276 S   0.0  0.0   0:00.00 malloc_test
    
    $ top -b -n 1 | grep -i malloc
    65246 root      20   0   40196    608    400 S   0.0  0.0   0:00.00 malloc_test
    
    $ top -b -n 1 | grep -i malloc
    65246 root      20   0   47392    608    400 S   0.0  0.0   0:00.00 malloc_test
    
    $ top -b -n 1 | grep -i malloc
    65246 root      20   0   55616    608    400 S   0.0  0.0   0:00.00 malloc_test



RES → task가 사용하고 있는 physical memory의 양을 의미

  • RES는 VIRT가 사용하고 있는 실제 물리 메모리의 크기
  • 일반적으로 RES는 VIRT보다 작은 값을 출력
  • RES는 실제로 메모리를 쓰고 있음
  • RES는 SWAP 값 제외
  • RES는 메모리 점유율이 높은 프로세스를 찾기 위해서는 RES 영역이 높은 프로세스를 찾아야함

1. 프로세스가 VIRT 할당 받은 메모리를 RES에 올려서 사용

  • 프로세스가 할당받은 메모리 영역에 실제로 쓰기 작업을 하면 Page fault가 발생
  • Page fault가 발생하면 커널은 실제 물리 메모리에 프로세스의 가상 메모리 공간을 매핑 → Page Table이라고 불리는 커널의 전역 변수로 관리
  • 물리 메모리에 바인딩된영역이 RES로 계산

2. RES의 malloc 테스트 → 이전의 malloc_test.c 파일을 수정

$ vi malloc_test.c
#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_test 파일을 malloc_test 실행 파일로 만듦
$ gcc -o malloc_test malloc_test.c
$ ls -al
-rwxr-xr-x  1 root root 8568 Oct 11 18:02 malloc_test
-rw-r--r--  1 root root  433 Oct 11 18:01 malloc_test.c

3. RES의 malloc 테스트 실행 결과

  • malloc()을 단독으로 요청하는 것이 아니라 할당 받은 메모리에 쓰기함 → RES 영역이 VIRT 영역 늘어나는 비율과 비슷하게 늘어남

  • 메모리 사용과 관련해서 VIRT 뿐만 아니라 실제 메모리를 쓰는 RES를 확인할 수 있음

  • top 명령으로 해당 프로세스의 변화 확인 → 시간이 지나면서 VIRT과 함께 RES가 계속해서 높아짐

    # 위에서 생성한 malloc_test 실행파일 실행
    $ ./malloc_test
    Currently allocating 1 MB
    Currently allocating 2 MB
    Currently allocating 3 MB
    Currently allocating 4 MB
    Currently allocating 5 MB
    # [...생략...]
    
    # 다른 터미널로 해당 내용 확인
    $ top -b -n 1 | grep -i malloc
    66652 root      20   0   21692  17772    400 S   0.0  0.0   0:00.01 malloc_test
    
    $ top -b -n 1 | grep -i malloc
    66652 root      20   0   35056  31236    400 S   0.0  0.0   0:00.04 malloc_test
    
    $ top -b -n 1 | grep -i malloc
    66652 root      20   0   45336  41532    400 S   0.0  0.0   0:00.05 malloc_test
    
    $ top -b -n 1 | grep -i malloc
    66652 root      20   0   57672  53940    400 S   0.0  0.0   0:00.07 malloc_test
    
    $ top -b -n 1 | grep -i malloc
    66652 root      20   0   62812  58956    400 S   0.0  0.0   0:00.08 malloc_test

※ Page Faults

  • 프로그램이 자신의 주소 공간에는 존재하지만 시스템의 RAM에는 현재 없는 데이터나 코드에 접근 시도하였을 경우 발생하는 현상
  • Page Faults 발생하면 운영 체제는 데이터를 메모리로 가져와서 Page Faults가 전혀 발생하지 않은 것처럼 프로그램이 계속적으로 작동하게 해줌

※ Page Table

  • 가상 주소와 물리 메모리 주소의 매핑 테이블
  • 프로세스마다 하나씩 존재하게 되며, 메인 메모리 (RAM)에 상주
  • 많은 프로세스가 구동될 수록, 페이지 테이블로 인한 메인 메모리 사용이 커짐을 의미


SHR → 다른 프로세스와 공유하고 있는 shared memory의 양을 의미

  • SHR는 공유하고 있는 메모리 크기
  • SHR 영역에 대표적으로 등록되는 내용은 라이브러리(library) → 대부분의 리눅스 프로세스는 glibc라는 라이브러리를 참고하기에 해당 라이브러리를 공유 메모리에 올려서 사용
  • 공유메모리에 올려놓고 사용하지 않고, 사용하는 프로세스마다 glibc의 내용을 메모리에 올려서 사용하는 것은 공간 낭비
  • 커널은 공유메모리라는 개념을 도입했고, 다수의 프로세스가 함께 사용하는 라이브러리는 공유메모리 영역에 올려서 함께 사용하도록 구현
  • VIRT는 실제는 할당되지 않는 가상공간이기 때문에 해당 값이 크다고 해도 문제가 되지 않음
  • 실제 사용하고 있는 메모리는 RES 영역이기 때문에 메모리 점유율이 높은 프로세스를 찾기 위해서는 RES 영역이 높은 프로세스를 찾아야함
  • 매핑된 라이브러리 전체는 VIRT와 SHR에 포함되어도, 실제 사용 중인 함수가 있는 일부 메모리만 RES에 포함
  • 커널은 메모리 낭비를 막기위해 공유 메모리라는 개념을 도입했고, 다수의 프로세스가 함께 사용하는 라이브러리는 공유 메모리 영역에 올려서 함께 사용하도록 구현

  • OOM Killer는 희생시킬 프로세스를 고르기 위해서 각 프로세스에 점수를 매기는 과정을 진행

  • OOM Killer는 메모리가 부족해졌을 때 프로세스 중 oom_score가 가장 높은 점수를 받은 순서대로 프로세스를 죽임

  • oom_score 점수는 기본적으로 프로세스에게 할당된 메모리의 양으로 점수가 메겨짐 → 해당 프로세스로부터 fork()된 자식 프로세스들의 메모리도 추가됨

  • 오래 돌고 있는 프로세스의 경우 oom_score 점수를 낮춰주고, 프로세스 순위가 낮은 프로세스는 oom_score 점수를 높여서 사용할 필요가 있음

  • 중요하게 취급되어지는 프로세스들(superuser에 의해서 실행되거나, 하드웨어와 직접적인 관련이 있는)은 값을 크게 낮춰줘서 OOM Killer를 통해서 kill되지 않도록 해야함

  • 각 서비스의 oom_score를 확인하는 방식 (PID → 프로세스 ID)

    # 기본 형식
    $ cat /proc/<pid>/oom_score
    oom_socre 출력
    
    # 19862 프로세스의 oom_score 확인
    $ cat /proc/19862/oom_score
    12

※ nice

  • 프로세스의 우선 순위를 낮춰주는 점수를 의미

oom_score_adj, oom_score, oom_adj 커널 파라미터

1. oom_score_adj

  • 앱이 포그라운드(foreground)에 있는지, 백그라운드(background)에 있는지로 결정
  • 정확한 값은 앱이 백그라운드 서비스를 가지고 있는지, 얼마나 오랫동안 포그라운드에 빠져 있는지에 따라 달라짐
  • oom_score_adj 는 -1000 ~ 1000 의 값을 가지며, 낮은 값 일수록 우선순위에서 밀려남
  • 프로세스가 OOM Killer 에 의해 죽지 않길 원한다면 oom_score 를 조정하는게 아니라 oom_score_adj 값을 변경해야함

2. oom_score

  • 기본적으로 프로세스에게 할당된 메모리의 양으로 점수가 메겨짐

3. oom_adj

  • -17 ~ 15 의 값을 가지며, 낮은 값 일수록 우선순위에서 밀려남
  • -17이면 OOM을 완전히 비활성화시킴
    /proc/<pid>/oom_adj
    /proc/<pid>/oom_score
    /proc/<pid>/oom_score_adj


OOM Killer의 대상에서 벗어나기

  • OOM Killer를 끄는 것은 불가능
  • 서비스의 안정성을 위해서 꺼지면 안 될 프로세스도 존재 → 특정한 작업을 하고 있던 worker 프로세스
  • OOM Killer를 직접적으로 끌 수는 없지만, OOM Killer의 scoring를 통해 OOM Killer가 멈추는 대상은 벗어날수 있음
  • /proc//oom_adj 값을 -17로 설정 → -17 값은 OOM_DISABLE의 상수 값
    $ echo -17 > /proc/<pid>/oom_adj
  • /proc/oom_scoring_adj 값을 -1000으로 설정
  • -1000 값으로 지정하는 것은 OOM Scoring을 비활성화한다기보다는 충분히 낮은 값을 주어서 OOM Killer에 벗어남
    $ echo -1000 > /proc/<pid>/oom_adj

관련해서 리눅스에서 실행(kill) 하는 순서

  1. alloc_pages
  2. out_of_memory
  3. select_bad_process
  4. oom_evaluate_task
  5. oom_badness

OOM Scoring과 관련된 함수들

1. select_bad_process

  • bad 프로세스를 선택

  • select_bad_process 함수 내용

    static void select_bad_process(struct oom_control *oc)
    {
      if (is_memcg_oom(oc))
          mem_cgroup_scan_tasks(oc->memcg, oom_evaluate_task, oc);
      else {
          struct task_struct *p;
    
            // RCU: Read-Copy Update
          rcu_read_lock();
    
          for_each_process(p)
              if (oom_evaluate_task(p, oc))
                  break;
          rcu_read_unlock();
      }
      oc->chosen_points = oc->chosen_points * 1000 / oc->totalpages;
    }

2. oom_evaluate_task

  • oom 선정된 프로세스가 OOM Killer를 통해 멈춰되 되는지 검증

  • oom_evaluate_task 함수 내용

    static int oom_evaluate_task(struct task_struct *task, void *arg)
    {
      struct oom_control *oc = arg;
      unsigned long points;
    
      if (oom_unkillable_task(task, NULL, oc->nodemask))
          goto next;
      /*
       * This task already has access to memory reserves and is being killed.
       * Don't allow any other task to have access to the reserves unless
       * the task has MMF_OOM_SKIP because chances that it would release
       * any memory is quite low.
       */
      if (!is_sysrq_oom(oc) && tsk_is_oom_victim(task)) {
          if (test_bit(MMF_OOM_SKIP, &task->signal->oom_mm->flags))
              goto next;
          goto abort;
      }
    
      /*
       * If task is allocating a lot of memory and has been marked to be
       * killed first if it triggers an oom, then select it.
       */
      if (oom_task_origin(task)) {
          points = ULONG_MAX;
          goto select;
      }
    
      points = oom_badness(task, NULL, oc->nodemask, oc->totalpages);
      if (!points || points < oc->chosen_points)
          goto next;
    
      /* Prefer thread group leaders for display purposes */
      if (points == oc->chosen_points && thread_group_leader(oc->chosen))
          goto next;
    select:
      if (oc->chosen)
          put_task_struct(oc->chosen);
      get_task_struct(task);
      oc->chosen = task;
      oc->chosen_points = points;
    next:
      return 0;
    abort:
      if (oc->chosen)
          put_task_struct(oc->chosen);
      oc->chosen = (void *)-1UL;
      return 1;
    }

3. oom_badness

  • kill 했을 때 가장 높은 메모리를 확보할 수 있는 task 인 경우, 높은 점수를 return 한다는 개념

  • oom_badness() 함수가 리턴해주는 점수가 가장 높은 task가 bad_process로 선정 되어 죽게 됨

  • oom_score_adj 라는 값을 oom_badness 에서 LONG_MIN 을 리턴하기 위해 사용 → LONG_MIN 을 리턴한다는 개념은 낮은 점수를 줘서 kill 하지 못하게 하겠다는 의미

  • RSS (프로세스가 사용하고 있는 물리 메모리) + 프로세스의 스왑 메모리 + (프로세스의 pagetable / page_size) 의 값이 프로세스 (task)의 점수 (point) 가 됨

  • 해당 코드만 보면 프로세스가 점유하고 있는 메모리가 클 경우 score가 높아진다고 이해 가능

  • oom_badness 함수 내용

    /**
     * oom_badness - heuristic function to determine which candidate task to kill
     * @p: task struct of which task we should calculate
     * @totalpages: total present RAM allowed for page allocation
     *
     * The heuristic for determining which task to kill is made to be as simple and
     * predictable as possible.  The goal is to return the highest value for the
     * task consuming the most memory to avoid subsequent oom failures.
     */
    long oom_badness(struct task_struct *p, unsigned long totalpages)
    {
        long points;
        long adj;
    
        if (oom_unkillable_task(p))
            return LONG_MIN;
    
        p = find_lock_task_mm(p);
        if (!p)
            return LONG_MIN;
    
        /*
         * Do not even consider tasks which are explicitly marked oom
         * unkillable or have been already oom reaped or the are in
         * the middle of vfork
         */
        adj = (long)p->signal->oom_score_adj;
        if (adj == OOM_SCORE_ADJ_MIN ||
                test_bit(MMF_OOM_SKIP, &p->mm->flags) ||
                in_vfork(p)) {
            task_unlock(p);
            return LONG_MIN;
        }
    
        /*
         * The baseline for the badness score is the proportion of RAM that each
         * task's rss, pagetable and swap space use.
         */
        points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
            mm_pgtables_bytes(p->mm) / PAGE_SIZE;
        task_unlock(p);
    
        /* Normalize to oom_score_adj units */
        adj *= totalpages / 1000;
        points += adj;
    
        return points;
    }

4. get_mm_rss 함수

  • get_mm_rss() 함수를 사용하여 여유 메모리(free memory)가 얼마나 되는지 추정
    static inline unsigned long get_mm_rss(struct mm_struct *mm)
    {
        return get_mm_counter(mm, MM_FILEPAGES) +
            get_mm_counter(mm, MM_ANONPAGES) +
            get_mm_counter(mm, MM_SHMEMPAGES);
    }

5. get_mm_counter 함수

  • get_mm_counter 함수를 통해 swap 메모리(swap memory)가 얼마나 되는지 추정

    static inline unsigned long get_mm_counter(struct mm_struct *mm, int member)
    {
        long val = atomic_long_read(&mm->rss_stat.count[member]);
    
        #ifdef SPLIT_RSS_COUNTING
        /*
         * counter is updated in asynchronous manner and may go to minus.
         * But it's never be expected number for users.
         */
        if (val < 0)
            val = 0;
        #endif
        return (unsigned long)val;
    }

+ Recent posts