그림으로 배우는 Linux 구조 - 3 / 프로세스와 CPU
By Bys on January 3, 2024
Process Scheduler
시스템에 존재하는 프로세스는 대부분 슬립 상태라고 설명했습니다.
- 하나의 논리 CPU는 동시에 하나의 프로세스만 처리합니다.
- 실행 가능한 여러 프로세스가 Time slice 단위로 순서대로 CPU를 사용합니다.
경과 시간과 사용 시간
- 경과 시간: 프로세스 시작부터 종료할 때까지 경과한 시간. 초시계로 프로세스 시작부터 종료할 때까지 측정한 값에 해당합니다.
- 사용 시간: 프로세스가 실제로 논리 CPU를 사용한 시간
time 명령어를 사용해서 프로세스를 실행하면 대상 프로세스의 시작부터 종료까지 경과 시간과 사용 시간을 알 수 있습니다.
#!/usr/bin/python3
# 부하 정도를 조절하는 값
NLOOP=100000000
for _ in range(NLOOP):
pass
$ time ./load.py
real 0m3.974s
user 0m3.967s
sys 0m0.005s
출력 결과를 보면 real, user, sys를 볼 수 있습니다. real은 경과 시간, user와 sys는 사용시간을 뜻 합니다. user는 프로세스가 사용자 공간에서 동작한 시간을 뜻하고, sys는 프로세스의 시스템 콜 호출 때문에 늘어난 커널이 동작한 시간을 뜻 합니다.
load.py 프로그램은 프로세스 시작부터 끝날 때까지 CPU를 계속해서 사용하지만 시스템 콜을 호출하지 않으므로 real과 user는 거의 동일한 값이고 sys는 거의 0입니다. ‘거의’인 이유는 프로세스를 시작하거나 종료할 때 파이썬 인터프리터가 몇 종류의 시스템 콜을 호출하기 때문 입니다.
$ time sleep 3
real 0m3.004s
user 0m0.002s
sys 0m0.000s
시작 후 3초 동안 기다렸으니 real은 약 3초입니다. 한편 이 명령어는 CPU를 사용하는 일 없이 슬립 상태에 들어가서 3초 뒤에 다시 CPU를 사용하지만 곧바로 종료하므로 user와 sys는 거의 0입니다.
- 경과시간 = real
- 사용시간 = user+sys
- 경과시간(real) = 슬립상태 + 실행 상태(user+sys)
논리 CPU 하나만 사용하는 경우
multiload.sh
#!/bin/bash
MULTICPU=0
PROGNAME=$0
SCRIPT_DIR=$(cd $(dirname $0) && pwd)
usage() {
exec >&2
echo "사용법: $PROGNAME [-m] <프로세스 개수>
일정 시간 동작하는 부하 처리 프로세스를 <프로세스 개수>로 지정한 만큼 동작시켜서 모두 끝날 때까지 기다립니다.
각 프로세스 실행에 걸린 시간을 출력합니다.
기본값은 모든 프로세스가 1개의 논리 CPU에서 동작합니다.
옵션 설명:
-m: 각 프로세스를 여러 CPU에서 동작시킵니다."
exit 1
}
while getopts "m" OPT ; do
case $OPT in
m)
MULTICPU=1
;;
\?)
usage
;;
esac
done
shift $((OPTIND - 1))
if [ $# -lt 1 ] ; then
usage
fi
CONCURRENCY=$1
if [ $MULTICPU -eq 0 ] ; then
# 부하 처리를 CPU0에서만 실행시킴
taskset -p -c 0 $$ >/dev/null
fi
for ((i=0;i<CONCURRENCY;i++)) do
time "${SCRIPT_DIR}/load.py" &
done
for ((i=0;i<CONCURRENCY;i++)) do
wait
done
-
참고
특수문자 의미 $0 The name of the script being executed. (Ex. sh test.sh -> test.sh, sh /workspace/temp/test.sh -> /workspace/temp/test.sh) $$ The process ID of the current shell $# The number of command-line arguments $? The exit status of the last executed command, 쉘에서 최근 실행한 명령어의 종료 상태를 담은 변수 $1-$9 The first nine command-line arguments $@ All command-line arguments as an array, $* 과 비슷하나 $@는 “$1”, …“$N”을 의미 $* All command-line arguments as a single string, 모든 위치 매개변수를 담고 있는 단일 문자열 $_ 마지막 인수를 출력하는 변수를 저장 - 프로그램을 간단히 설명하면 -m 옵션과 함께 MULTICPU 파라미터를 넘겨주어야만 멀티 프로세스를 사용
./multiload.sh 1
과 같은 커맨드를 수행할 경우에는 오로지 논리 CPU 0번에서만 taskset을 실행하며 1 의 파라미터는 load.py를 호출하는 Concurrency 개수로 사용
수행결과
$./multiload.sh 1
real 0m3.943s
user 0m3.939s
sys 0m0.004s
--------------------------------------------
$ ./multiload.sh 2
real 0m8.540s
user 0m4.255s
sys 0m0.014s
real 0m8.591s
user 0m4.317s
sys 0m0.003s
--------------------------------------------
$ ./multiload.sh 3
real 0m12.970s
user 0m4.321s
sys 0m0.000s
real 0m13.023s
user 0m4.352s
sys 0m0.000s
real 0m13.473s
user 0m4.791s
sys 0m0.005s
동시 실행을 2배, 3배 바꾸더라도 개별 프로세스의 사용 시간은 거의 변하지 않지만 전체 실행 시간은 2배, 3배 가까이 늘어났습니다. 논리 CPU는 한 번에 프로세스 하나만 처리할 수 있으므로, 스케줄러가 각 프로세스에 순서대로 CPU 자원을 할당하기 때문에 전체 실행 시간은 프로세스 개수에 비례합니다.
논리 CPU 여러 개를 사용하는 경우
- 테스트 환경
- c5.xlarge: 4vCPU
./multiload.sh -m 1
real 0m3.235s
user 0m3.235s
sys 0m0.000s
--------------------------------------------
$ ./multiload.sh -m 2
real 0m3.190s
user 0m3.189s
sys 0m0.000s
real 0m3.199s
user 0m3.189s
sys 0m0.010s
--------------------------------------------
$ ./multiload.sh -m 3
real 0m3.205s
user 0m3.205s
sys 0m0.000s
real 0m5.094s
user 0m5.082s
sys 0m0.010s
real 0m5.847s
user 0m5.834s
sys 0m0.010s
--------------------------------------------
$ ./multiload.sh -m 4
real 0m6.880s
user 0m6.858s
sys 0m0.000s
real 0m6.901s
user 0m6.832s
sys 0m0.010s
real 0m6.961s
user 0m6.911s
sys 0m0.000s
real 0m7.048s
user 0m7.022s
sys 0m0.000s
모든 프로세스에서 real과 user+sys 값이 거의 같습니다. 이 말은 즉, 프로세스마다 각각의 논리 CPU 자원을 독점했다는 뜻 입니다.
$ ./multiload.sh -m 5
real 0m8.400s
user 0m6.861s
sys 0m0.000s
real 0m8.473s
user 0m6.881s
sys 0m0.000s
real 0m8.540s
user 0m6.872s
sys 0m0.000s
real 0m8.633s
user 0m6.994s
sys 0m0.000s
real 0m8.709s
user 0m6.613s
sys 0m0.000s
하지만 프로세스가 5개가 되는 순간 부터 CPU를 독점하지 못하므로 real의 시간이 늘어납니다. (슬립상태 발생)
real보다 user+sys가 커지는 경우
직감적으로는 언제나 real >= user + sys 일 것 같은 느낌이 들지만 실제로는 user + sys 값이 real 값보다 조금 더 큰 경우가 있습니다. 이는 각각 시간을 측정하는 방법이 조금씩 다르고, 측정 정밀도가 그다지 높지 않기 때문입니다. 너무 신경 쓸 필요없이 넘어가면 됩니다.
또한 상황에 따라서는 real 보다 user + sys 가 훨씬 큰 값이 될 수도 있습니다.
$ time ./multiload.sh -m 2
real 0m8.220s
user 0m7.660s
sys 0m0.013s
real 0m8.295s
user 0m7.810s
sys 0m0.020s
real 0m8.301s
user 0m15.476s
sys 0m0.033s
첫 번째와 두 번째 부하 정보는 multiload.sh 프로그램의 부하 처리 프로세스에 관련된 데이터입니다. 세 번째 부하 정보는 multiload.sh 프로그램 그 자체에 관련된 데이터 입니다. 실은 time 명령어로 얻은 user와 sys 값은 정보 확인 대상의 프로세스 및 종료된 자식 프로세스의 값을 더한 값입니다. 따라서 어떤 프로세스가 자식 프로세스를 생성하고 각각 다른 논리 CPU에서 동작한다면 real 보다 user + sys 값이 커질 수 있습니다.
Time slice(타임 슬라이스)
CPU에서 동시에 동작하는 프로세스 개수는 하나라는 걸 보았습니다. 하지만 구체적으로 어떻게 CPU 자원을 배분하고 있는지는 지금까지 실험한 내용으로 알 수 없었습니다. 이 절에서는 스케줄러가 실행 가능한 프로세스에 Time slice 단위로 CPU를 나눠주는 걸 실습으로 확인해 보겠습니다.
결과 그래프를 보면 1개의 논리 CPU에서 여러 처리를 동시에 실행하는 경우, 각각의 처리는 수 밀리초 단위의 타임 슬라이스로 쪼개서 CPU를 교대로 사용하는 걸 알 수 있습니다.
타임슬라이스 구조 동시 실행 2와 비교해서 3인 경우에는 각 프로세스의 타임 슬라이스가 짧다는 것을 알 수 있습니다. 리눅스 스케줄러는 sysctl의 kernel.sched_latency_ns 파라미터에 지정한 목표 레이턴시 간격에 한 번씩 CPU 시간을 얻을 수 있습니다.
$ sudo cat /sys/kernel/debug/sched/latency_ns
12000000
# 0.012 seconds
목표 레이턴시나 타임 슬라이스 값을 계산하는 방법은 프로세스 개수가 늘어나거나 멀티 코어 CPU일 때 조금 복잡해 집니다. 다음과 같은 요건에 따라 변화합니다.
- 시스템에 설치된 논리 CPU 개수
- 일정한 값을 넘은 논리 CPU에서 실행 중/실행 대기 중인 프로세스 개수
- 프로세스 우선도를 뜻하는 nice 값
nice 값은 프로세스 실행 우선도를 -20 부터 19 사이의 값으로 정한 값입니다.(기본값 0 / -20이 최우선). 우선도를 낮추는 건 누구나 가능하지만 우선도를 높힐 수 있는 건 루트 권한을 가진 사용자 뿐입니다.
nice 값을 변경한 경우
Load0 가 Load1 보다 타임 슬라이스를 많이 가져간 걸 알 수 있습니다. sar 출력 결과에서 %nice 필드는 nice 값이 기본값 0보다 커진 프로세스가 사용자 모드로 실행한 시간 비율을 나타냅니다.(%user는 nice 값 0인 경우)
Context Switch(컨텍스트 스위치)
논리 CPU에서 동작하는 프로세스가 전환되는 것을 context switch(컨텍스트 스위치)라고 합니다.
컨텍스트 스위치는 프로세스가 어떤 코드를 실행하고 있든 간에 타임 슬라이스가 끝나면 주저 없이 발생합니다.
실제로는 foo() 직후에 bar()가 실행된다는 보장이 없습니다.
foo() 실행 직후에 타임 슬라이스가 끝나면 bar() 실행은 훨씬 뒤가 될 수도 있습니다. 이런 상황을 이해하면 어떤 처리에 생각보다 많은 시간이 걸릴 때, ‘처리 자체에 무슨 문제가 있는게 분명하다’고 이렇게 단순히 결론 내리기보다는 ‘처리를 하다가 컨텍스트 스위치가 발생해서 다른 프로세스가 동작했을 가능성이 있을지도 모른다’라는 관점으로 볼 수 있습니다.
처리 성능
- Turnaround time: 처리(반환) 시간. 시스템에 처리를 요청했을 때부터 처리가 끝날 때까지 걸린 시간
- Throughput: 처리량. 단위 시간당 처리를 끝낸 개수
논리 CPU 1개만 사용하고 최대 프로세스 개수를 8개로 지정해서 부하 처리 프로세스를 실행 한 결과 입니다.
논리 CPU 1개, 최대 프로세스 8개 일 때 Average Turnaround time
논리 CPU 1개, 최대 프로세스 8개 일 때 Throughput
결과를 살펴보면 논리 CPU 개수보다 프로세스 개수가 많아지면 평균 처리 시간만 늘어나고 처리량은 차이가 없음을 알 수 있습니다. 이 후 프로세스 개수를 더 늘리면 스케줄러가 발생시킨 Context switch 때문에 평균 처리 시간이 점점 길어지고 처리량도 계속 떨어집니다. 성능 관점에서 보자면, CPU 자원을 전부 사용하는 상태라면 프로세스 개수만 늘린다고 성능 문제를 해결할 수 없다는 뜻 입니다.
시스템에 다음과 같은 처리를 하는 Web application이 있다고 합시다.
- 네트워크를 경유해서 사용자로부터 요청을 받습니다.
- 요청에 따라 html 파일을 생성합니다.
- 결과를 네트워크를 경유해서 사용자에게 돌려줍니다.
논리 CPU 부하가 높은 상태일 때 이러한 요청이 새로 도착하면 평균 처리 시간이 점점 길어집니다. 사용자 입장에서 보면 웹 애플리케이션 응답 시간 지연과 직접적으로 관련되므로 사용자 경험이 나빠집니다. 응답 성능이 중요한 시스템이라면 처리량이 중요한 시스템에 비해서 시스템을 구성하는 각 기긱의 CPU 사용률을 적게 유지하는 것이 중요합니다.
이번에는 모든 논리 CPU를 사용해서 성능 데이터를 수집해 봅니다. 논리 CPU 개수는 아래 명령어로 확인 가능합니다.
$ grep -c processor /proc/cpuinfo
8
논리 CPU 개수는 2개가 됩니다.
실습 전 Simultaneous Multi Threading(SMT)를 무효로 만듭니다.
$ cat /sys/devices/system/cpu/smt/control
on
$ sudo echo off > /sys/devices/system/cpu/smt/control
$ cat /sys/devices/system/cpu/smt/control
$ grep -c processor /proc/cpuinfo
4
논리 CPU 4개, 최대 프로세스 8개 일 때 Average Turnaround time
프로세스 개수가 논리 CPU 개수와 같아질 때까지는 평균 처리 시간이 거의 유지되지만 그 후는 갑자기 길어지는 걸 알 수 있습니다.
논리 CPU 4개, 최대 프로세스 8개 일 때 Throughput
처리량은 프로세스 개수와 논리 CPU 개수가 같아질 때까지 향상되지만 그 후에는 한풀 꺽이는 걸 알 수 있습니다. 논리 CPU가 많이 내장된 경우라도 충분한 수의 프로세스가 실행되어야 비로소 처리량이 향상됩니다. 또한 무조건 프로세스 개수를 늘린다고 처리량이 개선되지는 않습니다.
프로그램 병렬 실행의 중요성
예전에는 새로운 CPU가 나올 때마다 논리 CPU당 성능이 극적으로 향상되었습니다. 그러면 프로그램을 변경하지 않아도 처리 속도가 빨라집니다. 하지만 최근에는 여러한 이유로 싱글 스레드 성능이 더이상 향상되기 어려워졌습니다. 따라서 CPU 세대가 하나 바뀌더라도 싱글 스레드 성능은 소소한 개선 정도가 한계입니다. 대신에 CPU 코어 개수를 늘리는 방법 등으로 CPU 전체 성능을 끌어올리는 쪽으로 방향이 바뀌었습니다. 커널도 이런 시대 흐름에 발맞춰서 코어 개수가 늘어난 경우의 확장성(Scalability)을 향상시켜왔습니다.
Reference
- 그림으로 배우는 리눅스 구조 (다케우치 사토루)
- E-Book
book
linux
cpu
process
scheduler
]