Linux 2022. 11. 8. 14:29

Solaris 10 ps command 전체 출력

  • Solaris 10 (SunOS 5.1) 에서 겪은 ps 명령어에 관련된 메모이다.
  • ps 명령어를 통해 CMD를 보고 싶을 경우, 터미널의 글자 수 제한 설정 때문에 상세 내용을 전부 볼 수 없다.
  • OS 종류 및 버전에 따라 ps 지원 옵션이 다르기 때문에, 흔히 사용하는 -x , -ww 옵션을 사용할 수 없었다.

해결

  • 완벽한 해결책은 아니지만, CMD를 봐야 하는 상황에서 다음 명령어를 통해 ps 컬럼들을 볼 수 있다.
$ pargs <PID>

 

참고

 

'Linux' 카테고리의 다른 글

우분투 oh-my-zsh 설치  (0) 2021.04.23
VSCode Remote-SSH 플러그인 에러  (0) 2021.04.02
Vim  (0) 2021.03.08
Linux/Linux Structure 2021. 9. 26. 03:07

실습과 그림으로 배우는 리눅스 구조 학습 내용

커버 이미지

실습과 그림으로 배우는 리눅스 구조 - 타케우치 사토루


Contents

Chapter 1. 컴퓨터 시스템의 개요

Chapter 2. 사용자 모드로 구현되는 기능

Chapter 3. 프로세스 관리

Chapter 4. 프로세스 스케줄러

Chapter 5. 메모리 관리

Chapter 6. 메모리 계층

Chapter 7. 파일시스템

Chapter 8. 저장 장치


실습 환경

Surface Pro 7 - Microsoft

  • CPU : 인텔® 코어™ i5-1035G4 프로세서 (6M 캐시, 최대 3.70GHz)

    주요 정보
    제품 컬렉션 10세대 인텔® 코어™ i5 프로세서
    코드 이름 이전 제품명 Ice Lake
    수직 분야 Mobile
    프로세서 번호 i5-1035G4
    상태 Launched
    출시일 Q3'19
    리소그래피 10 nm
    사용 조건 PC/Client/Tablet
    고객 권장가격 $309.00
    CPU 사양
    코어 수 4
    스레드 수 8
    프로세서 기본 주파수 1.10 GHz
    최대 터보 주파수 3.70 GHz
    캐시 6 MB Intel® Smart Cache
    버스 속도 4 GT/s
    TDP 15 W
    구성 가능한 TDP-up 주파수 1.50 GHz
    구성 가능한 TDP-up 25 W
    구성 가능한 TDP-down 주파수 800 MHz
    구성 가능한 TDP-down 12 W
    보조 정보
    사용 가능한 임베디드 옵션 아니요
    데이터시트 지금 보기
    메모리 사양
    최대 메모리 크기(메모리 유형에 따라 다름) 64 GB
    메모리 유형 DDR4-3200, LPDDR4-3733
    최대 메모리 채널 수 2
    최대 메모리 대역폭 58.3 GB/s
    ECC 메모리 지원 ‡ 아니요
    프로세서 그래픽
    프로세서 그래픽 ‡ 인텔® Iris® Plus 그래픽
    그래픽 기본 주파수 300 MHz
    그래픽 최대 동적 주파수 1.05 GHz
    그래픽 출력 eDP/DP/HDMI
    최대 해상도(HDMI 1.4)‡ 4096 x 2304@60Hz
    최대 해상도(DP)‡ 5120 x 3200@60Hz
    최대 해상도(eDP - 통합 평판) 5120 x 3200@60Hz
    DirectX* 지원 12
    OpenGL* 지원 4.5
    인텔® 퀵 싱크 비디오
    지원되는 디스플레이 수 ‡ 3
    장치 ID 0x8A5A
    확장 옵션
    PCI Express 개정판 3
    패키지 사양
    소켓 지원 FCBGA1526
    최대 CPU 구성 1
    TJUNCTION 100°C
    패키지 크기 50mm x 25mm
    고급 기술
    인텔® 딥 러닝 부스트
    인텔® Optane™ 메모리 지원 ‡
    Intel® Speed Shift Technology
    인텔® Thermal Velocity Boost 아니요
    인텔® 터보 부스트 기술 ‡ 2
    인텔® v프로™ 플랫폼 적격성 ‡ 아니요
    인텔® 하이퍼 스레딩 기술 ‡
    인텔® 가상화 기술(VT-x) ‡
    직접 I/O를 위한 인텔® 가상화 기술(VT-d) ‡
    인텔® VT-x with Extended Page Tables(EPT) ‡
    인텔® TSX-NI 아니요
    인텔® 64 ‡
    명령 세트 64-bit
    명령 세트 확장 Intel® SSE4.1, Intel® SSE4.2, Intel® AVX2, Intel® AVX-512
    유휴 상태
    열 모니터링 기술
    인텔® 안정화 이미지 플랫폼 프로그램(SIPP) 아니요
    인텔® Adaptix™ 기술
    보안 및 신뢰성
    Intel® AES New Instructions
    보안 키
    Intel® Software Guard Extensions(Intel®SGX) Yes with Intel® ME
    인텔® OS 가드
    인텔® 신뢰 실행 기술 ‡ 아니요
    실행 불능 비트 ‡
    인텔® 부트 가드
  • RAM : 8GB

Ubuntu 20.04 (Host OS)


소스 코드

'Linux > Linux Structure' 카테고리의 다른 글

Chapter 8. 저장 장치  (2) 2021.09.26
Chapter 7. 파일시스템  (0) 2021.09.26
Chapter 6. 메모리 계층  (0) 2021.09.26
Chapter 5. 메모리 관리  (3) 2021.09.26
Chapter 4. 프로세스 스케줄러  (0) 2021.09.26
Linux/Linux Structure 2021. 9. 26. 03:06

Chapter 8. 저장 장치

HDD의 동작 방식

HDD는 플래터platter라고 불리는 자기 장치에 데이터를 자기 정보로 기록하는 저장 장치이다. 데이터는 섹터sector라는 단위(512바이트 or 4KiB)로 읽고 쓴다. 섹터는 동심원 모양으로 원의 중심부터 바깥 방향으로 분할되어 있다.

플래터의 섹터 데이터를 읽으려면, 먼저 스윙 암을 움직여서 자기 헤드를 플래터의 위로 이동시키고, 그 다음 플래터를 회전시켜 자기 헤드를 목적 섹터의 바로 위에 오도록 한다. HDD로의 데이터 전송 흐름은 다음과 같다.

  1. 디바이스 드라이버가 데이터를 HDD에 전달한다. (섹터 번호, 섹터 개수, 읽기 쓰기 등)

  2. 스윙 암을 이동시키고 플래터를 회전시켜 자기 헤드를 섹터 위에 위치시킨다.

  3. 데이터를 읽거나 쓴다.

2, 3번의 경우 기계적 처리이기 때문에, 다른 전기적 처리에 비해 레이턴시가 길다. 즉 레이턴시 중 대부분이 기계적 처리에 걸리는 시간이다.


HDD의 성능 특성

HDD는 연속된 여러 섹터 데이터를 읽을 때, 플래터를 회전하는 것만으로 한 번에 읽을 수 있다. 한 번에 읽을 수 있는 양은 HDD마다 제한이 있다. 이런 특성때문에 파일시스템은 각 파일의 데이터를 최대한 연속된 영역에 배치되도록 한다. 프로그램의 레이턴시를 줄이려면 다음 사항을 생각해야 한다.

  • 파일 내의 데이터를 연속(혹은 가깝게)으로 배치한다.

  • 연속된 영역에는 한 번에 접근한다. (여러번 나눠서 하면 레이턴시 증가)

  • 파일은 큰 사이즈로 시퀀셜sequential하게 접근한다.


HDD 테스트

  • 사용하지 않는 파티션에서 테스트할 것!!

  • umount 명령어로 마운트를 해제해야 테스트할 수 있었다. 마운트된 상태로 테스트하면 device or resource busy 에러가 발생한다.

HDD 테스트 프로그램

측정 내용

  • I/O 사이즈에 따른 성능 변화

  • 시퀀셜 접근 vs 랜덤 접근

테스트 프로그램 사양

  • 지정된 파티션의 처음부터 1GiB까지의 영역에 총 64MiB의 I/O를 요청한다.

  • 파라미터

    • 파일명

    • 커널의 I/O 지원 기능 사용 여부(on / off)

    • 읽기 쓰기(r / w)

    • 접근 패턴(seq / rand)

    • 1회당 I/O 사이즈[KiB]

빌드

아쉽게도 현재 사용 가능한 HDD가 없어서 테스트를 해보지는 못했다. 책 저자의 실험 결과를 바탕으로 정리했다. 직접 해볼 때는 4장에서처럼 log로 남긴 뒤 plot으로 그려보면 좋을 것 같다.

HDD 시퀀셜 접근 테스트

  • 커널의 I/O 지원 기능을 끈 상태로 시퀀셜 접근의 r, w 속도를 측정해본다.

    $ sudo su
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 off r seq $i ; done
    
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 off w seq $i ; done
  • 읽기 쓰기 모두 1회당 I/O 사이즈가 커질수록 스루풋 성능이 향상된다. 하지만 HDD가 한 번에 접근할 수 있는 데이터량의 한계 이상부터는 스루풋이 비슷한데, 이때의 스루풋이 이 HDD의 최대 성능이다.

HDD 랜덤 접근 테스트

  • 커널의 I/O 지원 기능을 끈 상태로 랜덤 접근의 r, w 속도를 측정해본다.

    $ sudo su
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 off r rand $i ; done
    
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 off w rand $i ; done
  • 랜덤 접근의 성능은 시퀀셜 접근보다 전체적으로 떨어진다. 특히 I/O 사이즈가 작을 때 랜덤 접근의 성능이 처참하고, I/O 사이즈가 커질수록 스루풋 성능이 올라간다. I/O 사이즈가 커지면 프로그램의 접근 대기 시간이 줄어들기 때문이다. 그래도 시퀀셜 접근에 비하면 느리다.

블록 장치 계층

리눅스에서는 HDD나 SSD처럼 랜덤 접근이 가능하며 일정 단위로 접근 가능한 장치를 블록 장치라고 분류한다. 이 블록 장치에는 디바이스 파일을 통해 직접 접근하거나 파일시스템을 통해 간접 접근할 수 있다. (대부분은 간접 접근)

블록 장치들에는 공통되는 처리가 많기 때문에, 이를 각 디바이스 드라이버에서 구현하지 않고 커널의 블록 장치 계층에서 처리하여 디바이스 드라이버로 넘긴다.

블록 장치 계층에는 I/O 스케줄러미리 읽기 기능이 있다.

I/O 스케줄러

I/O 스케줄러의 역할

  • 병합(merge) : 연속된 섹터에 대한 I/O 요청을 하나로 모은다.

  • 정렬(sort) : 불연속적인 섹터에 대한 I/O 요청을 섹터 번호 순서대로 정리한다.

I/O 스케줄러는 블록 장치에 대한 접근 요청을 일정 기간동안 모아서, 위의 가공을 한 뒤 디바이스 드라이버에 I/O 요청을 한다. 이러면 I/O 성능이 좋아진다. 정렬 후 병합하는 경우도 있다.

이 I/O 스케줄러 덕분에, 블록 장치의 성능 특성에 대해 자세히 알지 못한 채로 애플리케이션을 개발해도 어느 정도의 I/O 성능은 보장된다.

미리 읽기read-ahead

프로세스에서 데이터에 접근할 때 공간적 국소성Spatial locality이라는 특징이 존재하므로, 이를 이용한 미리 읽기라는 기능이 있다. 특정 영역에 I/O 요청을 하면, 그 바로 뒤의 연속된 영역을 미리 읽어두는 기능이다. 해당 요청 직후에 예측대로 연속된 영역에 접근하면, 이미 데이터 읽기가 되어 있으므로 저장 장치에서의 읽기를 생략할 수 있고, 이를 통해 성능이 향상된다. 예측대로 접근하지 않았을 경우 단순히 읽었던 데이터를 버린다. (명령어 파이프라인과 비슷한 느낌)

커널의 I/O 지원 기능 테스트 - HDD 시퀀셜 접근

  • 커널의 I/O 지원 기능을 켠 상태로 시퀀셜 접근의 r, w 속도를 측정해본다.

    $ sudo su
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 on r seq $i ; done
    
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 on w seq $i ; done
  • I/O 지원 기능을 껐을 때에 비해 스루풋 성능이 엄청나게 향상되었다. I/O 사이즈가 작을 때부터 거의 HDD의 한계까지 성능이 나오는데, 미리 읽기 덕분이다.

  • io 프로그램 실행 중에 다른 터미널로 iostat -x 명령어를 실행하면 미리 읽기에 대한 정보를 관찰할 수 있다.

    $ iostat -x -p 지정파티션 1 # 1초마다 관찰
    $ iostat -x -p nvme0n1p5 1
    Linux 5.13.13-surface (ubun2-Surface-Pro-7)     2021년 09월 25일     _x86_64_    (8 CPU)
    
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
              4.59    0.01    1.06    0.05    0.00   94.28
    
    Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   wrqm/s  %wrqm w_await wareq-sz     d/s     dkB/s   drqm/s  %drqm d_await dareq-sz  aqu-sz  %util
    nvme0n1p5        4.49    214.47     1.43  24.21    0.24    47.77    7.50    224.07     6.86  47.76    1.67    29.87    0.00      0.00     0.00   0.00    0.00     0.00    0.01   0.77
    • rkB/s 필드가 초당 읽은 용량을 의미한다.

    • 지원 기능을 끈 상태와 켠 상태 두 가지를 비교해보면, 껐을 때는 여러 번에 걸쳐 읽어오고 켰을 때는 한번에 많이 불러오는 것을 알 수 있다. 즉 미리 읽기를 통해 데이터를 메모리에 미리 불러와서 스루풋 성능을 높이는 것이다.

    • rrqm/s 필드가 읽기 처리에 대한 병합 처리량이다. 읽기에 I/O 스케줄러가 동작하는 경우는 여러 프로세스로부터 병렬로 읽기를 하거나, 비동기 I/O 등이므로, 이 테스트 프로그램에서는 병합 기능이 동작하지 않는다.

    • wrqm/s 필드가 쓰기 처리에 대한 병합 처리량이다. 자잘한 I/O 쓰기 요청을 병합하여 일정 사이즈 이상이 되면 HDD의 실제 I/O가 수행되어 쓰기 처리를 고속화한다.

커널의 I/O 지원 기능 테스트 - HDD 랜덤 접근

  • 커널의 I/O 지원 기능을 켠 상태로 랜덤 접근의 r, w 속도를 측정해본다.

    $ sudo su
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 on r rand $i ; done
    
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 on w rand $i ; done
  • 지원 기능을 켜도 I/O 사이즈가 작을 때는 스루풋 성능이 처참하다.

    • 읽기의 경우 미리 읽기를 해도 랜덤 접근으로 연속된 섹터가 아닌 다른 데이터에 대해 읽기 요청을 하므로 미리 읽기한 데이터가 버려지는 것이다.

    • 쓰기의 경우 I/O 사이즈가 작을 때 I/O 스케줄러의 효과가 있기는 하다. 즉 랜덤 접근 중에 우연히 접근 영역이 연속된 I/O 요청에 대해 병합이 발생한다.


SSD의 동작 방식

SSD는 HDD와 달리 전기적으로 데이터에 접근하기 때문에, 스윙 암이나 플래터 등 기계적인 장치가 움직이는 시간이 없다. 즉 전체적으로 성능이 HDD보다 훨씬 빠르며, 랜덤 접근 성능도 좋다.

이 테스트는 sd카드에서 해보려했으나, 쓰기 테스트에서 1분이 넘게 걸리는 것을 보고 제대로 측정이 안된다고 판단하여 책의 내용을 보고 정리하였다.


SSD 테스트

SSD 시퀀셜 접근 테스트

  • 커널의 I/O 지원 기능을 끈 상태로 시퀀셜 접근의 r, w 속도를 측정해본다.

    $ sudo su
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 off r seq $i ; done
    
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 off w seq $i ; done
  • HDD에 비하면 모두 몇 배는 빠르다.

SSD 랜덤 접근 테스트

  • 커널의 I/O 지원 기능을 끈 상태로 랜덤 접근의 r, w 속도를 측정해본다.

    $ sudo su
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 off r rand $i ; done
    
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 off w rand $i ; done
  • 랜덤 접근의 경우 SSD의 스루풋 성능이 HDD보다 훨씬 좋다. 시퀀셜 접근에 비해서는 성능이 낮으나, HDD에서의 경우보다는 그 차이가 적다. 또한 I/O 사이즈가 커지면 시퀀셜 접근과 랜덤 접근의 성능이 비슷해진다.

커널의 I/O 지원 기능 테스트 - SSD 시퀀셜 접근

  • 커널의 I/O 지원 기능을 켠 상태로 시퀀셜 접근의 r, w 속도를 측정해본다.

    $ sudo su
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 on r seq $i ; done
    
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 on w seq $i ; done
  • HDD의 경우와 마찬가지로 I/O 사이즈가 작을 때도 스루풋 성능이 한계에 접근한다. 읽기 성능은 미리 읽기의 효과이고, 쓰기 성능은 I/O 스케줄러가 수행하는 병합 처리의 효과이다.

  • 쓰기에서 I/O 사이즈를 크게 하면 I/O 지원 기능을 껐을 때보다 스루풋 성능이 낮아지는데, 이는 I/O 스케줄러의 오버헤드가 커지기 때문에 발생한다. HDD의 경우에는 기계적 처리의 소요 시간이 이 오버헤드보다 커서 발견하지 못했던 문제점인데, SSD는 전기적 처리로 인한 고속화로 이런 문제점이 보이게 된다.

커널의 I/O 지원 기능 테스트 - SSD 랜덤 접근

  • 커널의 I/O 지원 기능을 켠 상태로 랜덤 접근의 r, w 속도를 측정해본다.

    $ sudo su
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 on r rand $i ; done
    
    root# for i in 4 8 16 32 64 128 256 512 1024 2048 4096 ;
    for> do time output/io 지정파티션 on w rand $i ; done
  • 시퀀셜 접근에 비해서는 당연히 느리지만, 어느정도 I/O 사이즈가 커지면 시퀀셜 접근을 할 때와 성능이 비슷해진다.

  • I/O 지원 기능을 껐을 때와 비교하면, 읽기는 비슷하고(HDD에서의 이유와 같음 - 미리 읽기가 소용없어서) 쓰기는 오히려 성능이 떨어진다. 이는 I/O 스케줄러의 오버헤드를 무시할 수 없고, SSD에서는 정렬의 효과가 별로 없기 때문이다.


정리

사용자가 블록 장치의 원리에 대해 깊게 생각하지 않아도, 커널의 지원 기능 덕분에 어느 정도의 접근 최적화는 이루어진다. 하지만 모든 경우에서 최적의 성능이 나오는 것은 아니며, 바로 위에서처럼 SSD의 경우 특정 상황에서는 I/O 스케줄러의 오버헤드때문에 성능이 오히려 저하되는 경우가 발생할 수 있다.

앞에서도 적었듯 프로그램을 개발할 때에는 다음 사항을 최대한 지키는게 여러모로 좋다.

  • 파일 안의 데이터가 연속되거나 가깝도록 배치한다.

  • 연속된 영역에 대한 접근은 되도록 한 번에 처리한다.

  • 파일에는 최대한 큰 사이즈로 시퀀셜하게 접근한다.


참고


소스 코드

Linux/Linux Structure 2021. 9. 26. 03:05

Chapter 7. 파일시스템

리눅스의 파일시스템

리눅스의 파일시스템에서는 데이터를 저장하는 일반 파일이 있고, 디렉터리directory는 일반 파일이나 다른 디렉터리를 보관할 수 있다. 이는 트리 구조로 되어있다.

리눅스 파일시스템은 ext4, XFS, Btrfs 등 여러가지가 존재하고, 각 파일시스템은 다룰 수 있는 파일의 사이즈, 파일시스템 사이즈, 파일 작성이나 삭제 및 읽기 쓰기의 처리 속도 등이 모두 다르다. 하지만 다음 시스템 콜을 통해 통일된 인터페이스로 접근할 수 있다.

시스템 콜 동작
creat(), unlink() 파일 생성, 삭제
open(), close() 파일 열기, 닫기
read() 파일로부터 데이터 읽기
write() 파일에 데이터 쓰기
lseek() 파일의 특정 위치로 이동
ioctl() 파일시스템에 의존적인 특수한 처리

위의 시스템 콜이 호출되면 다음 순서로 파일의 데이터를 읽는다.

  1. 커널 내의 파일시스템 공통 처리가 동작하고, 대상 파일의 파일시스템을 판별한다.

  2. 판별한 파일시스템을 처리하는 프로세스를 호출하여 시스템 콜에 대응되는 처리를 한다.

  3. 데이터 읽기의 경우 디바이스 드라이버를 통해 데이터를 읽는다.


데이터data와 메타데이터metadata

  • 데이터 : 사용자가 작성한 문서 등의 데이터

  • 메타데이터 : 파일의 이름, 저장 장치 내의 위치, 사이즈 등의 보조 정보

메타데이터는 다음과 같은 요소도 포함한다.

  • 종류 : 데이터를 보관하는 일반 파일이나 디렉토리인지, 아니면 다른 종류인지에 대한 정보

  • 시간 정보 : 작성한 시간, 최종 수정 시간, 최종 접근 시간

  • 권한 정보 : 파일 접근 권한 관련 정보

df 명령어로 얻은 파일시스템의 스토리지 사용량은 메타데이터도 포함한다.

$ script/df_test.sh           
Filesystem     1K-blocks     Used Available Use% Mounted on
4,5c4,5
< /dev/nvme0n1p5  51343840 13559544  35146472  28% /
< tmpfs            3864512    77616   3786896   3% /dev/shm
---
> /dev/nvme0n1p5  51343840 13586916  35119100  28% /
> tmpfs            3864512    82436   3782076   3% /dev/shm
  • 안에 다른 파일이나 디렉터리가 없는 빈 디렉터리는 메타데이터로만 구성되어있다.

  • 메타데이터의 양이 증가함에 따라 Used 필드가 증가한 것을 볼 수 있다.


용량 제한 - 쿼터quota

시스템의 원활한 동작을 위해 파일시스템의 용량을 용도별로 제한하는 기능이 쿼터이다.

  • 사용자 쿼터 : 사용자별로 용량을 제한하여 /home이 가득 차는 것을 방지할 수 있다. ext4와 XFS에서 사용할 수 있다.

  • 디렉터리 쿼터(프로젝트 쿼터) : 특정 디렉터리별로 용량을 제한할 수 있다. ext4와 XFS에서 사용할 수 있다.

  • 서브 볼륨 쿼터 : 디렉터리 쿼터와 유사한 사용법으로, 파일시스템 내의 서브 볼륨이라는 단위별 용량을 제한할 수 있다. Btrfs에서 사용할 수 있다.


파일시스템 깨짐 방지

파일시스템에서 어떤 디렉터리 foo를 다른 디렉터리 bar 안으로 이동하는 등의 동작은 다음과 같다.

  1. foo에서 bar로 링크를 연결한다.

  2. bar의 부모 노드에서 bar로의 링크를 삭제한다.

이 처리는 중간에 누락되면 안되므로 아토믹atomic 처리라고 부른다.

만약 1을 완료한 상태에서 전원이 차단되는 등 처리가 중단되면, bar로의 링크가 2개이므로 파일시스템이 깨진 상태라고 할 수 있다. 이렇게 되면 파일시스템이 읽기 전용 모드로 다시 마운트remount하거나, 시스템 패닉이 발생할 수 있다.

이렇게 파일시스템이 깨지는 경우를 방지하기 위해 일반적으로 저널링(ext4, XFS)이나 Copy on Write(Btrfs) 방식을 사용한다.

저널링

저널링은 파일시스템 내부에 저널 영역이라는 메타데이터를 보관한다. 저널링 방식에서 업데이트 처리 순서는 다음과 같다.

  1. 저널로그 작성 : 아토믹한 처리의 목록을 저널 영역에 작성한다.

    ex. foo -> bar 링크 연결

  2. 저널로그를 바탕으로 실제 파일시스템의 데이터 변경

  3. 저널 영역 삭제

전원 차단 시나리오

  • 1을 완료한 뒤 전원이 차단될 경우 : 단순히 저널 영역의 데이터만 사라지고, 실제 데이터는 처리 전과 같은 상태이다.

  • 2 중간에 전원이 차단될 경우(앞에서처럼 bar 디렉터리에 2번 링크가 되는 경우 등) : 저널로그를 처음부터 다시 수행하여 처리를 완료한다.

파일시스템의 Copy On Write

일단 메모리에서의 COW와는 살짝 다른 개념인 듯 하다.

고전적인 파일시스템(ext4, XFS 등)은 한번 파일을 작성하면 그 파일의 배치 장소는 원칙적으로 변하지 않고, 업데이트할 때마다 같은 장소에 새로운 데이터를 쓴다. 반면에 Btrfs 등의 Copy On Write형의 파일시스템은 업데이트할 때 업데이트된 부분만 다른 장소에 데이터를 쓴다.

만약 아토믹으로 여러 처리를 실행할 경우, 업데이트되는 데이터들을 다른 장소에 전부 쓴 뒤 링크를 고쳐쓰는 방식으로 동작한다. 중간에 전원이 차단될 경우, 새로 작성한 데이터를 삭제하면 문제가 발생하지 않는다. 파일의 변경 기록은 사라지지만 파일시스템이 깨지는 것은 방지할 수 있다.

대책

만약 파일시스템이 깨졌다면, 일단 가장 좋은 방법은 정기적으로 백업하여 마지막 백업 상태로 복원하는 것이다. 이게 불가능하다면 각 파일시스템에 준비된 복구용 명령어를 사용할 수 있다.

  • ext4 : fsck.ext4

  • XFS : xfs_repair

  • Btrfs : btrfs check

하지만 위의 명령어는 소요 시간이 길고(파일시스템 전체를 조사하기 때문), 실패율이 높고, 처리하면서 깨진 데이터는 삭제하기 때문에 별로 추천하지 않는다고 한다.

결국 대책은 정기적인 백업이다..


파일의 종류

리눅스의 파일에는 일반 파일과 디렉터리, 그리고 디바이스 파일이 존재한다.

리눅스는 하드웨어상의 장치를 거의 파일로 표현한다(네트워크 어댑터는 예외). 즉, 장치를 파일과 똑같이 open(), read(), write() 등의 시스템 콜로 사용하며 장치 고유의 복잡한 조작을 위해서는 ioctl() 시스템 콜을 호출한다. 일반적으로 root만 디바이스 파일에 접근할 수 있다.

디바이스 파일은 캐릭터 장치와 블록 장치로 구분된다.

$ ls -l /dev  
...
brw-rw----   1 root  disk    259,     5  9월 25  2021 nvme0n1p5 # 블록 장치
crw-------   1 root  root     10,   144  9월 25  2021 nvram     # 캐릭터 장치
...
  • c로 시작하면 캐릭터 장치, b는 블록 장치이다.

  • 5번째 필드가 Major number, 6번째 필드가 Minor number이다.

캐릭터 장치

캐릭터 장치는 읽기와 쓰기가 가능하지만, 탐색seek이 되지 않는다.

  • ex. 터미널(bash 등의 셸), 키보드, 마우스

디바이스 파일로 터미널 조작

  • 터미널의 디바이스 파일은 다음 시스템 콜로 다룰 수 있다.

    • write() : 터미널에 데이터 출력

    • read() : 터미널에 데이터 입력

  • 현재 터미널의 디바이스 파일 찾기

    $ ps ax | grep zsh | grep -v grep
    55850 pts/0    Ss     0:00 zsh
    • 두 번째 필드를 통해 /dev/pts/0 이 디바이스 파일인 것을 알 수 있다.
  • 디바이스 파일에 문자열을 써서(내부적으로 write() 시스템 콜) 현재 터미널에 hello 출력해보기

    $ sudo su
    # echo hello > /dev/pts/0
    hello
  • 다른 터미널 조작해보기 : 터미널 2개 열고 다음을 실행

    기존 터미널

    $ ps ax | grep zsh | grep -v grep
    55850 pts/0    Ss+    0:00 zsh
    56385 pts/1    Ss     0:00 zsh
    $ sudo su
    # echo hello > /dev/pts/1

    새로 연 터미널

    $ hello

실제로 애플리케이션이 터미널의 디바이스 파일을 직접 조작하기 보다는, 리눅스가 제공하는 셸이나 라이브러리가 직접 디바이스 파일을 다루는 편이다. 애플리케이션은 셸이나 라이브러리가 제공하는 인터페이스를 쉽게 사용하는 편이다.

블록 장치

블록 장치는 파일의 읽기, 쓰기 및 랜덤 접근이 가능하다. 대표적으로 HDD나 SSD 등의 저장 장치가 있다.

일반적으로는 블록 장치에 직접 접근하지 않고, 파일시스템을 작성하고 마운트함으로써 파일시스템을 통해 접근한다. 하지만 직접 블록 장치를 다뤄야 하는, 다음과 같은 경우들도 존재한다.

  • 파티션 테이블 업데이트(parted 명령어 등)

  • 블록 장치 레벨의 데이터 백업, 복구(dd 명령어 등)

  • 파일 시스템 작성(mkfs 명령어)

  • 파일시스템 마운트(mount 명령어)

  • fsck

블록 장치를 직접 다루는 방법은 일반 파일과 똑같이 dd 명령어로 처리하면 된다. 우분투에서 파티션 크기를 축소하려면 usb 부팅으로 live mode에 들어가서 처리하면 되는데, 귀찮아서 실습은 생략...

책에서 실습한 방법은 다음과 같다.

  • mkfs.ext4 명령어로 파티션에 ext4 파일시스템을 생성한다.

  • mount 명령어로 생성한 파일시스템을 마운트하고, testfile을 만든 뒤 내용에 "hello world" 같은 문자열을 작성하고 unmount 명령어로 마운트를 해제한다.

  • string -t x 명령어를 사용하여 해당 파일시스템의 데이터가 들어있는 블록 장치에서 문자열 정보만을 추출하여 주소를 확인한다. hello world의 경우 0x803d000에 있었다.

  • 다음과 같이 dd 명령어로 블록 장치에 직접 접근하여 파일 내용을 변경할 수 있다.

    $ dd if=testfile-overwrite of=/dev/sdc7 seek=$((0x803d000)) bs=1

파일시스템의 내용을 블록 장치로부터 직접 변경할 때의 주의사항

  • ext4 외의 파일시스템이나 나중에 버전업된 ext4에서 똑같이 동작한다고 보장할 수 없다.

  • 파일시스템의 내용물을 직접 바꾸는 것은 굉장히 위험하므로 반드시 테스트용 파일시스템에서 실습한다.

  • 파일시스템을 마운트한 상태에서 해당 블록 장치에 동시에 접근하는 것은 불가능하다. (파일시스템이 깨질 위험 존재)


여러 가지 파일 시스템

리눅스에는 아직까지 언급한 ext4, XFS, Btrfs 외에도 여러 가지의 파일시스템이 있다.

메모리 기반 파일시스템 - tmpfs

tmpfs는 저장 장치 대신 메모리에 존재하여 고속으로 사용할 수 있다. 메모리에 존재하므로 전원이 차단되면 데이터가 사라지므로, 재부팅 후 남아있을 필요가 없는 /tmp/var/run에 사용하는 경우가 많다고 한다.

내가 사용하는 환경에서도 tmpfs를 사용하고 있었다.

$ mount | grep ^tmpfs
tmpfs on /run type tmpfs (rw,nosuid,nodev,noexec,relatime,size=772904k,mode=755,inode64)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,inode64)
tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k,inode64)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755,inode64)
tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,relatime,size=772900k,mode=700,uid=1000,gid=1000,inode64)
tmpfs on /run/snapd/ns type tmpfs (rw,nosuid,nodev,noexec,relatime,size=772904k,mode=755,inode64)

파일시스템 tmpfs는 마운트 할 때 작성하며, size 마운트 옵션으로 최대 크기를 지정한다. 이 사이즈를 바로 확보하는 건 아니고, 디맨드페이징처럼 파일시스템 내의 각 영역에 처음 접근할 때 페이지 단위로 메모리를 확보한다.

free의 출력 결과에서 shared 필드가 tmpfs에 의해 실제로 사용된 메모리의 양이다.

$ free
              total        used        free      shared  buff/cache   available
Mem:        7729028     2284288     2212032      936804     3232708     4219772
스왑:       2097148           0     2097148

네트워크 파일시스템

네트워크 파일시스템은 네트워크를 통해 연결된 원격 호스트에 있는 파일에 접근하는 파일시스템을 의미한다.

  • ex. 윈도우 기반 호스트에는 cifs, 리눅스 등 UNIX 계열의 호스트에는 nfs 파일시스템을 사용

가상 파일시스템

커널 안의 정보를 얻거나 커널의 동작을 변경하기 위해서 다양한 파일시스템이 존재한다.

procfs

시스템에 존재하는 프로세스에 대한 정보를 얻을 수 있다.

procfs는 일반적으로 /proc 이하에 마운트되고, /proc/PID/ 이하의 파일에 접근함으로써 각 프로세스의 정보를 얻을 수 있다.

  • 현재 셸에 관한 정보를 얻는 과정은 다음과 같다.

    $ ps | grep zsh
      58714 pts/0    00:00:00 zsh
    $ echo $$  # $$는 현재 프로세스의 PID이다.
    58714
    $ ls /proc/$$
    arch_status         cwd        mem            patch_state   stat
    attr                environ    mountinfo      personality   statm
    autogroup           exe        mounts         projid_map    status
    auxv                fd         mountstats     root          syscall
    cgroup              fdinfo     net            sched         task
    clear_refs          gid_map    ns             schedstat     timens_offsets
    cmdline             io         numa_maps      sessionid     timers
    comm                limits     oom_adj        setgroups     timerslack_ns
    coredump_filter     loginuid   oom_score      smaps         uid_map
    cpu_resctrl_groups  map_files  oom_score_adj  smaps_rollup  wchan
    cpuset              maps       pagemap        stack

procfs의 파일은 크게 다음과 같다. 자세한 사항은 man proc으로 알 수 있다.

파일 설명
/proc/PID/maps 프로세스의 메모리 맵
/proc/PID/cmdline 프로세스의 명령어 라인 파라미터
/proc/PID/stat 프로세스의 상태, 사용한 CPU 시간, 우선도, 사용 메모리의 양
/proc/cpuinfo 시스템에 탑재된 CPU에 대한 정보
/proc/diskstat 시스템에 탑재된 저장 장치에 대한 정보
/proc/meminfo 시스템에 탑재된 메모리에 대한 정보
/proc/sys 이하의 파일 커널의 각종 튜닝 파라미터. sysctl과 /etc/sysctl.conf로 변경하는 파라미터와 1:1로 대응된다.

sysfs

procfs에 잡다한 정보가 많아져서, 이러한 정보들을 배치하는 장소를 어느정도 정하기 위해 만들어졌다. sysfs는 보통 /sys 이하에 마운트된다.

파일 설명
/sys/devices 이하의 파일 시스템에 탑재된 디바이스에 대한 정보
/sys/fs 이하의 파일 시스템에 존재하는 각종 파일시스템에 대한 정보

cgroupfs

일반적으로 cgroupfs는 /sys/fs/cgroup 이하에 마운트된다.

cgroup

  • 프로세스들(1개 포함)로 만들어진 그룹에 대해, 리소스 사용량을 제한할 수 있다.

    • docker 등의 컨테이너 관리 소프트웨어, virt-manager 등의 가상 시스템 관리 소프트웨어에서 리소스를 제한하기 위해 사용한다.

    • 특히 하나의 시스템상에 여러 컨테이너나 가상 시스템이 공존하는 서버 시스템에 사용된다.

  • cgroup은 파일시스템 cgroupfs를 통해 다룬다.

  • root만 다룰 수 있다.

  • 제한 리소스

    • CPU : /sys/fs/cgroup/cpu 이하의 파일을 통해 그룹이 CPU의 전체 리소스 중 일정 비율까지만 사용하도록 제한을 걸 수 있다.

    • 메모리 : /sys/fs/cgroup/memory 이하의 파일을 통해 그룹이 물리 메모리 중 특정량까지만 사용하도록 제한을 걸 수 있다.


Btrfs

ext4나 XFS는 UNIX가 만들어졌을 때부터 필요했던 파일의 작성, 삭제, 읽고 쓰기 등의 단순한 기능만을 제공한다. 최근에 이 이상으로 풍부한 기능을 제공하는 파일시스템들이 나왔는데, 대표적으로 Btrfs가 있다.

멀티 볼륨

ext4나 XFS가 하나의 파티션에 대응하여 하나의 파일시스템을 만드는 것에 비해, Btrfs는 여러 저장 장치나 파티션으로부터 거대한 스토리지 풀을 만들고, 거기에 마운트가 가능한 서브 볼륨 영역을 작성한다. LVMLogical Volume Manager에 비교하면, 스토리지 풀은 LVM으로 구현된 볼륨 그룹이고 서브 볼륨은 LVM으로 구현된 논리 볼륨과 파일시스템을 더한 것과 비슷하다. 즉 Btrfs는 파일시스템 + LVM과 같은 볼륨 매니저라고 생각하면 편하다.

생성된 Btrfs은 마운트 중에도 운영을 멈추지 않고 저장 장치의 추가, 삭제, 교환 등의 작업을 할 수 있다.

스냅샷

스냅샷이란 시스템을 특정 상태로 되돌리기 위해, 해당 지점에서 마치 사진을 찍듯 파일들에 대한 정보를 저장하는 것을 말한다. Btrfs는 서브 볼륨 단위로 스냅샷을 만들 수 있다. 스냅샷 생성 시 데이터를 전부 복사하는게 아니라, 메타데이터를 작성하거나 스냅샷 내의 더티 페이지를 라이트 백write back(git처럼 변경 이력을 관리하여 돌아갈 수 있는 것 같다.) 하는 것만으로 처리할 수 있으므로 일반적인 복사보다 빠르다고 한다. 또한 원본 서브 볼륨과 데이터를 공유하므로 공간적인 낭비도 적다.

RAID

Btrfs는 파일시스템 레벨에 RAIDRedundant Array of Inexpensive Disks, 레이드 작성을 포함한다.

지원되는 RAID

  • RAID 0 : 패리티가 없는 data striping 구조

  • RAID 1 : 패리티가 없는 데이터 미러링 구조

  • RAID 10(RAID 1+0) : 먼저 디스크를 미러링(RAID 1)하고, 그 이후 스트리핑(RAID 0)한 구조 (최소 디스크 4개)

  • RAID 5 : 패리티가 배분되는(distributed) 스트리핑된 세트 (최소 디스크 3개)

  • RAID 6 : 패리티가 배분되는(distributed) 스트리핑된 세트 (최소 디스크 4개)

  • dup : 같은 데이터를 같은 저장 장치에 이중화..라는데 찾아봐도 잘 안나옴

데이터 파손 검출, 복구

Btrfs는 데이터와 메타데이터 모두 일정 크기마다 체크섬checksum을 가지고 있어서 데이터 파손을 검출할 수 있고, 따라서 데이터 파손을 모르는 채로 계속 운영하는 경우가 발생하는 경우가 적다.

  • 데이터나 메타데이터를 읽는 도중에 체크섬 에러를 검출하면, 그 데이터를 버리고 읽기를 요청한 프로그램에 I/O 에러를 알린다.

  • 이 때 RAID 1, 10, 5, 6, dup 구성일 경우 데이터 파손을 복구할 수 있다. 이 때 읽기를 요청한 프로그램에서는 데이터 파손을 알지 못하고(성능 저하 등으로는 알 수 있겠지만) 지나간다.


참고


소스 코드

Linux/Linux Structure 2021. 9. 26. 03:05

Chapter 6. 메모리 계층

캐시 메모리

컴퓨터의 기본적인 동작 흐름

  1. 메모리에서 레지스터로 데이터를 읽는다.

  2. 레지스터의 데이터로 계산한다.

  3. 계산 결과를 메모리에 쓴다.

요즘의 하드웨어는 레지스터에서 계산하는 시간(위에서 2)보다 메모리에 접근하는 데 걸리는 시간(1, 3)이 훨씬 느리다. 따라서 1, 3의 느린 속도때문에 병목 현상이 생겨서 전체적인 레이턴시가 증가한다(느려진다).

캐시 메모리는 레지스터와 메모리 사이에서 중간 역할을 하여 1, 3의 속도를 고속화한다. 일반적으로 CPU에 내장되어 있지만, CPU 바깥에 있는 캐시 메모리도 존재한다. 또한 일부 경우(특정 타이밍에 커널이 캐시를 파기하는 등)를 제외하면, 캐시 메모리의 처리는 하드웨어에서 처리된다.

캐시 메모리 동작 과정

  • 메모리에서 먼저 캐시 메모리로 캐시 라인 사이즈cache line size만큼(CPU에서 정함) 읽어오고, 이를 다시 레지스터로 읽어온다. 다음에 같은 메모리 주소에서 데이터를 읽을 경우, 캐시 메모리에서 읽으면 되므로 속도가 향상된다.

  • 만약 값을 변경하여 메모리에 입력해야 할 경우, 일단 레지스터로부터 변경된 데이터를 캐시 메모리에 옮긴다(이 때도 캐시 라인 사이즈 단위로). 이 떄 캐시 라인에, 메모리로부터 읽은 데이터가 변경되었음을 나타내는 더티dirty 플래그를 추가한다. 더티 플래그가 있으면 나중에 백그라운드 처리로 메모리에 기록하고 더티 플래그를 제거한다.

캐시 메모리가 가득 찬 경우

캐시 메모리가 꽉 찬 상태에서 캐시 메모리에 없는 메모리 주소를 읽으면, 기존의 캐시 메모리 중 1개를 파기한다. 만약 파기하는 데이터가 더티 상태이면, 해당 캐시 라인을 메모리에 동기화시킨 뒤 버린다. 더 나아가 캐시 메모리가 가득 차고 모두 더티 상태라면, 다른 메모리에 접근할 때마다 캐시 라인의 데이터가 자주 바뀌게 되고 이를 스래싱이라고 한다. 이러면 오히려 성능이 크게 감소하게 된다.

  • 즉 스래싱은 원래의 의도와 달리 오버헤드가 더 커서 성능이 오히려 낮아지는 현상이라고 할 수 있다.

계층형 캐시 메모리

최근 x86_64 아키텍처의 CPU는 캐시 메모리가 계층형 구조(L1, L2, L3, L은 Level)로 되어 있으며, 각 계층은 사이즈, 레이턴시, 논리 CPU 사이의 공유 관계 등 차이가 있다. 레지스터와 가장 가까운 L1 캐시가 제일 빠르며 용량은 적다.

캐시 메모리의 정보

  • /sys/devices/system/cpu/cpu0/cache/index0/ 디렉토리의 파일들이 캐시 메모리에 대한 정보를 가지고 있다.

    • cpu0과 index0의 숫자를 바꿔가며 각 논리 CPU 및 계층형 캐시에 대한 정보를 볼 수 있다.
  • 먼저 전체 캐시 메모리에 대한 정보를 얻어보자. 내 CPU에 대한 정보는 상위 디렉토리의 README.md 에 작성되어 있다.

    $ lscpu | grep cache                              
    L1d cache:                       192 KiB
    L1i cache:                       128 KiB
    L2 cache:                        2 MiB
    L3 cache:                        6 MiB
    • L1d는 데이터를 위한 L1 캐시 메모리이고, L1i는 명령어(intstruction)를 위한 L1 캐시 메모리 공간이다.

    • 위에서 설명한 대로 L1쪽의 크기가 작은 것을 알 수 있다.

  • 이제 논리 CPU 0번의 index0 캐시 메모리에 대한 정보를 파악해보자.

    $ head /sys/devices/system/cpu/cpu0/cache/index0/*
    ==> /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size <==
    64 # 캐시 라인 사이즈
    
    ==> /sys/devices/system/cpu/cpu0/cache/index0/id <==
    0 # 캐시메모리 자체의 id. 내 경우 4코어 8스레드이고 2스레드씩 캐시를 공유하여 0~3의 값을 갖는 것을 확인했다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index0/level <==
    1 # Level 1 캐시임을 의미
    
    ==> /sys/devices/system/cpu/cpu0/cache/index0/number_of_sets <==
    64
    
    ==> /sys/devices/system/cpu/cpu0/cache/index0/physical_line_partition <==
    1
    
    ==> /sys/devices/system/cpu/cpu0/cache/index0/shared_cpu_list <==
    0,4 # 이 캐시 메모리를 공유하는 논리 CPU의 번호
    
    ==> /sys/devices/system/cpu/cpu0/cache/index0/shared_cpu_map <==
    11
    
    ==> /sys/devices/system/cpu/cpu0/cache/index0/size <==
    48K # 이 캐시 메모리의 사이즈. 총 4캐의 캐시 메모리이므로 4를 곱하면 총 L1d 캐시의 사이즈는 192KiB임을 알 수 있다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index0/type <==
    Data # 데이터용 캐시 메모리. 즉 이 캐시 메모리는 L1d 캐시이다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index0/uevent <==
    
    ==> /sys/devices/system/cpu/cpu0/cache/index0/ways_of_associativity <==
    12
  • 다음은 cpu0의 index1이다.

    $ head /sys/devices/system/cpu/cpu0/cache/index1/*
    ==> /sys/devices/system/cpu/cpu0/cache/index1/coherency_line_size <==
    64
    
    ==> /sys/devices/system/cpu/cpu0/cache/index1/id <==
    0
    
    ==> /sys/devices/system/cpu/cpu0/cache/index1/level <==
    1 # 위의 캐시와 같은 L1 캐시이다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index1/number_of_sets <==
    64
    
    ==> /sys/devices/system/cpu/cpu0/cache/index1/physical_line_partition <==
    1
    
    ==> /sys/devices/system/cpu/cpu0/cache/index1/shared_cpu_list <==
    0,4
    
    ==> /sys/devices/system/cpu/cpu0/cache/index1/shared_cpu_map <==
    11
    
    ==> /sys/devices/system/cpu/cpu0/cache/index1/size <==
    32K # 위와 다르게 캐시 당 32KiB이며, 4를 곱하면 총 L1i 캐시의 사이즈는 128KiB이다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index1/type <==
    Instruction # 이번에는 명령어용 캐시, 즉 L1i 캐시임을 알 수 있다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index1/uevent <==
    
    ==> /sys/devices/system/cpu/cpu0/cache/index1/ways_of_associativity <==
    8
  • cpu0의 index2

    $ head /sys/devices/system/cpu/cpu0/cache/index2/*
    ==> /sys/devices/system/cpu/cpu0/cache/index2/coherency_line_size <==
    64
    
    ==> /sys/devices/system/cpu/cpu0/cache/index2/id <==
    0
    
    ==> /sys/devices/system/cpu/cpu0/cache/index2/level <==
    2 # 이번에는 L2 캐시이다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index2/number_of_sets <==
    1024
    
    ==> /sys/devices/system/cpu/cpu0/cache/index2/physical_line_partition <==
    1
    
    ==> /sys/devices/system/cpu/cpu0/cache/index2/shared_cpu_list <==
    0,4
    
    ==> /sys/devices/system/cpu/cpu0/cache/index2/shared_cpu_map <==
    11
    
    ==> /sys/devices/system/cpu/cpu0/cache/index2/size <==
    512K # 사이즈가 꽤 커진 것을 볼 수 있다. 4를 곱하면 총 L2 캐시의 사이즈는 2MiB이다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index2/type <==
    Unified # 이번에는 데이터나 명령어가 아닌 Unified, 즉 데이터와 명령어 전부를 저장할 수 있는 영역임을 의미한다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index2/uevent <==
    
    ==> /sys/devices/system/cpu/cpu0/cache/index2/ways_of_associativity <==
    8
  • cpu0의 마지막(1035G4는 L3캐시까지 있으므로) index3 캐시이다.

    $ head /sys/devices/system/cpu/cpu0/cache/index3/*
    ==> /sys/devices/system/cpu/cpu0/cache/index3/coherency_line_size <==
    64
    
    ==> /sys/devices/system/cpu/cpu0/cache/index3/id <==
    0
    
    ==> /sys/devices/system/cpu/cpu0/cache/index3/level <==
    3 # L3 캐시이다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index3/number_of_sets <==
    8192
    
    ==> /sys/devices/system/cpu/cpu0/cache/index3/physical_line_partition <==
    1
    
    ==> /sys/devices/system/cpu/cpu0/cache/index3/shared_cpu_list <==
    0-7 # 이번에는 논리 CPU 0부터 7까지 전부 공유한다. 즉 L3 캐시는 1개밖에 없다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index3/shared_cpu_map <==
    ff
    
    ==> /sys/devices/system/cpu/cpu0/cache/index3/size <==
    6144K # L3 캐시는 약 6MiB이다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index3/type <==
    Unified # 역시 데이터와 명령어 모두 저장할 수 있다.
    
    ==> /sys/devices/system/cpu/cpu0/cache/index3/uevent <==
    
    ==> /sys/devices/system/cpu/cpu0/cache/index3/ways_of_associativity <==
    12
  • 이번에는 cpu1의 index0을 보자.

    $ head /sys/devices/system/cpu/cpu1/cache/index0/*
    ==> /sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size <==
    64
    
    ==> /sys/devices/system/cpu/cpu1/cache/index0/id <==
    1 # 이번에는 캐시 메모리의 id가 1이다.
    
    ==> /sys/devices/system/cpu/cpu1/cache/index0/level <==
    1
    
    ==> /sys/devices/system/cpu/cpu1/cache/index0/number_of_sets <==
    64
    
    ==> /sys/devices/system/cpu/cpu1/cache/index0/physical_line_partition <==
    1
    
    ==> /sys/devices/system/cpu/cpu1/cache/index0/shared_cpu_list <==
    1,5 # 공유하는 논리 CPU가 1번과 5번임을 알 수 있다.
    
    ==> /sys/devices/system/cpu/cpu1/cache/index0/shared_cpu_map <==
    22
    
    ==> /sys/devices/system/cpu/cpu1/cache/index0/size <==
    48K
    
    ==> /sys/devices/system/cpu/cpu1/cache/index0/type <==
    Data
    
    ==> /sys/devices/system/cpu/cpu1/cache/index0/uevent <==
    
    ==> /sys/devices/system/cpu/cpu1/cache/index0/ways_of_associativity <==
    12
  • cpu7의 index0은 다음과 같다.

    $ head /sys/devices/system/cpu/cpu7/cache/index0/*
    ==> /sys/devices/system/cpu/cpu7/cache/index0/coherency_line_size <==
    64
    
    ==> /sys/devices/system/cpu/cpu7/cache/index0/id <==
    3 # L1d 캐시 메모리는 4개이므로 3번인 것을 확인할 수 있다.
    
    ==> /sys/devices/system/cpu/cpu7/cache/index0/level <==
    1
    
    ==> /sys/devices/system/cpu/cpu7/cache/index0/number_of_sets <==
    64
    
    ==> /sys/devices/system/cpu/cpu7/cache/index0/physical_line_partition <==
    1
    
    ==> /sys/devices/system/cpu/cpu7/cache/index0/shared_cpu_list <==
    3,7 # 논리 CPU 3번과 7번이 공유한다.
    
    ==> /sys/devices/system/cpu/cpu7/cache/index0/shared_cpu_map <==
    88
    
    ==> /sys/devices/system/cpu/cpu7/cache/index0/size <==
    48K
    
    ==> /sys/devices/system/cpu/cpu7/cache/index0/type <==
    Data
    
    ==> /sys/devices/system/cpu/cpu7/cache/index0/uevent <==
    
    ==> /sys/devices/system/cpu/cpu7/cache/index0/ways_of_associativity <==
    12
  • 4장에서 논리 CPU 0번과 4번으로 테스트했었는데, 이 책 저자의 경우 L3 캐시가 논리 CPU 0-3, 4-7 이 공유 상태였고, L1과 L2 캐시는 논리 CPU 별로 각각 존재했다. 내 경우에는 L1, L2캐시가 각각 논리 CPU 0,4 1,5 2,6 3,7 에서 공유하므로.. 4장의 테스트를 나중에 다시 해봐야 할 듯 하다ㅠ

캐시 실험

  • 최적화를 해야 결과를 더 눈에 띄게 알 수 있다고 하니, 컴파일 옵션에 O3 최적화를 추가한다.

    $ gcc -O3 -o output/cache src/cache.c
    $ script/cache_test.sh

    log/cache.log

    4[KB] : 0.420382
    8[KB] : 0.367063
    16[KB] : 0.343080
    32[KB] : 0.325464
    64[KB] : 0.886360     # 여기
    128[KB] : 0.889040
    256[KB] : 0.892584
    512[KB] : 1.106641
    1024[KB] : 1.495455   # 여기
    2048[KB] : 1.510899
    4096[KB] : 1.661219
    8192[KB] : 2.780471   # 여기
    16384[KB] : 3.925238
    32768[KB] : 4.266433
    • 정밀하게 보기는 어렵지만, 접근 시간이 확 뛰는 구간이 캐시 메모리들의 용량과 관련이 있음을 대략적으로 알 수 있다.

    • 32KB 이하에서 성능이 더 좋은 것은 프로그램의 정밀도 문제이므로 신경쓰지 않아도 된다. 어셈블리어로 코드를 작성하면 해결할 수 있다.

메모리 참조의 국소성locality of reference

  • 시간 국소성 : 한 번 접근한 데이터는 가까운 미래에 다시 접근할 가능성이 크다.

    ex. 반복문의 코드 등

  • 공간 국소성 : 어떤 데이터에 접근하면 그 데이터와 가까운 주소에 있는 데이터에 접근할 가능성이 크다.

    ex. 배열 등

위와 같은 이유로, 캐시는 프로세스가 요구한 것보다 더 많은 데이터를 메모리에서 가져와놓고 cache hit를 높인다.

정리

프로그램의 성능을 향상시키는 방법 중 하나는, 워크로드를 캐시 메모리 사이즈에 들어가게 하는 것이다. 데이터 배열이나 알고리즘 등을 통해 단위 시간 당 메모리 접근 범위를 작게 하면 도움이 된다.

  • 예를 들어 행렬을 이용한 레이캐스팅 엔진에서 x, y 좌표를 나타내는 2차원 배열에 이중반복문을 돌릴 때, 어떤 순서로 반복하느냐가 locality에 영향을 미친다.

또한 시스템 설정을 변경했을 때 같은 프로그램의 성능이 크게 나빠진 경우, 프로그램의 데이터가 캐시 메모리에 전부 들어가지 않았을 가능성이 존재한다.


Translation Lookaside BufferTLB

CPU에 존재하는 가상 주소 변환 고속화 장치이다.

TLB가 없다면, 프로세스가 가상 주소의 데이터에 접근하는 순서는 다음과 같다.

  1. 물리 메모리상에 존재하는 페이지 테이블을 참고하여 가상 주소를 물리 주소로 변환한다.

  2. 1에서 구한 물리 메모리에 접근한다.

이 때 1에서 물리 메모리상에 있는 페이지 테이블에 접근해야 하고, 이는 캐시 메모리로 고속화할 수 없다. 따라서 가상 주소에서 물리 주소로의 변환표를 CPU에 저장하는데, 캐시 메모리처럼 고속으로 접근 가능하도록 만든 것이 TLB이다.


페이지 캐시

CPU에서 저장 장치에 접근하려면 굉장히 느리기 때문에 커널은 페이지 캐시 기능을 사용한다. 페이지 캐시는 캐시 메모리의 동작과 매우 비슷하다.

  • 캐시 메모리가 메모리의 데이터를 캐시 메모리에 캐싱하는 것처럼, 페이지 캐시는 저장 장치 내의 파일 데이터를 메모리에 캐싱한다.

  • 캐시 메모리가 캐시 라인 단위로 데이터를 다루는 것처럼, 페이지 캐시는 페이지 단위로 데이터를 다룬다.

  • 시스템의 메모리가 허용하는 한, 각 프로세스가 페이지 캐시에 없는 파일을 읽을 때마다 페이지 캐시 사이즈가 점점 증가한다.

    • 만약 시스템 메모리가 부족해지면, 커널이 페이지 캐시를 해제한다.

      • 더티 플래그가 없는 페이지부터 해제하며, 그래도 부족하면 더티 페이지를 라이트 백write back한 뒤 해제한다. 이 때 저장 장치에 접근하므로 성능 저하가 발생할 수 있다.

읽기

프로세스가 파일을 읽으면, 커널 메모리 내의 페이지 캐시 영역에 데이터를 복사한 뒤 이것을 프로세스 메모리에 복사한다.

  • 또한 커널 메모리에는 페이지 캐시에 캐싱한 파일과 범위 등의 정보를 저장한다.

  • 페이지 캐시는 전체 프로세스의 공유 자원이므로, 여러 프로세스에서 같은 파일을 읽는 경우에도 속도가 향상될 수 있다.

쓰기

프로세스가 파일에 쓰기 작업을 하면, 커널은 일단 페이지 캐시에 데이터를 쓴다. 이 때 캐시 메모리 동작과정과 같이 더티 플래그를 써넣고, 이 플래그가 있는 페이지를 더티 페이지dirty page라고 부른다.

  • 더티 페이지 역시 나중에 커널의 백그라운드 처리로 저장 장치에 반영되며, 저장 후 더티 플래그를 지운다.

쓰기 동기화

  • 더티 페이지가 존재하는 상태로 시스템이 꺼지면 변경된 데이터가 사라져버린다.

  • 따라서 중요한 파일을 열 때는, open() 시스템 콜에서 O_SYNC 플래그를 사용하여 수정할 때마다 동기화하여 저장 장치에도 반영하도록 한다. (write through)

버퍼 캐시buffer cache (7장)

파일시스템을 사용하지 않고 디바이스 파일을 통해 저장 장치에 직접 접근하는 방식이다.

페이지 캐시와 버퍼 캐시를 합쳐서 저장 장치 안의 데이터를 메모리에 넣어둔다.

파일 읽기 테스트

1GiB 용량을 가지는 testfile을 생성한다.

$ dd if=/dev/zero of=testfile oflag=direct bs=1M count=1K
1024+0 레코드 들어옴
1024+0 레코드 나감
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 2.54515 s, 422 MB/s
$ ls -al
합계 1048624
drwxrwxr-x 6 ubun2 ubun2       4096  9월 22 23:51 .
drwxrwxr-x 9 ubun2 ubun2       4096  9월 18 17:18 ..
-rw-rw-r-- 1 ubun2 ubun2      16413  9월 22 23:50 README.md
drwxrwxr-x 2 ubun2 ubun2       4096  9월 18 22:08 log
drwxrwxr-x 2 ubun2 ubun2       4096  9월 18 22:06 output
drwxrwxr-x 2 ubun2 ubun2       4096  9월 22 23:50 script
drwxrwxr-x 2 ubun2 ubun2       4096  9월 18 18:33 src
-rw-rw-r-- 1 ubun2 ubun2 1073741824  9월 22 23:51 testfile
  • oflag=direct는 쓰기에 페이지 캐시를 사용하지 않는 옵션이다. 이 옵션이 없으면 파일을 생성하면서 페이지 캐시에 영향을 주는 듯

이 시점에서 testfile 파일의 페이지 캐시는 없다. 이 상태에서 testfile을 읽는데 걸리는 시간과 메모리 사용량을 측정하면 다음과 같다.

$ free 
              total        used        free      shared  buff/cache   available
Mem:        7729028     1916920     3006032      710884     2806076     4818860
스왑:       2097148           0     2097148
$ time cat testfile > /dev/null
cat testfile > /dev/null  0.00s user 0.50s system 44% cpu 1.117 total
$ free
              total        used        free      shared  buff/cache   available
Mem:        7729028     1913976     1956100      713068     3858952     4818608
스왑:       2097148           0     2097148
  • 약 1.117초가 걸렸으며, 커널은 0.5초만을 사용했다. 즉 나머지 0.617초는 저장 장치로의 접근 시간이다.

  • 전후의 buff/cache 필드를 살펴보면 페이지 캐시때문에 약 1GiB정도 증가한 것을 볼 수 있다.

파일을 다시 읽으면 다음과 같다.

$ time cat testfile > /dev/null
cat testfile > /dev/null  0.00s user 0.13s system 99% cpu 0.134 total
$ free
              total        used        free      shared  buff/cache   available
Mem:        7729028     1946052     1872112      768788     3910864     4730752
스왑:       2097148           0     2097148
  • 시간이 훨씬 줄어든 것을 볼 수 있다.

  • 페이지 캐시는 아까에 비해 별 변화가 없다.

또한 페이지 캐시 용량은 다음 명령어의 kbcached 필드로 확인할 수 있다. (KiB 단위)

$ sar -r 1
Linux 5.13.13-surface (ubun2-Surface-Pro-7)     2021년 09월 22일     _x86_64_(8 CPU)

23시 57분 55초 kbmemfree   kbavail kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
23시 57분 56초   1909668   4768680   1850712     23.94     80212   3668120   9387676     95.54   1929640   2868344       100
$ rm testfile

시스템 통계 정보 확인

  • 위에서 했던 작업을 셸 스크립트 파일로 실행하는 동시에, sar -B 명령어로 페이지에 대한 정보를 관찰한다.

    $ sar -B 1
    Linux 5.13.13-surface (ubun2-Surface-Pro-7)     2021년 09월 23일     _x86_64_(8 CPU)
    
    19시 58분 34초  pgpgin/s pgpgout/s   fault/s  majflt/s  pgfree/s pgscank/s pgscand/s pgsteal/s    %vmeff
    19시 58분 35초      0.00      0.00   2977.00      0.00  32024.00      0.00      0.00      0.00      0.00
    19시 58분 36초     80.00 214016.00   9429.00      1.00   4048.00      0.00      0.00      0.00      0.00
    19시 58분 37초      0.00 425984.00   6482.00      0.00  15104.00      0.00      0.00      0.00      0.00
    19시 58분 38초      0.00 408708.00   2695.00      0.00   4759.00      0.00      0.00      0.00      0.00
    19시 58분 39초      0.00      0.00     22.00      0.00   4895.00      0.00      0.00      0.00      0.00
    19시 58분 40초      0.00      0.00     39.00      0.00   1038.00      0.00      0.00      0.00      0.00
    19시 58분 41초  18688.00      0.00    324.00      0.00    631.00      0.00      0.00      0.00      0.00
    19시 58분 42초 1029888.00      0.00    419.00      0.00  11895.00      0.00      0.00      0.00      0.00
    19시 58분 43초      0.00     48.00     57.00      0.00   1000.00      0.00      0.00      0.00      0.00
    19시 58분 44초      0.00      0.00      0.00      0.00    322.00      0.00      0.00      0.00      0.00
    19시 58분 45초      0.00      0.00    303.00      0.00    206.00      0.00      0.00      0.00      0.00
    19시 58분 46초      0.00      0.00  12903.00      0.00 268976.00      0.00      0.00      0.00      0.00
    $ script/read-twice.sh 
    2021. 09. 23. (목) 19:58:36 KST: start file creation
    1024+0 레코드 들어옴
    1024+0 레코드 나감
    1073741824 bytes (1.1 GB, 1.0 GiB) copied, 2.47484 s, 434 MB/s
    2021. 09. 23. (목) 19:58:38 KST: end file creation
    2021. 09. 23. (목) 19:58:38 KST: sleep 3 seconds
    2021. 09. 23. (목) 19:58:41 KST: start 1st read
    2021. 09. 23. (목) 19:58:42 KST: end 1st read
    2021. 09. 23. (목) 19:58:42 KST: sleep 3 seconds
    2021. 09. 23. (목) 19:58:45 KST: start 2nd read
    2021. 09. 23. (목) 19:58:45 KST: end 2nd read
    • 파일을 생성하는 36초~38초 동안, 페이지 아웃이 1GiB정도 발생한 것을 볼 수 있다. 이는 페이지 캐시를 사용하지 않을 때에도 저장 장치에 파일의 데이터를 쓸 때 페이지 아웃으로 계산하기 때문이다.

    • 41~42초동안 1GiB의 페이지 인이 발생하는 것을 볼 수 있다. testfile의 내용을 페이지 캐시로 불러온 것이다.

    • 45초(두 번째 읽기)에서는 페이지 인이 발생하지 않은 것을 볼 수 있다.

  • 이번에는 sar -d -p 명령어로 저장 장치에 발생한 I/O의 양을 확인해보자.

    • 일단 mount 명령어로 루트 파일시스템이 존재하는 저장 장치의 이름을 확인한다.

      $ mount | grep "on / "
      /dev/nvme0n1p5 on / type ext4 (rw,relatime,errors=remount-ro)
    • sar -d -p 명령어의 출력 필드는 다음과 같다.

      $ sar -d -p 1
      Linux 5.13.13-surface (ubun2-Surface-Pro-7)     2021년 09월 23일     _x86_64_(8 CPU)
      
      20시 09분 42초       DEV       tps     rkB/s     wkB/s     dkB/s   areq-sz    aqu-sz     await     %util
      20시 09분 43초     loop0      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초     loop1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초     loop2      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초     loop3      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초     loop4      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초     loop5      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초     loop6      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초     loop7      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초   nvme0n1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초     loop8      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초     loop9      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초    loop10      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초    loop11      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초       sda      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초    loop12      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 09분 43초    loop13      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      • rkB/s와 wkB/s가 저장 장치에 대해 읽고 쓴 데이터의 양이다.

      • %util 필드는 측정 시간(1초) 동안 저장 장치에 접근한 시간의 퍼센트량이다.

    • 스크립트 파일을 실행하면서 sar -d -p 명령어로 저장 장치의 I/O 발생량을 확인한다.

      $ sar -d -p 1 | grep nvme0n1
      20시 10분 37초   nvme0n1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 10분 38초   nvme0n1   1094.00      0.00 280064.00      0.00    256.00      1.46      1.34     66.80
      20시 10분 39초   nvme0n1    808.00      0.00 206848.00      0.00    256.00      2.27      2.80    100.00
      20시 10분 40초   nvme0n1    618.00      0.00 158208.00      0.00    256.00      2.34      3.78    100.00
      20시 10분 41초   nvme0n1    680.00      0.00 173592.00      0.00    255.28      2.37      3.46    100.00
      20시 10분 42초   nvme0n1    675.00      0.00 172800.00      0.00    256.00      2.34      3.47    100.00
      20시 10분 43초   nvme0n1    224.00      0.00  57344.00      0.00    256.00      0.80      3.56     34.00
      20시 10분 44초   nvme0n1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 10분 45초   nvme0n1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 10분 46초   nvme0n1   2692.00 672000.00    976.00      0.00    249.99      1.05      0.39     65.60
      20시 10분 47초   nvme0n1   1471.00 376576.00      0.00      0.00    256.00      0.59      0.40     38.80
      20시 10분 48초   nvme0n1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 10분 49초   nvme0n1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 10분 50초   nvme0n1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 10분 51초   nvme0n1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      $ script/read-twice.sh
      2021. 09. 23. (목) 20:10:37 KST: start file creation
      1024+0 레코드 들어옴
      1024+0 레코드 나감
      1073741824 bytes (1.1 GB, 1.0 GiB) copied, 5.00482 s, 215 MB/s
      2021. 09. 23. (목) 20:10:42 KST: end file creation
      2021. 09. 23. (목) 20:10:42 KST: sleep 3 seconds
      2021. 09. 23. (목) 20:10:45 KST: start 1st read
      2021. 09. 23. (목) 20:10:46 KST: end 1st read
      2021. 09. 23. (목) 20:10:46 KST: sleep 3 seconds
      2021. 09. 23. (목) 20:10:49 KST: start 2nd read
      2021. 09. 23. (목) 20:10:49 KST: end 2nd read
      • 일단 시간 측정의 오차로 1초씩 밀린 것 같다.

      • 38초~43초동안 파일을 생성하며 1GiB의 쓰기가 발생한 것을 볼 수 있다.

      • 46~47초동안 1GiB의 I/O 읽기가 발생한 것을 볼 수 있다.

      • 49초에는 저장 장치에서 읽기 작업이 일어나지 않은 것을 알 수 있다.

파일 쓰기 테스트

위에서와 같이 1GiB의 testfile을 생성할 때, 페이지 캐시 사용 유무에 따른 소요 시간의 차이를 관찰한다.

$ time dd if=/dev/zero of=testfile oflag=direct bs=1M count=1K 
1024+0 레코드 들어옴
1024+0 레코드 나감
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 2.50148 s, 429 MB/s
dd if=/dev/zero of=testfile oflag=direct bs=1M count=1K  0.00s user 0.26s system 10% cpu 2.504 total
$ rm -f testfile 
$ time dd if=/dev/zero of=testfile bs=1M count=1K
1024+0 레코드 들어옴
1024+0 레코드 나감
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.843678 s, 1.3 GB/s
dd if=/dev/zero of=testfile bs=1M count=1K  0.00s user 0.55s system 65% cpu 0.846 total
$ rm -f testfile
  • 다이렉트 I/O(페이지 캐시 사용 X)의 경우 2.5초가 걸린다.

  • 페이지 캐시를 사용하였을 경우 약 0.84초가 걸렸다. 2배가 넘게 빠른 셈이다.

시스템 통계 정보 확인

  • write.sh 스크립트를 실행하며 sar -B 명령어로 시스템의 통계 정보를 확인한다.

  • 책의 내용과 달리 내 환경에서는 페이지 아웃이 발생한다. 그래서 다이렉트 I/O와 페이지 아웃 발생량을 비교해본다.

    • 페이지 캐시 사용 X

      $ sar -B 1
      Linux 5.13.13-surface (ubun2-Surface-Pro-7)     2021년 09월 23일     _x86_64_    (8 CPU)
      
      20시 36분 30초  pgpgin/s pgpgout/s   fault/s  majflt/s  pgfree/s pgscank/s pgscand/s pgsteal/s    %vmeff
      20시 36분 31초      0.00      0.00     97.00      0.00    698.00      0.00      0.00      0.00      0.00
      20시 36분 32초      0.00 181248.00   4202.00      0.00  17727.00      0.00      0.00      0.00      0.00
      20시 36분 33초      0.00 423936.00   3791.00      0.00   9221.00      0.00      0.00      0.00      0.00
      20시 36분 34초      0.00 419840.00   1660.00      0.00   1759.00      0.00      0.00      0.00      0.00
      20시 36분 35초      0.00  23552.00   6978.00      0.00   6528.00      0.00      0.00      0.00      0.00
      20시 36분 36초      0.00      0.00   2855.00      0.00   6085.00      0.00      0.00      0.00      0.00
      $ script/write-direct.sh
      2021. 09. 23. (목) 20:36:31 KST: start write (file creation)
      1024+0 레코드 들어옴
      1024+0 레코드 나감
      1073741824 bytes (1.1 GB, 1.0 GiB) copied, 2.74401 s, 391 MB/s
      2021. 09. 23. (목) 20:36:34 KST: end write
    • 페이지 캐시 사용 O

      $ sar -B 1
      Linux 5.13.13-surface (ubun2-Surface-Pro-7)     2021년 09월 23일     _x86_64_    (8 CPU)
      
      20시 37분 34초  pgpgin/s pgpgout/s   fault/s  majflt/s  pgfree/s pgscank/s pgscand/s pgsteal/s    %vmeff
      20시 37분 35초      0.00      0.00    736.00      0.00   3488.00      0.00      0.00      0.00      0.00
      20시 37분 36초      0.00 236760.00   5643.00      0.00  15954.00      0.00      0.00      0.00      0.00
      20시 37분 37초      0.00 189244.00   3852.00      0.00 266577.00      0.00      0.00      0.00      0.00
      20시 37분 38초      0.00      0.00   8121.00      0.00  23635.00      0.00      0.00      0.00      0.00
      20시 37분 39초      0.00      0.00      1.00      0.00   1057.00      0.00      0.00      0.00      0.00
      $ script/write.sh
      2021. 09. 23. (목) 20:37:36 KST: start write (file creation)
      1024+0 레코드 들어옴
      1024+0 레코드 나감
      1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.768723 s, 1.4 GB/s
      2021. 09. 23. (목) 20:37:36 KST: end write
      • 페이지 아웃이 발생하긴 했으나, 다이렉트 I/O의 경우보다 절반도 안되는 양임을 확인할 수 있다.
  • sar -d -p 명령어로 저장 장치의 I/O 발생량을 확인한다. (4번째 필드가 wkB/s)

    • 페이지 캐시 사용 X

      $ sar -d -p 1 | grep nvme0n1
      20시 39분 21초   nvme0n1     11.00      0.00     52.00      0.00      4.73      0.04      2.00      2.40
      20시 39분 22초   nvme0n1   1156.00      0.00 295936.00      0.00    256.00      1.54      1.33     70.40
      20시 39분 23초   nvme0n1    960.00      0.00 245760.00      0.00    256.00      2.29      2.38    100.00
      20시 39분 24초   nvme0n1    596.00      0.00 152576.00      0.00    256.00      2.34      3.93    100.00
      20시 39분 25초   nvme0n1    662.00      0.00 169296.00      0.00    255.73      2.32      3.51    100.00
      20시 39분 26초   nvme0n1    725.00      0.00 167868.00      0.00    231.54      2.59      3.55    100.00
      20시 39분 27초   nvme0n1     69.00      0.00  17664.00      0.00    256.00      0.25      3.59     10.40
      20시 39분 28초   nvme0n1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      $ script/write-direct.sh    
      2021. 09. 23. (목) 20:39:21 KST: start write (file creation)
      1024+0 레코드 들어옴
      1024+0 레코드 나감
      1073741824 bytes (1.1 GB, 1.0 GiB) copied, 4.80191 s, 224 MB/s
      2021. 09. 23. (목) 20:39:26 KST: end write
    • 페이지 캐시 사용

      $ sar -d -p 1 | grep nvme0n1
      20시 38분 47초   nvme0n1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      20시 38분 48초   nvme0n1    206.00      0.00  45236.00      0.00    219.59      2.64     12.82     23.20
      20시 38분 49초   nvme0n1    744.00      0.00 180920.00      0.00    243.17    205.91    276.76    100.00
      20시 38분 50초   nvme0n1    379.00      0.00  93708.00      0.00    247.25    348.13    918.41     73.60
      20시 38분 51초   nvme0n1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
      $ script/write.sh
      2021. 09. 23. (목) 20:38:47 KST: start write (file creation)
      1024+0 레코드 들어옴
      1024+0 레코드 나감
      1073741824 bytes (1.1 GB, 1.0 GiB) copied, 1.17943 s, 910 MB/s
      2021. 09. 23. (목) 20:38:49 KST: end write
      • 저장 장치에 I/O가 발생하긴 했으나, 다이렉트 I/O의 경우보다 발생량이 적은 것을 알 수 있다.

튜닝 파라미터

리눅스에는 페이지 캐시를 제어하기 위한 튜닝 파라미터들이 존재한다.

sysctl의 vm.dirty_writeback_centisecs

  • 더티 페이지의 라이트 백이 발생하는 주기 (단위 : 1/100초)

  • 값을 0으로 하면 주기적인 라이트 백은 발생하지 않지만 위험하다고 한다.

  • 기본값 : 5초에 1번

    $ sysctl vm.dirty_writeback_centisecs
    vm.dirty_writeback_centisecs = 500

sysctl의 vm.dirty_background_ratio

  • 시스템의 물리 메모리 중 더티 페이지가 차지하는 최대 비율

  • 더티 페이지가 이 값(퍼센트)을 초과할 경우 백그라운드 라이트 백 처리가 동작한다.

  • 메모리가 부족할 때 라이트 백 부하가 커지는 것을 방지한다.

  • 기본값 : 10 (10%)

    $ sysctl vm.dirty_background_ratio
    vm.dirty_background_ratio = 10

sysctl의 vm.dirty_background_bytes

  • vm.dirty_background_ratio를 비율 대신 바이트 단위로 설정할 때 사용한다.

  • 기본값 : 0 (사용하지 않음)

    $ sysctl vm.dirty_background_bytes
    vm.dirty_background_bytes = 0

sysctl의 vm.dirty_ratio

  • 더티 페이지가 차지하는 비율이 이 값(퍼센트)을 초과하면, 프로세스에 의한 파일에 쓰기의 연장으로 동기적인 라이트 백을 수행한다.

  • 기본값 : 20

    $ sysctl vm.dirty_ratio
    vm.dirty_ratio = 20

sysctl의 vm.dirty_bytes

  • vm.dirty_ratio를 비율 대신 바이트 단위로 설정할 때 사용한다.

  • 기본값 : 0 (사용하지 않음)

    $ sysctl vm.dirty_bytes      
    vm.dirty_bytes = 0

시스템의 페이지 캐시 비우기

  • /proc/sys/vm/drop_caches 라는 파일에 3을 넣으면 페이지 캐시가 비워진다고 한다.

    $ sudo su
    ubun2-Surface-Pro-7# free   
                  total        used        free      shared  buff/cache   available
    Mem:        7729020     1409952     3616468      526436     2702600     5506612
    스왑:       2097148           0     2097148
    ubun2-Surface-Pro-7# echo 3 > /proc/sys/vm/drop_caches 
    ubun2-Surface-Pro-7# free
                  total        used        free      shared  buff/cache   available
    Mem:        7729020     1404616     5333292      527360      991112     5546864
    스왑:       2097148           0     2097148
    • 왜인지 zsh에서는 sudo 명령어로도 덮어쓰기가 안돼서 루트 유저로 진행했다.

    • 페이지 캐시가 약 1.7GiB 비워진 것을 볼 수 있다.

정리

  • 파일의 데이터가 페이지 캐시에 있으면 파일 접근이 굉장히 빨라진다.

  • 설정을 변경했거나 시간이 지나면서 시스템 성능이 안좋아졌을 경우, 파일의 데이터가 페이지 캐시에 제대로 들어가는지 확인하는게 좋다.

  • sysctl 파라미터들을 잘 튜닝하면 페이지 캐시의 라이트 백 I/O 부하를 예방할 수 있다.

  • sar -B, sar -d -p 명령어를 통해 페이지 캐시에 관한 통계를 얻을 수 있다.


하이퍼스레드hyper-thread

CPU의 계산은 굉장히 빠른 것에 비해, 메모리 및 캐시 메모리의 접근 레이턴시는 상대적으로 느리다. 따라서 CPU 사용 시간 중 대부분은 메모리나 캐시 메모리로부터 데이터를 기다리는 시간이라고 할 수 있다.

하이퍼스레드 기능은 CPU 코어 1개의 레지스터 등 일부 자원을 2개씩(일반적) 구성하여 2개의 논리 CPU(하이퍼스레드라는 단위)로 인식되도록 하는 하드웨어의 기능이다.

4코어 8스레드의 CPU라고 해도 4코어 4스레드인 CPU에 비해 2배 성능이 나오는 것은 아니고, 현실적으로 20~30%의 성능 향상이 나오면 훌륭한 것이라고 한다.

책의 저자가 커널 빌드에 걸리는 시간을 기준으로 테스트했을 때, 하이퍼 스레드를 끈 경우에 비해 켰을 경우 22%(93초 -> 73초)의 성능 향상이 있었다. 즉 생각처럼 2배만큼 빨라지는 것은 아니다.

하이퍼스레드의 짝을 이루는 논리 CPU는 /sys/devices/system/cpu/cpu0/topology/thread_siblings_list 파일에서 볼 수 있다. cpu0 대신 보고 싶은 논리 CPU의 번호를 넣으면 된다.

$ ls /sys/devices/system/cpu/cpu*/topology/thread_siblings_list
/sys/devices/system/cpu/cpu0/topology/thread_siblings_list
/sys/devices/system/cpu/cpu1/topology/thread_siblings_list
/sys/devices/system/cpu/cpu2/topology/thread_siblings_list
/sys/devices/system/cpu/cpu3/topology/thread_siblings_list
/sys/devices/system/cpu/cpu4/topology/thread_siblings_list
/sys/devices/system/cpu/cpu5/topology/thread_siblings_list
/sys/devices/system/cpu/cpu6/topology/thread_siblings_list
/sys/devices/system/cpu/cpu7/topology/thread_siblings_list
$ cat < $(ls /sys/devices/system/cpu/cpu*/topology/thread_siblings_list)
0,4
1,5
2,6
3,7
0,4
1,5
2,6
3,7

하이퍼스레드를 켰을 때 오히려 성능 저하가 발생하는 경우도 있다고 하는데, 아마 계산 시간이 길면 오버헤드가 더 커져서 스래싱같은 상황이 발생할 수도 있겠다고 생각한다.


참고


소스 코드

'Linux > Linux Structure' 카테고리의 다른 글

Chapter 8. 저장 장치  (2) 2021.09.26
Chapter 7. 파일시스템  (0) 2021.09.26
Chapter 5. 메모리 관리  (3) 2021.09.26
Chapter 4. 프로세스 스케줄러  (0) 2021.09.26
Gnuplot  (0) 2021.09.26
Linux/Linux Structure 2021. 9. 26. 03:04

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
  • 테스트 프로그램

    1. 프로세스의 메모리 맵 정보(/proc/PID/maps)를 출력한다.

    2. testfile을 열어둔다.

    3. 파일을 mmap()으로 메모리 공간에 매핑한다.

    4. 프로세스의 메모리 맵 정보를 다시 출력한다.

    5. 매핑 영역의 데이터를 읽어서 출력한다.

    6. 매핑 영역에 쓰기를 시도한다.

    7. 파일 확인

  • 프로그램 실행 결과

    $ 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() 함수를 통해 동적으로 메모리를 획득한 경우, "가상 메모리를 확보한 상태"라고 볼 수 있다. 이 가상 메모리에 접근할 때 물리 메모리를 확보하고 매핑하는 것을 "물리 메모리를 확보한 상태"라고 표현한다. 이 때 가상 메모리와는 상관없이 물리 메모리 부족이 발생할 수 있다.

디맨드 페이징 실험

  • 확인할 항목

    • 메모리 획득 시 가상 메모리 사용량만 증가한다. (물리 메모리 사용량은 그대로)

    • 획득한 메모리에 접근하면 페이지 폴트가 발생하고, 물리 메모리의 사용량이 증가한다.

  • 테스트 프로그램

    1. 이 프로그램이 실행되는 동안 sar -r 명령어를 통해 메모리 사용량을 분석한다. 이를 위해 출력 메시지에 현재 시간을 표시한다.

    2. 메모리 획득 전에 메시지를 출력한다. 그 후 사용자의 입력을 기다린다.

    3. 100MiB 메모리 획득

    4. 다시 한번 메시지를 출력하고 사용자의 입력을 기다린다.

    5. 획득한 메모리를 처음부터 끝까지 1페이지씩 접근하고, 10MiB씩 접근할 때마다 메시지 출력

    6. 사용자의 입력을 기다린다.

  • 테스트 결과 (터미널 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에 있는 쓰기 권한을 둘 다 무효화(불가능) 처리한다. 이후 페이지의 내용을 변경하려고 하면 다음과 같은 흐름이 발생한다.

  1. 페이지 쓰기 권한이 없으므로 CPU에 페이지 폴트가 발생한다.

  2. CPU가 커널 모드로 변경되고, 커널의 페이지 폴트 핸들러가 동작한다. 접근한 페이지를 다른 메모리 영역에 복사하고, 해당 프로세스에 할당한 뒤 내용을 변경한다.

  3. 변경하려한 프로세스의 PTE에서 가상 주소 영역을 새로 복사한 물리 메모리 영역으로 매핑하고, 부모 프로세스와 자식 프로세스 PTE의 쓰기 권한을 둘 다 허용으로 변경하여 이후 자유로운 쓰기가 가능하도록 한다. (더 이상 해당 물리 메모리를 공유하지 않으므로)

즉, fork() 시스템 콜을 호출했을 때가 아닌 쓰기 작업이 발생할 때 물리 메모리를 복사하기 때문에, 이 방식을 CoWCopy on Write라고 부른다.

  • fork() 시스템 콜을 호출할 시점과 상관없이 쓰기 작업이 발생한 시점에 물리 메모리에 여유 공간이 부족하다면 물리 메모리 부족이 발생한다.

CoW 실험

  • 확인할 사항

    • fork() 시스템 콜을 호출했을 떄부터 쓰기 작업이 발생할 때까지는 부모 프로세스와 자식 프로세스가 물리 메모리 영역을 공유한다.

    • 쓰기 작업이 발생할 때 페이지 폴트가 발생한다.

  • 테스트 프로그램

    1. 100MiB 메모리를 확보하여 모든 페이지에 접근한다.

    2. 시스템 메모리 사용량 출력

    3. fork() 시스템 콜 호출

    4. 자식 프로세스에서 시스템 메모리 사용량 및 자식 프로세스의 메모리 사용량을 출력한다.

    5. 1에서 확보한 메모리 영역에 자식 프로세스가 전부 쓰기 작업을 수행한다.

    6. 자식 프로세스에서 시스템 메모리 사용량 및 자식 프로세스의 메모리 사용량을 출력한다.

  • 실험 결과

    $ 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개씩 묶어서 하위 페이지 테이블의 주소를 가리키도록 한다. 03, 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을 증가시켜 성능을 향상시킨다.

사용 방법

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
Linux/Linux Structure 2021. 9. 26. 03:04

Chapter 4. 프로세스 스케줄러

  • 프로세스 스케줄러는 여러 프로세스를 타임 슬라이스 방식으로 번갈아 처리한다.

    • CPU는 한번에 하나의 프로세스만 처리할 수 있다.
  • 시스템에서 CPU로 인식하는 것을 논리 CPU라고 하며, 하이퍼스레딩이 적용된 경우 각 하이퍼스레드가 논리 CPU로 인식된다.


테스트 프로그램으로 작동 방식 확인하기

  • 알고자 하는 정보

    • 논리 CPU가 특정 시점에 어떤 프로세스를 실행 중인가

    • 각 프로세스의 진행률

  • 테스트 프로그램 사양

    • 명령어 라인 파라미터(nproc, total, resolution)

      파라미터 설명
      nproc 동시 동작 프로세스 수
      total 프로그램이 동작하는 총 시간[ms]
      resolution 데이터 수집 시간 간격[ms]
    • 각 프로세스의 동작 방식

      • CPU 시간을 total 밀리초만큼 사용한 후 종료한다.

      • CPU 시간을 resolution 밀리초만큼 사용할 때마다 다음 내용을 출력한다.

        프로세스고유ID(0 ~ nproc-1)   프로그램시작시점부터경과한시간   진행도[%]

테스트 프로그램 컴파일

$ gcc -o output/sched src/sched.c

실험 1

  • 모든 프로세스가 1개의 논리 CPU에서만 동작하게 하여 스케줄러의 동작을 확인한다.

    • 아래 형식처럼 taskset 명령어의 -c 옵션으로 논리 CPU를 지정할 수 있다.

      $ taskset -c 0 ./sched nproc total resolution
  • 프로세스의 개수를 3가지 경우(1개, 2개, 4개)로 나눠서 결과를 측정할 것이다.

결과를 파일로 저장

  • 프로세스 1개를 100ms동안 1ms 정밀도로 측정하며 동작시킨다.

    $ taskset -c 0 output/sched 1 100 1
    0       1       1
    0       1       2
    0       2       3
    ...
    0       92      99
    0       93      100
  • 결과를 그래프로 보기 위해서, 다음과 같이 파일로 저장한다.

    $ taskset -c 0 output/sched 1 100 1 > log/1core-1process.log
    $ taskset -c 0 output/sched 2 100 1 > log/1core-2process.log
    $ taskset -c 0 output/sched 4 100 1 > log/1core-4process.log

프로세스 개수에 따른 진행도 시각화

  • gnuplot 툴을 이용하여 각 결과를 그래픽으로 확인할 수 있다.

    $ gnuplot plot/1core-1process.gnu
    $ gnuplot plot/1core-2process.gnu
    $ gnuplot plot/1core-4process.gnu
  • 1코어 1프로세스

    • 프로세스 1개가 코어를 독점하므로 선형적인 그래프가 나온다.
  • 1코어 2프로세스

    • 2개의 프로세스가 타임 슬라이스 방식으로 진행되는 것을 볼 수 있다.
  • 1코어 4프로세스

    • 4개의 프로세스가 타임 슬라이스 방식으로 진행되는 것을 볼 수 있다.

논리 CPU의 타임 슬라이스 시각화

  • 1코어 1프로세스

    • 프로세스 1개가 코어를 독점하는 것을 볼 수 있다.
  • 1코어 2프로세스

    • 2개의 프로세스가 동시에 처리되지 않고 타임 슬라이스 방식으로 진행되는 것을 볼 수 있다.
  • 1코어 4프로세스

    • 4개의 프로세스가 타임 슬라이스 방식으로 진행되는 것을 볼 수 있다.

실험 1 고찰

  • 위의 결과를 통해 다음 사실을 알 수 있다.

    • 각 프로세스는 논리 CPU를 사용하는 동안에만 진행된다.

    • 특정 순간에 논리 CPU에서 동작되는 프로세스는 1개이다.

    • 총 소요 시간은 프로세스 수에 비례한다.

    • 각 프로세스는 대략 같은 양의 타임 슬라이스를 가진다. (라운드 로빈)


컨텍스트 스위치(Context switch)

  • 논리 CPU 상에서 동작 중인 프로세스를 바꾸는 것을 컨텍스트 스위치라고 한다.

  • 위의 프로세스 4개짜리 그래프에서, 논리 CPU에서 동작 중인 프로세스가 계속 변하는 것을 볼 수 있고, 이 때마다(타임 슬라이스 타이밍) 컨텍스트 스위치가 발생하는 것이다.

  • 프로세스가 어떤 함수를 수행 중이더라도 타임 슬라이스를 모두 소비하면 컨텍스트 스위치가 발생한다.


프로세스의 상태

실행 중인 프로세스 확인

  • ps ax 명령어로 현재 시스템에 존재하는 프로세스를 출력할 수 있다.

    $ ps ax | wc -l
    274    # 프로세스 개수
    • | 기호는 파이프이며, 앞의 출력 결과를 뒤에 있는 명령어의 입력으로 넘긴다.

    • wc -l 명령어는 라인의 수를 출력한다.

  • 앞의 실험에서 sched 프로세스가 진행 중일 때, 다른 프로세스들은 대부분 슬립 상태였다.

  • 프로세스의 상태

    상태 의미
    실행 상태 논리 CPU를 사용 중인 상태
    실행 대기 상태 CPU 시간이 할당되기를 기다리는 상태
    슬립 상태 이벤트 발생을 기다리는 상태. CPU 시간 사용 X
    좀비 상태 프로세스 종료 후 부모 프로세스가 종료 상태를 인식할 때까지 대기하는 상태
    • 이벤트 예시

      • 대기하도록 정해진 시간이 경과

      • 키보드나 마우스, HDD나 SSD 등의 I/O 이벤트

      • 네트워크 송수신 종료

  • 상태 직접 확인

    • ps ax 명령어의 결과 중 3번째 필드인 STAT 의 첫 문자를 통해 상태를 알 수 있다.

      STAT 필드 첫 문자 상태
      R 실행(Run) or 실행 대기(Ready)
      S or D 슬립 상태. 시그널에 따라 실행 상태도 되돌아 오는 것이 S, 저장 장치 접근 대기 등이 D
      Z 좀비 상태
      I Idle 상태
      • D 상태가 오래 지속된다면 스토리지의 I/O가 종료되지 않은 상태이거나, 커널에 문제가 있다는 것을 의미한다.
    $ ps ax
      PID TTY      STAT   TIME COMMAND
        1 ?        Ss     0:01 /sbin/init splash
        2 ?        S      0:00 [kthreadd]
        3 ?        I<     0:00 [rcu_gp]
        4 ?        I<     0:00 [rcu_par_gp]
        6 ?        I<     0:00 [kworker/0:0H-events_highpri]
        9 ?        I<     0:00 [mm_percpu_wq]
       10 ?        S      0:00 [rcu_tasks_rude_]
       11 ?        S      0:00 [rcu_tasks_trace]
       12 ?        S      0:00 [ksoftirqd/0]
       13 ?        I      0:20 [rcu_sched]
       14 ?        S      0:00 [migration/0]
       15 ?        S      0:00 [idle_inject/0]
       16 ?        S      0:00 [cpuhp/0]
       ...

프로세스의 상태 변화

  • 프로세스의 상태도는 운영체제마다, 버전마다 다른 것 같다. 다음은 위키피디아의 그림이다.

    • 프로세스는 생성부터 종료까지 여러 상태로 변화하며 진행된다.
  • 실험 1의 1core-1process 는 해당 프로세스가 논리 CPU를 거의 독점하기 때문에 슬립이 없다고 할 수 있다.

  • 실험 1의 1core-2process 는 2개의 프로세스가 번갈아서 CPU 시간이 할당되기를 기다리므로, 실행 상태와 실행 대기 상태를 왔다갔다한다.

idle 상태

  • 논리 CPU에서 아무 프로세스도 동작하지 않으면, idle 프로세스라고 하는 "아무 것도 하지 않는" 프로세스가 동작한다.

    • 무한루프처럼 무언가를 계속 반복하는 것은 아니고, CPU의 특수한 명령을 이용하여 CPU를 휴식 상태로 만들어서 실행 가능한 프로세스가 있을 떄까지 소비 전력을 낮춘다.

    • 스마트폰의 배터리가 오래 가는 이유도 CPU가 이 idle 상태로 오래 있는 덕분이다.

  • sar 명령어를 통해 논리 CPU가 얼마나 idle 상태에 있는지 알 수 있다.

    $ sar -P ALL 1
    Linux 5.11.0-27-generic (ubun2-Surface-Pro-7)     2021년 09월 08일     _x86_64_(8 CPU)
    
    20시 23분 48초     CPU     %user     %nice   %system   %iowait    %steal     %idle
    20시 23분 49초     all      2.14      0.00      0.75      0.00      0.00     97.11
    20시 23분 49초       0      3.00      0.00      1.00      0.00      0.00     96.00
    20시 23분 49초       1      0.00      0.00      0.00      0.00      0.00    100.00
    20시 23분 49초       2      2.00      0.00      1.00      0.00      0.00     97.00
    20시 23분 49초       3      1.01      0.00      1.01      0.00      0.00     97.98
    20시 23분 49초       4      4.90      0.00      0.98      0.00      0.00     94.12
    20시 23분 49초       5      1.01      0.00      0.00      0.00      0.00     98.99
    20시 23분 49초       6      3.03      0.00      1.01      0.00      0.00     95.96
    20시 23분 49초       7      2.00      0.00      1.00      0.00      0.00     97.00
    • %idle이 대부분 거의 100인 것을 보아, 현재 시스템 전체가 CPU를 거의 사용하지 않음을 알 수 있다.
  • 무한 루프 실행 후 확인

    • 논리 CPU 0번에서 loop.py 스크립트를 실행하고 확인해보면 다음과 같다.

      $ taskset -c 0 python src/loop.py &
      [1] 37983
      $ sar -P ALL 1 1                   
      Linux 5.11.0-27-generic (ubun2-Surface-Pro-7)     2021년 09월 08일     _x86_64_    (8 CPU)
      
      20시 29분 05초     CPU     %user     %nice   %system   %iowait    %steal     %idle
      20시 29분 06초     all      0.38     12.56      0.00      0.00      0.00     87.06
      20시 29분 06초       0      0.00    100.00      0.00      0.00      0.00      0.00 # 여기
      20시 29분 06초       1      1.01      0.00      0.00      0.00      0.00     98.99
      20시 29분 06초       2      0.00      0.00      0.00      0.00      0.00    100.00
      20시 29분 06초       3      0.00      0.00      0.00      0.00      0.00    100.00
      20시 29분 06초       4      1.00      0.00      0.00      0.00      0.00     99.00
      20시 29분 06초       5      0.99      0.00      0.00      0.00      0.00     99.01
      20시 29분 06초       6      0.00      0.00      0.00      0.00      0.00    100.00
      20시 29분 06초       7      0.00      0.00      0.00      0.00      0.00    100.00
      ...
      $ kill 37983
      [1]  + 37983 terminated  taskset -c 0 python src/loop.py 
      • 결과를 보면 CPU 0번에서 %idle의 값이 0임을 볼 수 있다.

스루풋, 레이턴시

  • 논리 CPU에서 스루풋과 레이턴시는 다음을 의미한다.

    • 스루풋throughput = 완료한 프로세스의 수 / 경과 시간

      • 단위 시간 당 처리된 일의 양
    • 레이턴시latency = 처리 종료 시간 - 처리 시작 시간

      • 각 처리의 시작부터 종료까지 경과된 시간

예시

  • 1초 동안 프로세스 1개의 처리를 끝내는 경우

    • 논리 CPU의 경과 시간 중 40%가 idle 상태라고 가정하면 다음 그림과 같다.

      • 일단 1개의 프로세스를 1초만에 끝냈으므로 스루풋은 1프로세스 / 1초 이다.
    • 처음에 시간 차를 두고 2개의 프로세스를 실행하면 이상적인 조건에서 다음과 같이 동작할 수 있다.

      • 이 경우 2개의 프로세스를 1.2초만에 끝냈으므로, 스루풋은 1.67프로세스 / 1초 이다.
    • 즉 idle 상태가 최소일 때 스루풋이 최대가 된다.

실험 1 고찰 - 스루풋, 레이턴시

  • 1core-1process

    • 스루풋

      • 1개의 프로세스를 100ms동안 처리하므로, 스루풋은 10프로세스 / 1초 이다.
    • 평균 레이턴시

      • 처리의 시작부터 종료까지 약 100ms가 걸리므로 100ms라고 할 수 있다.
  • 1core-2process

    • 스루풋

      • 2개의 프로세스를 200ms동안 처리하므로, 스루풋은 위와 같은 10프로세스 / 1초 이다.
    • 평균 레이턴시

      • 200ms
  • 1core-4process

    • 스루풋

      • 4프로세스 / 400ms == 10프로세스 / 1초
    • 평균 레이턴시

      • 400ms
  • 이를 표로 나타내면 다음과 같다.

    프로세스 개수 스루풋[프로세스 / 초] 레이턴시[ms]
    1 10 100
    2 10 200
    4 10 400
  • 결론

    • 이론상 논리 CPU가 idle 상태로 변하지 않는 경우, 스루풋은 동일하다.

      • 오히려 컨텍스트 스위치의 오버헤드로 인해 더 느려진다.
    • 프로세스 개수를 늘리면 레이턴시는 증가한다.

    • 각 프로세스의 평균 레이턴시는 비슷하다.

      • 타임 슬라이스 방식으로 프로세스를 진행시키기 때문이다.

실제 시스템

  • 실제 시스템에서 논리 CPU는 다음의 상태를 엄청나게 반복하며 전환한다.

    • idle : 논리 CPU가 쉬는 상태이므로 스루풋이 떨어진다.

    • 프로세스 동작 중 : 프로세스를 실행 중이므로 이상적인 상태지만, 다른 프로세스가 실행 대기 상태이면 여러 프로세스의 레이턴시가 증가한다. (타임 슬라이스 방식으로 작업을 나누기 때문에)

    • 프로세스 대기 중 : 실행 대기 중인 프로세스가 존재하는 상태이므로 스루풋은 높지만, 프로세스들의 레이턴시가 길어진다.

  • 실제 시스템을 설계할 때는 스루풋과 레이턴시의 목표치를 정한 뒤 시스템을 튜닝한다고 한다.

  • 실행 중, 실행 대기 중인 프로세스 확인

    • sar 명령어의 -q 옵션을 이용하여 runq-sz 필드를 통해 확인할 수 있다.

      $ sar -q 1 1
      Linux 5.13.13-surface (ubun2-Surface-Pro-7)     2021년 09월 10일     _x86_64_(8 CPU)
      
      02시 21분 57초   runq-sz  plist-sz   ldavg-1   ldavg-5  ldavg-15   blocked
      02시 21분 58초         0       861      0.48      0.47      0.40         0
      평균값:          0       861      0.48      0.47      0.40         0
      $ taskset -c 0 python src/loop.py &
      [1] 5915
      $ sar -q 1 1
      Linux 5.13.13-surface (ubun2-Surface-Pro-7)     2021년 09월 10일     _x86_64_(8 CPU)
      
      02시 22분 13초   runq-sz  plist-sz   ldavg-1   ldavg-5  ldavg-15   blocked
      02시 22분 14초         1       851      0.37      0.44      0.39         0
      평균값:          1       851      0.37      0.44      0.39         0
      $ taskset -c 0 python src/loop.py &
      [2] 5968
      $ sar -q 1 1
      Linux 5.13.13-surface (ubun2-Surface-Pro-7)     2021년 09월 10일     _x86_64_(8 CPU)
      
      02시 22분 18초   runq-sz  plist-sz   ldavg-1   ldavg-5  ldavg-15   blocked
      02시 22분 19초         2       852      0.42      0.45      0.39         0
      평균값:          2       852      0.42      0.45      0.39         0
      $ kill 5915 5968
      [1]  - 5915 terminated  taskset -c 0 python src/loop.py                         
      [2]  + 5968 terminated  taskset -c 0 python src/loop.py

실험 2

  • 모든 프로세스가 2개의 논리 CPU에서 동작하게 하여 스케줄러의 동작을 확인한다.

로드밸런서load balancer

  • 여러 개의 논리 CPU에 공평하게 프로세스를 분배하는, 스케줄러 안에서 동작하는 기능이다.

    • 글로벌 스케줄러global scheduler라고도 한다.

결과를 파일로 저장

  • 먼저 논리 CPU의 개수를 알아야 적절한 논리 CPU 번호를 고를 수 있다.

    $ grep -c processor /proc/cpuinfo
    8
  • 8개의 논리 CPU가 존재한다면, 실험에는 CPU 0과 CPU 4가 적절하다.

    • 이 2개의 논리 CPU는 캐시 메모리를 공유하고 있지 않는 등 서로 독립성이 높기 때문에, 보다 정확한 실험 결과를 도출할 수 있다.

    • 만약 하이퍼스레드가 적용된 환경인데 논리 CPU가 2개라면, 사실 1코어에서 실행되는 것이므로 결과가 부적절할 수 있다.

  • 논리 CPU 2개를 사용하며, 프로세스가 1개, 2개, 4개일 때의 결과를 파일로 저장한다.

    $ taskset -c 0,4 output/sched 1 100 1 > log/2core-1process.log
    $ taskset -c 0,4 output/sched 2 100 1 > log/2core-2process.log
    $ taskset -c 0,4 output/sched 4 100 1 > log/2core-4process.log

프로세스 개수에 따른 진행도 시각화

  • 위와 마찬가지로 gnuplot을 사용해 그래프로 표현한다.

    $ gnuplot plot/2core-1process.gnu
    $ gnuplot plot/2core-2process.gnu
    $ gnuplot plot/2core-4process.gnu
  • 2코어 1프로세스

- 프로세스 1개가 코어를 독점하므로 선형적인 그래프가 나온다.
  • 2코어 2프로세스

    • 2개의 프로세스가 2개의 논리 CPU를 하나씩 독점해서 사용하므로 위의 경우와 비슷한 것을 알 수 있다.
  • 2코어 4프로세스

    • 2개의 논리 CPU에 각 2개의 프로세스가 번갈아가며 동작하는 것을 볼 수 있다.

논리 CPU의 타임 슬라이스 시각화

  • 2코어 1프로세스

    • 프로세스 1개가 코어를 독점하는 것을 볼 수 있다.
  • 2코어 2프로세스

    • 2개의 프로세스가 2개의 논리 CPU를 독점한다.
  • 2코어 4프로세스

    • 4개의 프로세스가 2개씩 타임 슬라이스 방식으로 진행되는 것을 볼 수 있다.

실험 2 스루풋, 레이턴시

  • 프로세스가 증가하면 경과 시간이 100ms를 조금씩 넘는데, 단순화를 위해 100ms로 계산했다. 책에서는 대략 100 +- 10 ms 로 나온걸 보니 내 코드를 좀 수정해야 할 것 같다.

  • 2core-1process

    • 스루풋

      • 1개의 프로세스를 100ms동안 처리하므로, 스루풋은 10프로세스 / 1초 이다.
    • 평균 레이턴시

      • 처리의 시작부터 종료까지 약 100ms가 걸리므로 100ms라고 할 수 있다.
  • 2core-2process

    • 스루풋

      • 2개의 프로세스를 100ms동안 처리하므로, 스루풋은 위와 같은 20프로세스 / 1초 이다.
    • 평균 레이턴시

      • 100ms
  • 2core-4process

    • 스루풋

      • 4프로세스 / 200ms == 20프로세스 / 1초
    • 평균 레이턴시

      • 200ms
  • 이를 표로 나타내면 다음과 같다.

    프로세스 개수 스루풋[프로세스 / 초] 레이턴시[ms]
    1 10 100
    2 20 100
    4 20 200

실험 2 고찰

  • 1개의 논리 CPU에서 동시에 처리되는 프로세스의 수는 1개이다.

  • 여러 프로세스가 실행 가능한 경우, 타임 슬라이스를 이용해 각 프로세스를 CPU에 순차적으로 할당한다.

  • 멀티코어 CPU 환경에서는 여러 프로세스를 동시에 동작시켜야 스루풋이 증가한다.

    • 실행 가능한 프로세스 수가 논리 CPU보다 많아지면 스루풋은 더 이상 오르지 않고, 레이턴시가 증가한다.

경과 시간과 사용 시간

time 명령어

time 명령어를 통해 프로세스를 동작시키면 경과 시간과 사용 시간 두 가지 수치를 알 수 있다.

  • 경과 시간 : 프로세스의 실행 시작 시간부터 실행 종료 시간까지의 경과 시간을 나타낸다.

  • 사용 시간 : 프로세스가 실제로 논리 CPU를 사용한 시간을 의미한다.

    • 즉 프로세스가 "실행 상태"인 구간의 시간을 합친 값이다.

bash와 zsh의 출력 형식이 약간 다르다.

  • bash

    real  경과시간
    user  유저모드_CPU동작시간
    sys   커널모드_CPU동작시간
  • zsh

    %J  %U user %S system %P cpu %*E total
    작동명령어 유저모드_CPU동작시간 user 커널모드_CPU동작시간 system CPU사용량[%] cpu 경과시간 total

실제 측정

  • 오차를 최소화하기 위해 실행 시간을 10초로 설정한다.

    $ time taskset -c 0 output/sched 1 10000 10000
    0       9410    100
    taskset -c 0 output/sched 1 10000 10000  11.68s user 0.00s system 99% cpu 11.677 total
    $ time taskset -c 0 output/sched 2 10000 10000
    1       18740   100
    0       18769   100
    taskset -c 0 output/sched 2 10000 10000  20.99s user 0.00s system 99% cpu 20.992 total
    $ time taskset -c 0 output/sched 4 10000 10000
    3       36168   100
    1       36183   100
    2       36213   100
    0       36242   100
    taskset -c 0 output/sched 4 10000 10000  38.54s user 0.00s system 99% cpu 38.547 total
    $ time taskset -c 0,4 output/sched 1 10000 10000
    0       9188    100
    taskset -c 0,4 output/sched 1 10000 10000  11.42s user 0.00s system 99% cpu 11.424 total
    $ time taskset -c 0,4 output/sched 2 10000 10000
    1       11455   100
    0       11461   100
    taskset -c 0,4 output/sched 2 10000 10000  25.16s user 0.00s system 183% cpu 13.701 total
    $ time taskset -c 0,4 output/sched 4 10000 10000
    0       22894   100
    3       22936   100
    2       22939   100
    1       22946   100
    taskset -c 0,4 output/sched 4 10000 10000  48.12s user 0.00s system 191% cpu 25.182 total
  • 결과 읽는법

    taskset -c 0 output/sched 1 10000 10000  11.68s user 0.00s system 99% cpu 11.677 total
    • 11.68s user : 유저 모드에서 11.68ms 동안 동작

    • 0.00s system : 커널 모드에서는 동작하지 않음

    • 99% cpu : CPU 동작 시간이 99%

    • 11.677 total : 경과 시간이 11.677ms

  • 1core-1process

    $ time taskset -c 0 output/sched 1 10000 10000
    0       9410    100
    taskset -c 0 output/sched 1 10000 10000  11.68s user 0.00s system 99% cpu 11.677 total
    • 1개의 프로세스가 논리 CPU를 독점하므로 경과 시간과 사용 시간이 거의 같다.

    • 대부분의 시간이 사용자 모드에서 반복문을 돌리는 시간이므로 system 항목의 시간이 0에 가깝다.

    • 경과 시간과 결과 첫 줄의 두 번째 필드인 9410ms에서 차이가 나는 이유는 계산량을 측정하기 위한 함수 loops_per_msec() 의 처리를 위한 시간때문이다.

  • 1core-2process

    $ time taskset -c 0 output/sched 2 10000 10000
    1       18740   100
    0       18769   100
    taskset -c 0 output/sched 2 10000 10000  20.99s user 0.00s system 99% cpu 20.992 total
    • 2개의 프로세스가 번갈아서 논리 CPU를 독점하므로 경과 시간과 사용 시간이 위의 경우보다 2배가 되었다.
  • 1core-4process

    time taskset -c 0 output/sched 4 10000 10000
    3       36168   100
    1       36183   100
    2       36213   100
    0       36242   100
    taskset -c 0 output/sched 4 10000 10000  38.54s user 0.00s system 99% cpu 38.547 total
    • 프로세스 2개일 때와 마찬가지로, 프로세스의 개수만큼 경과 시간과 사용 시간이 4배가 되었다.
  • 2core-1process

    $ time taskset -c 0,4 output/sched 1 10000 10000
    0       9188    100
    taskset -c 0,4 output/sched 1 10000 10000  11.42s user 0.00s system 99% cpu 11.424 total
    • 1코어의 경우와 같다. 이는 2개의 코어 중 1개만 사용 중이기 때문이다.
  • 2core-2process

    time taskset -c 0,4 output/sched 2 10000 10000
    1       11455   100
    0       11461   100
    taskset -c 0,4 output/sched 2 10000 10000  25.16s user 0.00s system 183% cpu 13.701 total
    • 경과 시간보다 사용 시간이 약 2배인데, 이는 사용 시간이 CPU가 동시에 작동되는 시간을 합친 것이기 때문이다.
  • 2core-4process

    time taskset -c 0,4 output/sched 4 10000 10000
    0       22894   100
    3       22936   100
    2       22939   100
    1       22946   100
    taskset -c 0,4 output/sched 4 10000 10000  48.12s user 0.00s system 191% cpu 25.182 total
    • 프로세스 2개일 때와 마찬가지로, 사용 시간이 경과 시간의 약 2배가 되는 것을 볼 수 있다.

sleep

  • sleep 명령어의 경우 말 그대로 프로세스가 슬립 상태로 지속되므로, CPU는 idle 상태를 유지하게 된다.

    $ time sleep 10
    sleep 10  0.00s user 0.00s system 0% cpu 10.002 total
    • 경과 시간은 10초인데, 사용 시간이 0으로 나온다. 이는 논리 CPU가 거의 사용되지 않음을 의미한다.

ps -eo

  • ps 명령어의 -eo 옵션을 이용하여 time 명령어 말고도 프로세스의 경과 시간(ELAPSED)과 사용 시간(TIME)을 얻을 수 있다.

    $ ps -eo pid,comm,etime,time  
      PID COMMAND             ELAPSED     TIME
        1 systemd            01:33:13 00:00:00
        2 kthreadd           01:33:13 00:00:00
        3 rcu_gp             01:33:13 00:00:00
        4 rcu_par_gp         01:33:13 00:00:00
        ...
  • 1core-1process

    $ taskset -c 0 python src/loop.py &       
    [1] 12165
    $ ps -eo pid,comm,etime,time | grep python
      12165 python                00:09 00:00:08
    $ kill 12165
    [1]  + 12165 terminated  taskset -c 0 python src/loop.py 
    • 경과 시간과 사용 시간이 비슷하게 나왔다.
  • 1core-2process

    $ taskset -c 0 python src/loop.py &
    [1] 11839
    $ taskset -c 0 python src/loop.py & # 위의 명령어 실행 직후에 바로 실행해야 정확함
    [2] 11890
    $  ps -eo pid,comm,etime,time | grep python
        11839 python                00:13 00:00:07
        11890 python                00:13 00:00:06
    $ ps -eo pid,comm,etime,time | grep python
        11839 python                00:19 00:00:10
        11890 python                00:19 00:00:09
    $ kill 11839 11890
    [1]  - 11839 terminated  taskset -c 0 python src/loop.py
    [2]  + 11890 terminated  taskset -c 0 python src/loop.py
    • 경과 시간이 사용 시간의 거의 2배이다. 즉 2개의 프로세스 사용 시간의 합이다.
  • 1core-4process

    $ taskset -c 0 python src/loop.py &       
    [1] 12539
    $ taskset -c 0 python src/loop.py &
    [2] 12565
    $ taskset -c 0 python src/loop.py &
    [3] 12591
    $ taskset -c 0 python src/loop.py &
    [4] 12617
    $ ps -eo pid,comm,etime,time | grep python
      12539 python                00:12 00:00:03
      12565 python                00:12 00:00:02
      12591 python                00:12 00:00:02
      12617 python                00:11 00:00:02
    $ kill 12539 12565 12591 12617
    [3]  - 12591 terminated  taskset -c 0 python src/loop.py
    [1]    12539 terminated  taskset -c 0 python src/loop.py
    [4]  + 12617 terminated  taskset -c 0 python src/loop.py
    [2]  + 12565 terminated  taskset -c 0 python src/loop.py
    • 초의 소수점을 고려하면 역시나 경과 시간이 각 프로세스 사용 시간들의 합이 된다.
  • 2core-2process

    $ taskset -c 0,4 python src/loop.py &
    [1] 12989
    $ taskset -c 0,4 python src/loop.py &
    [2] 13040
    $ ps -eo pid,comm,etime,time | grep python
      12989 python                00:09 00:00:09
      13040 python                00:09 00:00:08
    $ kill 12989 13040
    [1]  - 12989 terminated  taskset -c 0,4 python src/loop.py
    [2]  + 13040 terminated  taskset -c 0,4 python src/loop.py
    • 이번에는 각 프로세스가 논리 CPU를 독점하므로 경과 시간과 사용 시간이 거의 같음을 볼 수 있다.

우선 순위 변경

nice() 시스템 콜

타임 슬라이스를 통해 여러 프로세스들을 공평하게 논리 CPU에 배정할 수 있다. 이 때 nice() 시스템 콜을 통해 프로세스 간에 우선순위를 두어, 특정 프로세스가 시간을 많이 배정받을 수 있게 할 수 있다.

우선순위를 높이는 것은 root 권한을 가진 슈퍼유저만 가능하고, 내리는 것은 누구나 가능하다.

우선순위는 -19 ~ 20의 값을 가질 수 있고, 숫자가 낮을 수록 우선순위가 높다. 기본 값은 0이다.

예제

  • 기존의 src/sched.c 코드에서, 2번째 프로세스(프로세스 1)의 nice 값을 5로 변경(nice(5)을 추가)한 뒤 1core-2process 테스트를 진행한다.

    $ gcc -o output/sched_nice src/sched_nice.c
    $ taskset -c 0 output/sched_nice 2 100 1 > log/1core-2process-nice.log
    $ gnuplot plot/1core-2process-nice.gnu 
    $ gnuplot plot/1core-2process-nice-slice.gnu 


  • 그림을 통해 우선순위가 높은 (nice 값이 작은) 프로세스의 CPU 점유 시간이 더 긴 것을 볼 수 있다.

nice 명령어

  • 우선순위 설정을 명령어로 수행할 수 있다.

  • sar 명령어의 %nice 필드는 우선순위를 변경한 프로세스에 할당한 시간의 비율을 나타낸다.

    $ nice -n 5 python src/loop.py &
    [1] 7431
    $ sar -P ALL 1 1
    Linux 5.13.13-surface (ubun2-Surface-Pro-7)     2021년 09월 15일        _x86_64_        (8 CPU)
    
    00시 17분 00초     CPU     %user     %nice   %system   %iowait    %steal     %idle
    00시 17분 01초     all      2.00     12.50      0.62      0.12      0.00     84.75
    00시 17분 01초       0      2.00      0.00      2.00      0.00      0.00     96.00
    00시 17분 01초       1      0.00    100.00      0.00      0.00      0.00      0.00
    00시 17분 01초       2      1.98      0.00      1.98      0.00      0.00     96.04
    00시 17분 01초       3      3.00      0.00      0.00      0.00      0.00     97.00
    00시 17분 01초       4      3.00      0.00      0.00      1.00      0.00     96.00
    00시 17분 01초       5      2.00      0.00      0.00      0.00      0.00     98.00
    00시 17분 01초       6      2.02      0.00      0.00      0.00      0.00     97.98
    00시 17분 01초       7      2.00      0.00      1.00      0.00      0.00     97.00
    ...
    $ kill 7431
    [1]  + 7431 terminated  nice -n 5 python src/loop.py  
    • 코어1의 %nice 값이 100인 것을 볼 수 있다.

    • 우선순위 5로 작동되는 무한루프가 CPU1을 계속 점유하고 있음을 나타낸다.


참고


소스 코드

'Linux > Linux Structure' 카테고리의 다른 글

Chapter 6. 메모리 계층  (0) 2021.09.26
Chapter 5. 메모리 관리  (3) 2021.09.26
Gnuplot  (0) 2021.09.26
Chapter 3. 프로세스 관리  (1) 2021.09.26
Chapter 2. 사용자 모드로 구현되는 기능  (0) 2021.09.26
Linux/Linux Structure 2021. 9. 26. 02:57

Gnuplot

  • 그래프를 그리는 툴로 유명한 gnuplot을 살짝 사용해보았다.

설치

  • 우분투 20.04 기준, 패키지 매니저로 설치할 수 있다.

    $ sudo apt install -y gnuplot

사용법

기본 조작

  • 다음과 같이 함수를 통해 출력할 수 있다.

    $ gnuplot
    
            G N U P L O T
            Version 5.2 patchlevel 8    last modified 2019-12-01 
    
            Copyright (C) 1986-1993, 1998, 2004, 2007-2019
            Thomas Williams, Colin Kelley and many others
    
            gnuplot home:     http://www.gnuplot.info
            faq, bugs, etc:   type "help FAQ"
            immediate help:   type "help"  (plot window: hit 'h')
    
    Terminal type is now 'qt'
    gnuplot> plot sin(x)
    gnuplot> unset key                          # 그래프의 레이블을 지운다. (sin(x))
    gnuplot> replot
    gnuplot> plot [x=-5:5] sin(x)               # x축을 -5부터 5까지만 표시한다.
    gnuplot> plot [-10:10] cos(x)               # "x=" 표시를 생략할 수 있다.
    gnuplot> plot [-10:10] [-0.5:0.5] cos(x)    # y축도 설정할 수 있다.
    gnuplot> set grid                           # 격자 표시
    gnuplot> replot
    gnuplot> set xlabel "Elapsed time[ms]"      # x축 레이블 설정
    gnuplot> set ylabel "Progress[%]"           # y축 레이블 설정
    gnuplot> set title "1core-1process"         # 그래프 제목 설정
    gnuplot> replot
  • 마우스 조작

    • 휠클릭으로 점을 찍어둘 수 있다.

    • 우클릭으로 영역을 지정하여 확대할 수 있다.

    • 위 패널의 autoscale(돋보기 3번째)를 클릭하여 화면에 꽉차도록 볼 수 있다.

파일의 데이터 출력

  • 파일을 기반으로 사용할 열을 지정하여 출력할 수 있다.

    gnuplot> plot "log/1core-1process.log" using 2:3  # 파일의 2열을 x축, 3열을 y축으로 사용

스크립트 실행

  • 셸에서 gnuplot 스크립트파일명 형태로 바로 실행할 수 있다.

    $ gnuplot plot/1core-1process.gnu

간단한 조건문 예시

  • 열의 값으로 조건을 걸어서 원하는 데이터만 출력할 수 있다.

    gnuplot> filename="log/1core-4process.log"
    gnuplot> plot filename using 2:($1=="0"?$3:1/0) title "Process 0"
    • filename 변수를 설정하고, 그래프로 나타낸다.

    • $1은 첫 번째 열의 값을 나타내고, 삼항 연산자로 조건문을 만들 수 있다.

      • 값이 "0"이면 3번째 열을 표시한다.

      • "0"이 아니면 표시하지 않는다. (1/0 : 표시 X)

    • title : 해당 그래프의 key로 나타낼 명칭을 설정한다.


참고

'Linux > Linux Structure' 카테고리의 다른 글

Chapter 5. 메모리 관리  (3) 2021.09.26
Chapter 4. 프로세스 스케줄러  (0) 2021.09.26
Chapter 3. 프로세스 관리  (1) 2021.09.26
Chapter 2. 사용자 모드로 구현되는 기능  (0) 2021.09.26
리눅스 sar 명령어  (0) 2021.09.26
Linux/Linux Structure 2021. 9. 26. 02:55

Chapter 3. 프로세스 관리

프로세스 생성의 목적

  • 목적 1 : 같은 프로그램의 처리를 여러 프로세스가 분산해서 처리 (웹 서버의 리퀘스트 처리 등)

  • 목적 2 : 전혀 다른 프로그램 생성 (bash에서 여러 프로그램 실행 등)

  • 위의 작업을 할 때 fork() 함수와 execve() 함수가 호출되며, 내부적으로는 clone()execve() 시스템 콜이 호출된다.


fork() 함수

  • 실행 중인 프로세스에서 자식 프로세스를 생성한다.

프로세스 생성 순서

  1. 자식 프로세스용 메모리 영역을 작성하고 부모 프로세스의 메모리를 복사한다.

  2. fork() 함수의 리턴 값이 다른 것을 이용하여 코드를 분기한다.

fork.c 프로그램 예제

  1. 프로세스를 새로 만든다.
  2. 부모 프로세스는 자신의 PIDProcess ID와 자식 프로세스의 PID를 출력한다.
$ gcc -o output/fork src/fork.c
$ output/fork
부모 프로세스의 PID : 13202, 자식 프로세스의 PID : 13203
자식 프로세스의 PID : 13203
  • 부모 프로세스(13202)가 분기되어 자식 프로세스(13203)이 생성된 것을 볼 수 있고, 각각의 출력을 통해 두 프로세스 처리가 분기되어 실행되는 것을 알 수 있다.

execve() 함수

  • 전혀 다른 프로세스를 생성하는 함수이다.

함수의 흐름

  1. 실행 파일을 읽어서 메모리 맵에 정보를 로딩한다.
  2. 현재 프로세스의 메모리 영역을 새 프로세스의 데이터로 덮어쓴다.
  3. 새로운 프로세스의 첫 번째 명령부터 실행한다.
  • 프로세스의 수가 증가하는 것이 아니고, 현재 프로세스를 다른 프로세스로 변경하는 것이다.

실행 파일의 구성 요소

  • 실행 파일은 코드와 데이터 외에도 다음과 같은 정보들이 필요하다.

    • 코드 영역의 파일상 오프셋, 사이즈, 메모리 맵 시작 주소

    • 데이터 영역의 파일상 오프셋, 사이즈, 메모리 맵 시작 주소

    • 엔트리 포인트 (최초로 실행할 명령의 주소)

  • 프로그램을 실행하면 위의 정보들을 바탕으로 메모리에 매핑하고, 엔트리 포인트부터 명령을 실행한다.

  • 리눅스의 실행 파일은 ELFExecutable and Linkable Format 형식을 사용하는데, 이는 readelf 명령어로 볼 수 있다.

  • -h 옵션 : ELF 파일 헤더 출력. 엔트리 포인트를 볼 수 있다.

    $ readelf -h /bin/sleep
    ELF Header:
      Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
      Class:                             ELF64
      Data:                              2's complement, little endian
      Version:                           1 (current)
      OS/ABI:                            UNIX - System V
      ABI Version:                       0
      Type:                              DYN (Shared object file)
      Machine:                           Advanced Micro Devices X86-64
      Version:                           0x1
      Entry point address:               0x2850
      Start of program headers:          64 (bytes into file)
      Start of section headers:          37336 (bytes into file)
      Flags:                             0x0
      Size of this header:               64 (bytes)
      Size of program headers:           56 (bytes)
      Number of program headers:         13
      Size of section headers:           64 (bytes)
      Number of section headers:         30
      Section header string table index: 29
    • Entry point address 항목이 엔트리 포인트의 주소이다.
  • -S 옵션 : 섹션 헤더 출력. 코드와 데이터 영역의 파일상 오프셋, 사이즈, 메모리 맵 시작 주소를 볼 수 있다.

    $ readelf -S /bin/sleep
    There are 30 section headers, starting at offset 0x91d8:
    
    Section Headers:
      [Nr] Name              Type             Address           Offset
          Size              EntSize          Flags  Link  Info  Align
      [ 0]                   NULL             0000000000000000  00000000
          0000000000000000  0000000000000000           0     0     0
      [ 1] .interp           PROGBITS         0000000000000318  00000318
          000000000000001c  0000000000000000   A       0     0     1
      [ 2] .note.gnu.propert NOTE             0000000000000338  00000338
          0000000000000020  0000000000000000   A       0     0     8
      [ 3] .note.gnu.build-i NOTE             0000000000000358  00000358
          0000000000000024  0000000000000000   A       0     0     4
      [ 4] .note.ABI-tag     NOTE             000000000000037c  0000037c
          0000000000000020  0000000000000000   A       0     0     4
      [ 5] .gnu.hash         GNU_HASH         00000000000003a0  000003a0
          00000000000000a8  0000000000000000   A       6     0     8
      [ 6] .dynsym           DYNSYM           0000000000000448  00000448
          0000000000000600  0000000000000018   A       7     1     8
      [ 7] .dynstr           STRTAB           0000000000000a48  00000a48
          000000000000031f  0000000000000000   A       0     0     1
      [ 8] .gnu.version      VERSYM           0000000000000d68  00000d68
          0000000000000080  0000000000000002   A       6     0     2
      [ 9] .gnu.version_r    VERNEED          0000000000000de8  00000de8
          0000000000000060  0000000000000000   A       7     1     8
      [10] .rela.dyn         RELA             0000000000000e48  00000e48
          00000000000002b8  0000000000000018   A       6     0     8
      [11] .rela.plt         RELA             0000000000001100  00001100
          00000000000003f0  0000000000000018  AI       6    25     8
      [12] .init             PROGBITS         0000000000002000  00002000
          000000000000001b  0000000000000000  AX       0     0     4
      [13] .plt              PROGBITS         0000000000002020  00002020
          00000000000002b0  0000000000000010  AX       0     0     16
      [14] .plt.got          PROGBITS         00000000000022d0  000022d0
          0000000000000010  0000000000000010  AX       0     0     16
      [15] .plt.sec          PROGBITS         00000000000022e0  000022e0
          00000000000002a0  0000000000000010  AX       0     0     16
      [16] .text             PROGBITS         0000000000002580  00002580
          0000000000003692  0000000000000000  AX       0     0     16
      [17] .fini             PROGBITS         0000000000005c14  00005c14
          000000000000000d  0000000000000000  AX       0     0     4
      [18] .rodata           PROGBITS         0000000000006000  00006000
          0000000000000f6c  0000000000000000   A       0     0     32
      [19] .eh_frame_hdr     PROGBITS         0000000000006f6c  00006f6c
          00000000000002b4  0000000000000000   A       0     0     4
      [20] .eh_frame         PROGBITS         0000000000007220  00007220
          0000000000000d18  0000000000000000   A       0     0     8
      [21] .init_array       INIT_ARRAY       0000000000009bb0  00008bb0
          0000000000000008  0000000000000008  WA       0     0     8
      [22] .fini_array       FINI_ARRAY       0000000000009bb8  00008bb8
          0000000000000008  0000000000000008  WA       0     0     8
      [23] .data.rel.ro      PROGBITS         0000000000009bc0  00008bc0
          00000000000000b8  0000000000000000  WA       0     0     32
      [24] .dynamic          DYNAMIC          0000000000009c78  00008c78
          00000000000001f0  0000000000000010  WA       7     0     8
      [25] .got              PROGBITS         0000000000009e68  00008e68
          0000000000000190  0000000000000008  WA       0     0     8
      [26] .data             PROGBITS         000000000000a000  00009000
          0000000000000080  0000000000000000  WA       0     0     32
      [27] .bss              NOBITS           000000000000a080  00009080
          00000000000001b8  0000000000000000  WA       0     0     32
      [28] .gnu_debuglink    PROGBITS         0000000000000000  00009080
          0000000000000034  0000000000000000           0     0     4
      [29] .shstrtab         STRTAB           0000000000000000  000090b4
          000000000000011d  0000000000000000           0     0     1
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
      L (link order), O (extra OS processing required), G (group), T (TLS),
      C (compressed), x (unknown), o (OS specific), E (exclude),
      l (large), p (processor specific)
    • [16] .text 영역과 [26] .data 영역이 각각 코드와 데이터 영역의 정보이다.

    • Address : 메모리 맵 시작 주소

    • Offset : 파일상 오프셋

    • Size : 사이즈

  • /bin/sleep 파일의 정보는 위의 내용에 의하면 다음과 같다.

    구성 값 (16진수)
    엔트리 포인트 0x2850
    코드 영역의 메모리 맵 시작 주소 0000000000002580
    코드 영역의 파일상 오프셋 00002580
    코드 영역의 사이즈 0000000000003692
    데이터 영역의 메모리 맵 시작 주소 000000000000a000
    데이터 영역의 파일상 오프셋 00009000
    데이터 영역의 사이즈 0000000000000080
  • /proc/PID번호/maps 파일을 통해 프로세스의 메모리 맵을 볼 수 있다.

    $ /bin/sleep 10000 &
    [1] 16292
    $ cat /proc/16292/maps
    55addbc8d000-55addbc8f000 r--p 00000000 103:05 2098267                   /usr/bin/sleep
    55addbc8f000-55addbc93000 r-xp 00002000 103:05 2098267                   /usr/bin/sleep  # 코드 영역
    55addbc93000-55addbc95000 r--p 00006000 103:05 2098267                   /usr/bin/sleep
    55addbc96000-55addbc97000 r--p 00008000 103:05 2098267                   /usr/bin/sleep
    55addbc97000-55addbc98000 rw-p 00009000 103:05 2098267                   /usr/bin/sleep  # 데이터 영역
    55addcff5000-55addd016000 rw-p 00000000 00:00 0                          [heap]
    7f27b5a7a000-7f27b612f000 r--p 00000000 103:05 2097617                   /usr/lib/locale/locale-archive
    7f27b612f000-7f27b6154000 r--p 00000000 103:05 2099337                   /usr/lib/x86_64-linux-gnu/libc-2.31.so
    7f27b6154000-7f27b62cc000 r-xp 00025000 103:05 2099337                   /usr/lib/x86_64-linux-gnu/libc-2.31.so
    7f27b62cc000-7f27b6316000 r--p 0019d000 103:05 2099337                   /usr/lib/x86_64-linux-gnu/libc-2.31.so
    7f27b6316000-7f27b6317000 ---p 001e7000 103:05 2099337                   /usr/lib/x86_64-linux-gnu/libc-2.31.so
    7f27b6317000-7f27b631a000 r--p 001e7000 103:05 2099337                   /usr/lib/x86_64-linux-gnu/libc-2.31.so
    7f27b631a000-7f27b631d000 rw-p 001ea000 103:05 2099337                   /usr/lib/x86_64-linux-gnu/libc-2.31.so
    7f27b631d000-7f27b6323000 rw-p 00000000 00:00 0 
    7f27b6334000-7f27b6335000 r--p 00000000 103:05 2099333                   /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f27b6335000-7f27b6358000 r-xp 00001000 103:05 2099333                   /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f27b6358000-7f27b6360000 r--p 00024000 103:05 2099333                   /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f27b6361000-7f27b6362000 r--p 0002c000 103:05 2099333                   /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f27b6362000-7f27b6363000 rw-p 0002d000 103:05 2099333                   /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f27b6363000-7f27b6364000 rw-p 00000000 00:00 0 
    7ffd9fb3e000-7ffd9fb5f000 rw-p 00000000 00:00 0                          [stack]
    7ffd9fb67000-7ffd9fb6b000 r--p 00000000 00:00 0                          [vvar]
    7ffd9fb6b000-7ffd9fb6d000 r-xp 00000000 00:00 0                          [vdso]
    ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]
    
    $ kill 16292
    [1]  + 16292 terminated  /bin/sleep 10000 
    • 각 항목은 다음과 같은 순서이다.

      address   perms   offset   dev   inode   pathname
    • 코드 영역 : r-xp로 읽기, 실행 권한이 주어진다.

    • 데이터 영역 : rw-p로 읽기, 쓰기 권한이 주어진다.

    • 책이나 인터넷에서는 address 부분이 가상 메모리로 깔끔하게 00400000 처럼 주어지는데, 나는 왜 저렇게 나오는지 모르겠다.

    • offset을 참고해서 보면, 코드 영역과 데이터 영역의 메모리 맵 시작 주소가 해당 범위에 들어가 있음을 볼 수 있다.

    • 참고 : https://stackoverflow.com/questions/1401359/understanding-linux-proc-pid-maps-or-proc-self-maps

fork and exec

  • 전혀 다른 프로세스를 생성할 때는, 부모가 될 프로세스로부터 fork() 함수를 호출한 다음 자식 프로세스가 exec() 함수를 호출하는 방식을 주로 사용한다.

    $ gcc -o output/fork-and-exec src/fork-and-exec.c
    $ output/fork-and-exec
    부모 프로세스의 PID : 18241, 자식 프로세스의 PID : 18242
    자식 프로세스의 PID : 18242
    hello
  • 파이썬에서는 OS.exec() 함수를 통해 execve() 함수를 호출할 수 있다.


종료 처리

  • 프로그램 종료는 _exit() 함수를 통해 이루어진다.

    • 내부적으로 exit_group() 시스템 콜을 호출한다.

    • 이를 통해 프로세스에 할당된 메모리를 전부 회수한다.

  • 보통은 libc의 exit() 함수를 호출하여 종료하며, 이 함수는 자신의 종료 처리를 전부 수행한 후 _exit() 함수를 호출한다.

    • main() 함수로부터 리턴된 경우도 같은 동작을 수행한다.

참고

'Linux > Linux Structure' 카테고리의 다른 글

Chapter 4. 프로세스 스케줄러  (0) 2021.09.26
Gnuplot  (0) 2021.09.26
Chapter 2. 사용자 모드로 구현되는 기능  (0) 2021.09.26
리눅스 sar 명령어  (0) 2021.09.26
Chapter 1. 컴퓨터 시스템의 개요  (0) 2021.09.26
Linux/Linux Structure 2021. 9. 26. 02:54

Chapter 2. 사용자 모드로 구현되는 기능

시스템 콜

  • 프로세스는 커널의 도움이 필요한 경우 (프로세스 생성, 하드웨어 조작 등) 시스템 콜을 통해 커널에 처리를 요청한다.

  • 시스템 콜의 종류

    • 프로세스 생성, 삭제

    • 메모리 확보, 해제

    • 프로세스 간 통신 (IPC)

    • 네트워크

    • 파일시스템 다루기

    • 파일 다루기(디바이스 접근)


CPU의 모드 변경

  • 프로세스는 유저 모드(사용자 모드)에서 실행되다가, 시스템 콜을 호출하면 CPU에서 인터럽트 이벤트가 발생한다.

  • 이 인터럽트 이벤트가 발생하면 CPU는 커널 모드로 변경되어 동작하고, 처리가 끝나면 다시 유저 모드로 변경된다.

  • 커널은 이 요청을 처리하기 전에 요구 사항의 유효성을 검사하여, 유효하지 않은 요청의 경우 시스템 콜을 실패했다고 처리한다.

    ex. 메모리를 허용량 이상 요구하는 경우 등

  • 유저 모드에서 시스템 콜을 통하지 않고 커널 모드로 변경하는 방법은 없다.


시스템 콜 동작 순서

  • strace 명령어로 실행 파일의 시스템 콜 호출 로그를 출력해서 확인할 수 있다.

  • sar 명령어

strace - hello.c

  • hello.c 컴파일

    $ gcc -o output/hello src/hello.c
    $ output/hello
    Hello world
  • strace 로 시스템 콜 확인

    $ strace -o log/hello.log output/hello
    Hello world
  • hello.log 파일의 34번째 줄에서 write 시스템 콜을 통해 화면에 출력하는 것을 알 수 있다.

strace - hello.py

  • strace로 hello.py 실행

    $ strace -o log/hello.py.log python src/hello.py
    Hello world
  • hello.py.log 파일의 516번째 줄에서 똑같이 write 시스템 콜을 호출하는 것을 볼 수 있다.

    • C언어보다 파이썬이 상대적으로 느리다는 것은 알고 있었는데, 생각보다 파이썬의 처리량이 훨씬 많다.

sar - loop.c

  • 시스템 콜을 호출하지 않고 무한루프를 도는 프로그램이다.

    $ gcc -o output/loop src/loop.c
    $ output/loop &                 # 백그라운드로 실행
    [1] 30132
    
    $ sar -P ALL 1 1
    Linux 5.11.0-27-generic (ubun2-GL63-8RC)     2021년 09월 06일     _x86_64_    (8 CPU)
    
    01시 31분 19초     CPU     %user     %nice   %system   %iowait    %steal     %idle
    01시 31분 20초     all      0.13     12.59      0.00      0.00      0.00     87.28
    01시 31분 20초       0      0.00      0.00      0.00      0.00      0.00    100.00
    01시 31분 20초       1      0.00    100.00      0.00      0.00      0.00      0.00 # 여기
    01시 31분 20초       2      0.00      0.00      0.00      0.00      0.00    100.00
    01시 31분 20초       3      0.00      0.00      0.00      0.00      0.00    100.00
    01시 31분 20초       4      0.00      0.00      0.00      0.00      0.00    100.00
    01시 31분 20초       5      0.00      0.00      0.00      0.00      0.00    100.00
    01시 31분 20초       6      0.00      0.00      0.00      0.00      0.00    100.00
    01시 31분 20초       7      0.99      0.00      0.00      0.00      0.00     99.01
    ...
    $ kill 30132                    # 실행한 프로세스 종료
    [1]  + 30132 terminated  output/loop
  • 1번 프로세서에서 nice가 100%이고 system은 0%임을 볼 수 있다. 즉 유저 모드에서만 돌아가는 프로그램이라는 뜻이다.

    • user와 nice는 모두 유저 모드에서 작동한 시간의 비율을 나타내는데, nice는 프로세스의 우선순위와 관련있다.

sar - ppidloop.c

  • getppid() : 부모 프로세스의 PID(Process ID)를 얻는 시스템 콜

    $ gcc -o output/ppidloop src/ppidloop.c
    $ output/ppidloop &
    [1] 31624
    
    $ sar -P ALL 1 1
    Linux 5.11.0-27-generic (ubun2-GL63-8RC)     2021년 09월 06일     _x86_64_    (8 CPU)
    
    01시 44분 03초     CPU     %user     %nice   %system   %iowait    %steal     %idle
    01시 44분 04초     all      0.00      8.53      4.14      0.00      0.00     87.33
    01시 44분 04초       0      0.00      0.00      1.01      0.00      0.00     98.99
    01시 44분 04초       1      0.00      0.00      0.00      0.00      0.00    100.00
    01시 44분 04초       2      0.00     68.00     32.00      0.00      0.00      0.00 # 여기
    01시 44분 04초       3      0.00      0.00      0.00      0.00      0.00    100.00
    01시 44분 04초       4      0.00      0.00      0.00      0.00      0.00    100.00
    01시 44분 04초       5      0.00      0.00      0.00      0.00      0.00    100.00
    01시 44분 04초       6      0.00      0.00      0.00      0.00      0.00    100.00
    01시 44분 04초       7      0.00      0.00      0.00      0.00      0.00    100.00
    ...
    $ kill 31624
    [1]  + 31624 terminated  output/ppidloop
  • 아까의 결과는 nice가 100%였던 것에 비해, 이번에는 system이 약 32%에 도달한 것을 볼 수 있다. (값은 실행 환경에 따라 다르다.)

시스템 콜의 소요 시간

  • strace 명령어에 -T 옵션을 주면 시스템 콜 처리에 걸린 시간을 마이크로초까지 정밀하게 볼 수 있다. (단위는 초)

    $ strace -T -o log/hello_T.log output/hello
    Hello world
    • hello_T.log 파일을 보면 34번째 줄의 write 시스템 콜에서 0.000018초(18us)인 것을 볼 수 있다.

      • 출력 함수가 되게 느린 줄 알았는데 생각보다 빠르다.
  • strace 명령어에 -tt 옵션을 주면 시스템 콜이 호출된 시간을 볼 수 있다.

    $ strace -tt -o log/hello_tt.log output/hello
    Hello world
    $ strace -T -tt -o log/hello_T_tt.log output/hello
    Hello world

시스템 콜의 wrapper 함수

어셈블리 코드

  • 시스템 콜은 일반 함수 호출과는 다르게, 아키텍처에 의존하는 어셈블리 코드를 통해 호출해야 한다.

    • x86_64 아키텍처에서 getppid() 시스템 콜은 아래처럼 호출된다.

      mov $0x6e, $eax
      syscall
      • eax 레지스터에 getppid의 시스템 콜 번호 0x6e를 넣고 시스템 콜을 호출하면서 커널 모드로 전환하는 것이다.
  • 어셈블리 코드는 아키텍처에 의존하기 때문에 각 아키텍처마다 시스템 콜을 호출하는 방식이 다르고, 이를 통일하기 위해 OS에서 wrapper 함수를 사용한다.

    • 아키텍처 별로 어셈블리 코드를 만드는 것보다, OS에 있는 wrapper 함수를 사용하는게 효율적이다.

시스템 콜 wrapper

  • OS에서 내부적으로 시스템 콜만 호출하는 함수를 의미한다.

  • 각 아키텍처마다 존재하며, 고급 언어로 쓰여진 프로그램에서 해당 wrapper 함수를 호출하기만 하면 되므로 이식성이 높다.

표준 C 라이브러리

  • C언어에는 ISO에 의해 정해진 표준 라이브러리가 존재하며, 대부분의 C 프로그램은 GNU 프로젝트가 제공하는 glibc를 링크한다.

  • glibc는 시스템 콜의 wrapper 함수를 포함하고, POSIX 규격에 정의된 함수도 포함한다.

  • ldd 명령어

    • 프로그램이 어떤 라이브러리를 링크하는지 확인할 수 있는 명령어이다.

      $ ldd /bin/echo
      linux-vdso.so.1 (0x00007ffd50bcd000)
      libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f15598f6000)
      /lib64/ld-linux-x86-64.so.2 (0x00007f1559b06000)
      
      $ ldd output/ppidloop 
      linux-vdso.so.1 (0x00007ffeceb86000)
      libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f376d2e5000)
      /lib64/ld-linux-x86-64.so.2 (0x00007f376d4ef000)
      
      $ ldd /usr/bin/python
      linux-vdso.so.1 (0x00007fff397f1000)
      libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1cebea0000) # 파이썬도 내부적으로 libc를 사용한다.
      libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f1cebe7d000)
      libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f1cebe77000)
      libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f1cebe72000)
      libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f1cebd23000)
      libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007f1cebcf5000)
      libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f1cebcd7000)
      /lib64/ld-linux-x86-64.so.2 (0x00007f1cec0a5000)
      
      $ ldd /usr/bin/ls
      linux-vdso.so.1 (0x00007ffc42e54000)
      libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f6806574000)
      libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6806382000)
      libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f68062f2000)
      libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f68062ec000)
      /lib64/ld-linux-x86-64.so.2 (0x00007f68065d7000)
      libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f68062c9000)
      
      $ ldd /usr/bin/cat
      linux-vdso.so.1 (0x00007ffec5f46000)
      libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa363535000)
      /lib64/ld-linux-x86-64.so.2 (0x00007fa363746000)
      
      $ ldd /usr/bin/pwd
      linux-vdso.so.1 (0x00007fff9ab54000)
      libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f242d8e0000)
      /lib64/ld-linux-x86-64.so.2 (0x00007f242daf1000)
      • 자주 사용하는 명령어들이 대부분 libc를 링크하는 것을 볼 수 있다.

OS가 제공하는 프로그램

  • 대부분의 프로그램이 필요로 하는, OS의 동작을 변경시키는 프로그램도 OS의 일부로써 제공된다.

  • 목록

    용도 프로그램
    시스템 초기화 init
    OS의 동작을 바꿈 sysctl, nice, sync
    파일 관련 touch, mkdir
    텍스트 데이터 가공 grep, sort, uniq
    성능 측정 sar, iostat
    컴파일러 gcc
    스크립트 언어 실행 환경 perl, python, ruby
    bash
    윈도우 시스템 X

참고

'Linux > Linux Structure' 카테고리의 다른 글

Chapter 4. 프로세스 스케줄러  (0) 2021.09.26
Gnuplot  (0) 2021.09.26
Chapter 3. 프로세스 관리  (1) 2021.09.26
리눅스 sar 명령어  (0) 2021.09.26
Chapter 1. 컴퓨터 시스템의 개요  (0) 2021.09.26
Linux/Linux Structure 2021. 9. 26. 02:53

리눅스 sar 명령어

  • System Activity Report

  • 시스템을 모니터링할 때 사용되는 명령어이다.

-P 옵션

  • processor를 의미한다.

    $ sar -P CPU번호 주기(초)
  • 일정 주기(초)마다 해당 코어의 활동을 볼 수 있다.

    $ sar -P 0 1      # 0번 프로세서에 대한 정보를 1초마다 확인
    Linux 5.11.0-27-generic (ubun2-GL63-8RC)     2021년 09월 06일     _x86_64_    (8 CPU)
    
    01시 02분 52초     CPU     %user     %nice   %system   %iowait    %steal     %idle
    01시 02분 53초       0      5.05      0.00      2.02      0.00      0.00     92.93
    01시 02분 54초       0      4.95      0.00      1.98      0.00      0.00     93.07
    01시 02분 55초       0      4.17      0.00      2.08      0.00      0.00     93.75
    ^C                # Ctrl + C 로 실행을 멈추면 아직까지의 평균값을 출력한다.
    
    평균값:        0      4.73      0.00      2.03      0.00      0.00     93.24
  • CPU번호 대신 ALL 키워드(대문자)를 입력하면 모든 프로세서에 대한 값을 볼 수 있다.

    $ sar -P ALL 1
    Linux 5.11.0-27-generic (ubun2-GL63-8RC)     2021년 09월 06일     _x86_64_    (8 CPU)
    
    01시 06분 46초     CPU     %user     %nice   %system   %iowait    %steal     %idle
    01시 06분 47초     all      6.06      0.00      1.77      0.00      0.00     92.17
    01시 06분 47초       0      4.04      0.00      1.01      0.00      0.00     94.95
    01시 06분 47초       1     11.00      0.00      3.00      0.00      0.00     86.00
    01시 06분 47초       2     13.00      0.00      1.00      0.00      0.00     86.00
    01시 06분 47초       3      6.12      0.00      1.02      0.00      0.00     92.86
    01시 06분 47초       4      5.00      0.00      4.00      0.00      0.00     91.00
    01시 06분 47초       5      2.94      0.00      2.94      0.00      0.00     94.12
    01시 06분 47초       6      2.02      0.00      1.01      0.00      0.00     96.97
    01시 06분 47초       7      4.26      0.00      0.00      0.00      0.00     95.74
  • 네 번째 인자로 측정 횟수를 입력할 수 있다.

    sar -P ALL 1 1
    Linux 5.11.0-27-generic (ubun2-GL63-8RC)     2021년 09월 06일     _x86_64_    (8 CPU)
    
    01시 13분 35초     CPU     %user     %nice   %system   %iowait    %steal     %idle
    01시 13분 36초     all      1.13      0.00      0.38      0.00      0.00     98.49
    01시 13분 36초       0      0.00      0.00      0.00      0.00      0.00    100.00
    01시 13분 36초       1      0.00      0.00      0.00      0.00      0.00    100.00
    01시 13분 36초       2      1.96      0.00      0.98      0.00      0.00     97.06
    01시 13분 36초       3      2.02      0.00      1.01      0.00      0.00     96.97
    01시 13분 36초       4      3.00      0.00      1.00      0.00      0.00     96.00
    01시 13분 36초       5      1.03      0.00      0.00      0.00      0.00     98.97
    01시 13분 36초       6      0.00      0.00      0.00      0.00      0.00    100.00
    01시 13분 36초       7      1.00      0.00      0.00      0.00      0.00     99.00
    
    평균값:      CPU     %user     %nice   %system   %iowait    %steal     %idle
    평균값:      all      1.13      0.00      0.38      0.00      0.00     98.49
    평균값:        0      0.00      0.00      0.00      0.00      0.00    100.00
    평균값:        1      0.00      0.00      0.00      0.00      0.00    100.00
    평균값:        2      1.96      0.00      0.98      0.00      0.00     97.06
    평균값:        3      2.02      0.00      1.01      0.00      0.00     96.97
    평균값:        4      3.00      0.00      1.00      0.00      0.00     96.00
    평균값:        5      1.03      0.00      0.00      0.00      0.00     98.97
    평균값:        6      0.00      0.00      0.00      0.00      0.00    100.00
    평균값:        7      1.00      0.00      0.00      0.00      0.00     99.00

항목

  • %user

  • %nice

  • %system

    • 커널 모드에서 시스템 콜 등의 처리를 실행하는 시간의 비율이다.

'Linux > Linux Structure' 카테고리의 다른 글

Chapter 4. 프로세스 스케줄러  (0) 2021.09.26
Gnuplot  (0) 2021.09.26
Chapter 3. 프로세스 관리  (1) 2021.09.26
Chapter 2. 사용자 모드로 구현되는 기능  (0) 2021.09.26
Chapter 1. 컴퓨터 시스템의 개요  (0) 2021.09.26
Linux/Linux Structure 2021. 9. 26. 02:53

Chapter 1. 컴퓨터 시스템의 개요

프로그램

  • 컴퓨터 시스템이 동작할 떄 하드웨어에서 다음의 순서가 반복된다.

    • 입력 장치 또는 네트워크 어댑터를 통해 처리 요청이 들어온다.

    • 메모리의 명령을 읽어 CPU를 실행하고 결과를 다시 메모리에 기록한다.

    • 메모리의 데이터를 저장 장치에 기록하거나 네트워크를 통해 전송, 혹은 디스플레이에 출력하여 처리한다.

  • 프로그램

    • 위의 과정을 반복하여 사용자에게 필요한 하나의 기능으로 정리한 것이다.

    • 종류

      • 애플리케이션 : 사용자가 직접 사용한다. 오피스 프로그램이나 스마트폰의 앱 등이 있다.

      • 미들웨어 : 여러 애플리케이션이 공통으로 사용하는 처리를 묶어서 애플리케이션의 실행을 도와준다. 웹 서버, 데이터베이스 등이 있다.

      • 운영체제(OS) : 하드웨어를 직접 조작하여 애플리케이션이나 미들웨어에 필요한 기능을 제공한다. 리눅스, 윈도우, MacOS 등이 있다.

    • 운영체제는 여러 프로그램을 프로세스라는 단위로 실행한다.

디바이스 드라이버

  • OS가 없는 경우, 여러 프로세스가 각각 디바이스를 조작하는 코드를 작성해야 한다.

    • 모든 애플리케이션 개발자가 디바이스 스펙을 상세히 알아야 디바이스를 조작할 수 있다.

    • 개별 개발이므로 비용이 증가한다.

    • 여러 프로세스가 동시에 디바이스를 조작할 경우 의도치 않은 동작이 발생할 수 있다.

  • 위의 단점을 해결하기 위해 리눅스는 디바이스 드라이버를 통해서만 디바이스를 조작할 수 있다.

    • 여러 프로세스가 동시에 디바이스를 조작하는 상황을 방지하기 위해 프로세스가 직접 하드웨어에 접근하는 것을 차단한다.

      • CPU는 커널 모드, 유저 모드가 있으며 커널 모드로 동작할 때만 디바이스에 접근할 수 있다.
  • 디바이스의 종류가 같으면 같은 인터페이스로 조작하도록 되어있다.

    ex. 입출력 장치 공통 처리, 저장 장치 공통 처리, 네트워크 어댑터 공통 처리 등


커널

  • 디바이스 조작 외에도 일반적인 프로세스로 실행하면 문제가 되는 처리가 몇 가지 존재한다.

    ex. 프로세스 관리 시스템, 프로세스 스케줄링, 메모리 관리 시스템 등

    • 이러한 처리도 커널 모드에서 동작하며, 커널 모드에서 동작하는 OS의 핵심 처리를 모아 담당하는 프로그램을 커널이라고 한다.
  • 시스템 콜

    • 프로세스가 커널이 제공하는 기능을 사용하려 할 때는 시스템 콜을 사용하여 커널에 요청한다.
  • OS는 커널뿐만 아니라 사용자 모드에서 동작하는 다양한 프로그램을 의미한다.

  • 커널은 CPU나 메모리 등의 리소스를 관리하며, 각 프로세스에 리소스를 적절히 분배한다.


프로세스 실행

  • 프로세스 실행 시 여러 데이터가 메모리를 중심으로 CPU의 레지스터나 저장 장치 같은 기억장치 사이에서 전송된다.

    • 기억장치 계층이라고 불리는 계층 구조가 있으며, 각 계층은 크기나 가격, 전송 속도에서 장단점이 있다.
  • 저장 장치에 보관된 데이터는 디바이스 드라이버에 직접 요청해서 접근하기 보다는 파일시스템이라고 하는 프로그램을 통해 접근한다.

  • 시스템이 작동하려면 저장 장치에서 OS에 대한 정보를 읽어야 한다.


참고

'Linux > Linux Structure' 카테고리의 다른 글

Chapter 4. 프로세스 스케줄러  (0) 2021.09.26
Gnuplot  (0) 2021.09.26
Chapter 3. 프로세스 관리  (1) 2021.09.26
Chapter 2. 사용자 모드로 구현되는 기능  (0) 2021.09.26
리눅스 sar 명령어  (0) 2021.09.26
Linux/Error 2021. 4. 30. 22:19

SSH 접속 시 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ECDSA key sent by the remote host is
SHA256:~~~.
Please contact your system administrator.
Add correct host key in ~/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in ~/.ssh/known_hosts:4
Host key for IP has changed and you have requested strict checking.
Host key verification failed.
  • 이미 known_hosts 파일에 등록된 IP에 대해 다른 공개키를 전송받은 경우 발생하는 문제이다.

  • .ssh/known_hosts 파일에서 해당 IP를 지워주면 된다.

Linux 2021. 4. 23. 02:00

우분투 oh-my-zsh 설치

설치

.zshrc

  • ~/.zshrc 파일에서 테마를 지정해줄 수 있다.

    ZSH_THEME="agnoster"
  • 맨 앞에 뜨는 사용자명을 생략하려면 ~/.zshrc 파일 맨 아래에 DEFAULT_USER=유저명을 작성한다.

    • 유저명 보는 법

      $ whoami
      ubun2
  • 터미널에 source ~/.zshrc를 입력하면 적용된다.


agnoster 테마 사용 시 폰트 깨짐

  • 터미널에 다음을 입력

    $ sudo apt install fonts-powerline
  • 재시작하면 폰트가 적용된다.


우분투 VSCode 터미널에서 폰트 깨짐

  • VSCode의 설정(Ctrl + ,)에서 terminal font를 검색하면 Terminal > Intergrated: Font Family가 뜨는데, 거기에 위에서 설치한 'Ubuntu Mono', 'PowerlineSymbols'를 입력한다. (작은 따옴표까지)

  • 재시작하면 폰트가 적용된다.

'Linux' 카테고리의 다른 글

Solaris 10 ps command 전체 출력  (0) 2022.11.08
VSCode Remote-SSH 플러그인 에러  (0) 2021.04.02
Vim  (0) 2021.03.08
Linux 2021. 4. 2. 23:13

VSCode Remote-SSH 플러그인 에러

  • 갑자기 VSCode로 원격 접속이 안되면서 The process tried to write to a nonexistent pipe 에러가 발생했다.

  • VSCode 설정에서 Remote-SSH 부분의 Config File을 설정해주니 파일이 생성되면서 해결되었다.

참고

'Linux' 카테고리의 다른 글

Solaris 10 ps command 전체 출력  (0) 2022.11.08
우분투 oh-my-zsh 설치  (0) 2021.04.23
Vim  (0) 2021.03.08
Linux 2021. 3. 8. 23:04

Vim

Vim에서 방향키가 제대로 작동하지 않을 때

  • vim으로 편집하는데, 방향키가 제대로 적용이 안된다면 다시 설치해야 한다.

    sudo apt install vim -y

Vim에서 클립보드 사용

  • Vim으로 편집할 때 클립보드로 복사가 안되는 현상이 있었다.

  • vi --version | grep clip으로 확인하면 -clipboard라고 나온다.

  • vim-gtk를 설치하면 된다.

    sudo apt install vim-gtk -y

'Linux' 카테고리의 다른 글

Solaris 10 ps command 전체 출력  (0) 2022.11.08
우분투 oh-my-zsh 설치  (0) 2021.04.23
VSCode Remote-SSH 플러그인 에러  (0) 2021.04.02