요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
오차 재현 program
cpu-load.rst:53-117Microsecond timer와 CPU-burning loop를 조합한 C program, 관련 discussion과 contributor를 제공합니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
========
CPU load
========
Linux exports various bits of information via ``/proc/stat`` and
``/proc/uptime`` that userland tools, such as top(1), use to calculate
the average time system spent in a particular state, for example::
$ iostat
Linux 2.6.18.3-exp (linmac) 02/20/2007
avg-cpu: %user %nice %system %iowait %steal %idle
10.01 0.00 2.92 5.44 0.00 81.63
...
Here the system thinks that over the default sampling period the
system spent 10.01% of the time doing work in user space, 2.92% in the
kernel, and was overall 81.63% of the time idle.
In most cases the ``/proc/stat`` information reflects the reality quite
closely, however due to the nature of how/when the kernel collects
this data sometimes it can not be trusted at all.
So how is this information collected? Whenever timer interrupt is
signalled the kernel looks what kind of task was running at this
moment and increments the counter that corresponds to this tasks
kind/state. The problem with this is that the system could have
switched between various states multiple times between two timer
interrupts yet the counter is incremented only for the last state.
Example
-------
If we imagine the system with one task that periodically burns cycles
in the following manner::
time line between two timer interrupts
|--------------------------------------|
^ ^
|_ something begins working |
|_ something goes to sleep
(only to be awaken quite soon)
In the above situation the system will be 0% loaded according to the
``/proc/stat`` (since the timer interrupt will always happen when the
system is executing the idle handler), but in reality the load is
closer to 99%.
One can imagine many more situations where this behavior of the kernel
will lead to quite erratic information inside ``/proc/stat``::
/* gcc -o hog smallhog.c */
#include <time.h>
#include <limits.h>
#include <signal.h>
#include <sys/time.h>
#define HIST 10
static volatile sig_atomic_t stop;
static void sighandler(int signr)
{
(void) signr;
stop = 1;
}
static unsigned long hog (unsigned long niters)
{
stop = 0;
while (!stop && --niters);
return niters;
}
int main (void)
{
int i;
struct itimerval it = {
.it_interval = { .tv_sec = 0, .tv_usec = 1 },
.it_value = { .tv_sec = 0, .tv_usec = 1 } };
sigset_t set;
unsigned long v[HIST];
double tmp = 0.0;
unsigned long n;
signal(SIGALRM, &sighandler);
setitimer(ITIMER_REAL, &it, NULL);
hog (ULONG_MAX);
for (i = 0; i < HIST; ++i) v[i] = ULONG_MAX - hog(ULONG_MAX);
for (i = 0; i < HIST; ++i) tmp += v[i];
tmp /= HIST;
n = tmp - (tmp / 3.0);
sigemptyset(&set);
sigaddset(&set, SIGALRM);
for (;;) {
hog(n);
sigwait(&set, &i);
}
return 0;
}
References
----------
- https://lore.kernel.org/r/[email protected]
- Documentation/filesystems/proc.rst (1.8)
Thanks
------
Con Kolivas, Pavel Machek
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
Procfs CPU time과 sampling model
1-30Linux는 `/proc/stat`과 `/proc/uptime`을 통해 여러 정보를 export합니다. `top(1)` 같은 userland tool은 이 값을 사용해 system이 특정 state에서 보낸 평균 시간을 계산합니다. 원문의 `iostat` 예시는 다음과 같습니다.
$ iostat
Linux 2.6.18.3-exp (linmac) 02/20/2007
avg-cpu: %user %nice %system %iowait %steal %idle
10.01 0.00 2.92 5.44 0.00 81.63
...
이 출력에서 system은 기본 sampling period 동안 user space work에 10.01%, kernel work에 2.92%를 썼고 전체 시간의 81.63%는 idle이었다고 판단합니다.
대부분 `/proc/stat` 정보는 실제 상태를 상당히 가깝게 반영하지만, kernel이 data를 수집하는 방식과 시점 때문에 전혀 신뢰할 수 없는 경우도 있습니다.
Timer interrupt가 발생할 때마다 kernel은 그 순간 실행 중인 task의 종류와 state를 보고 대응 counter를 증가시킵니다. 두 timer interrupt 사이에서 system state가 여러 번 바뀌어도 마지막 순간의 state counter만 증가한다는 것이 문제입니다.
원문 예시의 주요 CPU state 비율입니다.
Counter는 tick 사이의 전체 history가 아니라 interrupt 순간의 state를 sample합니다.
Tick 사이 work가 사라지는 예
31-52한 task가 두 timer interrupt 사이에서 주기적으로 CPU cycle을 소모한다고 가정합니다. 첫 tick 직후 work를 시작하고 다음 tick 직전에 sleep에 들어가며 곧 다시 깨어납니다.
이 경우 timer interrupt는 항상 system이 idle handler를 실행할 때 발생하므로 `/proc/stat`에는 load가 0%로 보입니다. 하지만 실제 load는 99%에 가깝습니다.
즉 periodic sampling과 workload phase가 맞물리면 kernel accounting data가 심하게 왜곡될 수 있습니다.
Source의 ASCII timeline을 동일한 의미의 시간축으로 다시 그렸습니다.
Sampling 오차 재현 program
53-105원문은 `/proc/stat`에 불규칙한 정보가 나타나는 상황을 재현하는 `smallhog.c` program을 제공합니다. Source는 다음과 같으며 compile command도 그대로 보존합니다.
/* gcc -o hog smallhog.c */
#include <time.h>
#include <limits.h>
#include <signal.h>
#include <sys/time.h>
#define HIST 10
static volatile sig_atomic_t stop;
static void sighandler(int signr)
{
(void) signr;
stop = 1;
}
static unsigned long hog (unsigned long niters)
{
stop = 0;
while (!stop && --niters);
return niters;
}
int main (void)
{
int i;
struct itimerval it = {
.it_interval = { .tv_sec = 0, .tv_usec = 1 },
.it_value = { .tv_sec = 0, .tv_usec = 1 } };
sigset_t set;
unsigned long v[HIST];
double tmp = 0.0;
unsigned long n;
signal(SIGALRM, &sighandler);
setitimer(ITIMER_REAL, &it, NULL);
hog (ULONG_MAX);
for (i = 0; i < HIST; ++i) v[i] = ULONG_MAX - hog(ULONG_MAX);
for (i = 0; i < HIST; ++i) tmp += v[i];
tmp /= HIST;
n = tmp - (tmp / 3.0);
sigemptyset(&set);
sigaddset(&set, SIGALRM);
for (;;) {
hog(n);
sigwait(&set, &i);
}
return 0;
}
Program은 1 microsecond 간격의 `ITIMER_REAL`을 설정하고 `SIGALRM` handler에서 `stop`을 세웁니다. 먼저 `hog(ULONG_MAX)` loop를 여러 번 측정해 interrupt 사이에 실행할 iteration 수를 추정하고 평균값의 약 3분의 2를 `n`으로 정합니다.
그 뒤 무한 loop에서 `hog(n)`으로 CPU를 사용하고 `sigwait()`로 다음 `SIGALRM`을 기다립니다. Work와 signal wait의 phase가 accounting tick과 관계를 맺으면서 실제 CPU 사용과 sampled counter 사이의 차이를 보여 줍니다.
Calibration 뒤 CPU-burning loop와 signal wait를 반복합니다.
참고 자료와 감사
106-117관련 논의는 `https://lore.kernel.org/r/[email protected]`과 `Documentation/filesystems/proc.rst`의 1.8절을 참조하십시오.
이 문서는 Con Kolivas와 Pavel Machek에게 감사를 표합니다.
Tick 기반 CPU accounting
cpu-load.rst:1-52`/proc/stat`과 `/proc/uptime`의 평균 CPU state가 timer interrupt 순간의 sample에 의존해 phase-aligned workload를 놓칠 수 있음을 보여 줍니다.