요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
=====================
The errseq_t datatype
=====================
An errseq_t is a way of recording errors in one place, and allowing any
number of "subscribers" to tell whether it has changed since a previous
point where it was sampled.
The initial use case for this is tracking errors for file
synchronization syscalls (fsync, fdatasync, msync and sync_file_range),
but it may be usable in other situations.
It's implemented as an unsigned 32-bit value. The low order bits are
designated to hold an error code (between 1 and MAX_ERRNO). The upper bits
are used as a counter. This is done with atomics instead of locking so that
these functions can be called from any context.
Note that there is a risk of collisions if new errors are being recorded
frequently, since we have so few bits to use as a counter.
To mitigate this, the bit between the error value and counter is used as
a flag to tell whether the value has been sampled since a new value was
recorded. That allows us to avoid bumping the counter if no one has
sampled it since the last time an error was recorded.
Thus we end up with a value that looks something like this:
+--------------------------------------+----+------------------------+
| 31..13 | 12 | 11..0 |
+--------------------------------------+----+------------------------+
| counter | SF | errno |
+--------------------------------------+----+------------------------+
The general idea is for "watchers" to sample an errseq_t value and keep
it as a running cursor. That value can later be used to tell whether
any new errors have occurred since that sampling was done, and atomically
record the state at the time that it was checked. This allows us to
record errors in one place, and then have a number of "watchers" that
can tell whether the value has changed since they last checked it.
A new errseq_t should always be zeroed out. An errseq_t value of all zeroes
is the special (but common) case where there has never been an error. An all
zero value thus serves as the "epoch" if one wishes to know whether there
has ever been an error set since it was first initialized.
API usage
=========
Let me tell you a story about a worker drone. Now, he's a good worker
overall, but the company is a little...management heavy. He has to
report to 77 supervisors today, and tomorrow the "big boss" is coming in
from out of town and he's sure to test the poor fellow too.
They're all handing him work to do -- so much he can't keep track of who
handed him what, but that's not really a big problem. The supervisors
just want to know when he's finished all of the work they've handed him so
far and whether he made any mistakes since they last asked.
He might have made the mistake on work they didn't actually hand him,
but he can't keep track of things at that level of detail, all he can
remember is the most recent mistake that he made.
Here's our worker_drone representation::
struct worker_drone {
errseq_t wd_err; /* for recording errors */
};
Every day, the worker_drone starts out with a blank slate::
struct worker_drone wd;
wd.wd_err = (errseq_t)0;
The supervisors come in and get an initial read for the day. They
don't care about anything that happened before their watch begins::
struct supervisor {
errseq_t s_wd_err; /* private "cursor" for wd_err */
spinlock_t s_wd_err_lock; /* protects s_wd_err */
}
struct supervisor su;
su.s_wd_err = errseq_sample(&wd.wd_err);
spin_lock_init(&su.s_wd_err_lock);
Now they start handing him tasks to do. Every few minutes they ask him to
finish up all of the work they've handed him so far. Then they ask him
whether he made any mistakes on any of it::
spin_lock(&su.su_wd_err_lock);
err = errseq_check_and_advance(&wd.wd_err, &su.s_wd_err);
spin_unlock(&su.su_wd_err_lock);
Up to this point, that just keeps returning 0.
Now, the owners of this company are quite miserly and have given him
substandard equipment with which to do his job. Occasionally it
glitches and he makes a mistake. He sighs a heavy sigh, and marks it
down::
errseq_set(&wd.wd_err, -EIO);
...and then gets back to work. The supervisors eventually poll again
and they each get the error when they next check. Subsequent calls will
return 0, until another error is recorded, at which point it's reported
to each of them once.
Note that the supervisors can't tell how many mistakes he made, only
whether one was made since they last checked, and the latest value
recorded.
Occasionally the big boss comes in for a spot check and asks the worker
to do a one-off job for him. He's not really watching the worker
full-time like the supervisors, but he does need to know whether a
mistake occurred while his job was processing.
He can just sample the current errseq_t in the worker, and then use that
to tell whether an error has occurred later::
errseq_t since = errseq_sample(&wd.wd_err);
/* submit some work and wait for it to complete */
err = errseq_check(&wd.wd_err, since);
Since he's just going to discard "since" after that point, he doesn't
need to advance it here. He also doesn't need any locking since it's
not usable by anyone else.
Serializing errseq_t cursor updates
===================================
Note that the errseq_t API does not protect the errseq_t cursor during a
check_and_advance_operation. Only the canonical error code is handled
atomically. In a situation where more than one task might be using the
same errseq_t cursor at the same time, it's important to serialize
updates to that cursor.
If that's not done, then it's possible for the cursor to go backward
in which case the same error could be reported more than once.
Because of this, it's often advantageous to first do an errseq_check to
see if anything has changed, and only later do an
errseq_check_and_advance after taking the lock. e.g.::
if (errseq_check(&wd.wd_err, READ_ONCE(su.s_wd_err)) {
/* su.s_wd_err is protected by s_wd_err_lock */
spin_lock(&su.s_wd_err_lock);
err = errseq_check_and_advance(&wd.wd_err, &su.s_wd_err);
spin_unlock(&su.s_wd_err_lock);
}
That avoids the spinlock in the common case where nothing has changed
since the last time it was checked.
Functions
=========
.. kernel-doc:: lib/errseq.c
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
errseq_t 자료형과 비트 배치
1-45The errseq_t datatype (errseq_t 자료형)
`errseq_t`는 한 곳에 오류를 기록하고, 임의 개수의 subscriber가 이전에 값을 표본화한 시점 이후 오류 상태가 바뀌었는지 확인하게 하는 방법입니다.
최초 사용 사례는 파일 동기화 시스템 호출인 fsync, fdatasync, msync 및 sync_file_range의 오류 추적이지만 다른 상황에서도 사용할 수 있습니다.
구현은 unsigned 32-bit value입니다. 하위 비트에는 1부터 MAX_ERRNO 사이의 error code를 저장하고 상위 비트는 counter로 사용합니다. locking 대신 atomics를 사용하므로 어느 문맥에서든 이 함수들을 호출할 수 있습니다.
counter에 사용할 수 있는 비트가 적으므로 새 오류가 자주 기록되면 값이 충돌할 위험이 있습니다.
이를 완화하기 위해 error value와 counter 사이의 비트를, 새 값이 기록된 뒤 그 값을 표본화했는지를 나타내는 flag로 사용합니다. 마지막 오류 기록 이후 아무도 값을 표본화하지 않았다면 counter를 증가시키지 않아도 됩니다.
따라서 값의 비트 배치는 다음과 같습니다.
상위 counter, 표본화 플래그 SF, 하위 errno 필드의 구조입니다.
일반적인 방식은 watcher가 `errseq_t` 값을 표본화해 계속 갱신하는 cursor로 보관하는 것입니다. 나중에 그 값을 사용하면 표본화 이후 새 오류가 발생했는지 확인하는 동시에, 검사한 시점의 상태를 atomically 기록할 수 있습니다. 이 방식으로 한 곳에 오류를 기록하면서 여러 watcher가 마지막 검사 이후 값이 변했는지 각각 판단할 수 있습니다.
새 `errseq_t`는 항상 0으로 초기화해야 합니다. 모든 비트가 0인 값은 오류가 한 번도 없었던 특수하면서도 흔한 상태입니다. 따라서 최초 초기화 이후 오류가 설정된 적이 있는지 알고 싶을 때 all-zero value를 epoch로 사용할 수 있습니다.
API 사용 모델과 worker_drone
46-68API usage
한 worker drone의 이야기로 API를 설명하겠습니다. 그는 전반적으로 훌륭한 작업자이지만 회사는 관리자가 지나치게 많습니다. 오늘은 77명의 supervisor에게 보고해야 하고, 내일은 외지에서 big boss가 와서 이 작업자를 시험할 예정입니다.
모든 supervisor가 일을 넘기므로 작업자는 누가 어떤 일을 줬는지 추적할 수 없습니다. 그러나 이는 큰 문제가 아닙니다. supervisor는 자신이 지금까지 맡긴 모든 일이 끝났는지, 그리고 마지막으로 물어본 뒤 작업자가 실수했는지만 알면 됩니다.
실수는 해당 supervisor가 실제로 맡기지 않은 작업에서 발생했을 수도 있습니다. 작업자는 그 정도 세부 사항을 추적할 수 없으며 자신이 저지른 가장 최근 실수만 기억합니다.
다음은 `worker_drone` 표현입니다.
struct worker_drone {
errseq_t wd_err; /* for recording errors */
};
초기화, 표본화와 supervisor cursor
69-97매일 `worker_drone`은 빈 상태로 시작합니다.
struct worker_drone wd;
wd.wd_err = (errseq_t)0;
supervisor들은 출근해 그날의 초기 값을 읽습니다. 자신의 감시가 시작되기 전에 일어난 일은 신경 쓰지 않습니다.
struct supervisor {
errseq_t s_wd_err; /* private "cursor" for wd_err */
spinlock_t s_wd_err_lock; /* protects s_wd_err */
}
struct supervisor su;
su.s_wd_err = errseq_sample(&wd.wd_err);
spin_lock_init(&su.s_wd_err_lock);
이제 supervisor들이 작업자에게 일을 맡기기 시작합니다. 몇 분마다 지금까지 맡긴 일을 모두 끝내라고 한 다음, 그 작업 중 실수가 있었는지 묻습니다.
spin_lock(&su.su_wd_err_lock);
err = errseq_check_and_advance(&wd.wd_err, &su.s_wd_err);
spin_unlock(&su.su_wd_err_lock);
이 시점까지는 검사 결과로 계속 0이 반환됩니다.
오류 기록과 각 watcher의 일회 보고
98-113회사의 소유주는 매우 인색해서 작업자에게 질 낮은 장비를 제공했습니다. 장비가 가끔 오동작하면 작업자는 실수를 하고, 한숨을 쉰 뒤 그 오류를 기록합니다.
errseq_set(&wd.wd_err, -EIO);
그런 다음 다시 작업합니다. supervisor가 나중에 다시 poll하면 각자는 다음 검사에서 이 오류를 받습니다. 그 뒤의 호출은 또 다른 오류가 기록될 때까지 0을 반환하며, 새 오류가 생기면 각 supervisor에게 한 번씩 보고됩니다.
supervisor는 작업자가 실수한 횟수는 알 수 없습니다. 마지막 검사 이후 실수가 있었는지와 가장 최근에 기록된 값만 알 수 있습니다.
일회성 검사와 errseq_check
114-129가끔 big boss가 불시에 찾아와 작업자에게 일회성 작업을 맡깁니다. supervisor처럼 작업자를 계속 감시하지는 않지만, 자신의 작업이 처리되는 동안 실수가 발생했는지는 알아야 합니다.
현재 작업자의 `errseq_t`를 표본화한 뒤 나중에 오류가 발생했는지 다음처럼 확인하면 됩니다.
errseq_t since = errseq_sample(&wd.wd_err);
/* submit some work and wait for it to complete */
err = errseq_check(&wd.wd_err, since);
이후 `since`를 버릴 것이므로 여기서는 cursor를 advance할 필요가 없습니다. 다른 사용자가 이 값을 사용할 수 없으므로 locking도 필요하지 않습니다.
errseq_t cursor 갱신 직렬화
130-155Serializing errseq_t cursor updates
`errseq_t` API는 check_and_advance_operation 중 `errseq_t` cursor 자체를 보호하지 않습니다. canonical error code만 atomically 처리합니다. 둘 이상의 task가 같은 `errseq_t` cursor를 동시에 사용할 수 있다면 cursor 갱신을 직렬화하는 것이 중요합니다.
직렬화하지 않으면 cursor가 뒤로 이동할 수 있으며, 그 결과 같은 오류를 두 번 이상 보고할 수 있습니다.
따라서 먼저 `errseq_check`를 수행해 변경이 있는지 확인하고, 변경이 있을 때만 lock을 얻은 뒤 `errseq_check_and_advance`를 수행하는 방식이 유리한 경우가 많습니다.
if (errseq_check(&wd.wd_err, READ_ONCE(su.s_wd_err)) {
/* su.s_wd_err is protected by s_wd_err_lock */
spin_lock(&su.s_wd_err_lock);
err = errseq_check_and_advance(&wd.wd_err, &su.s_wd_err);
spin_unlock(&su.s_wd_err_lock);
}
이렇게 하면 마지막 검사 이후 아무것도 바뀌지 않은 일반적인 경우에는 spinlock을 얻지 않아도 됩니다. 예제의 READ_ONCE, spin_lock 및 spin_unlock은 공유 cursor에 대한 빠른 검사와 직렬화된 갱신을 구분합니다.
함수 참조
156-159Functions
함수별 kernel-doc 참조는 `lib/errseq.c`에서 생성됩니다: `.. kernel-doc:: lib/errseq.c`.
요약과 해설
errseq.rst:1-159`errseq_t`는 최신 errno와 오류 세대를 32비트 값 하나에 담습니다. 각 watcher는 자신의 cursor를 유지하므로 같은 오류를 한 번씩 독립적으로 관찰할 수 있지만 오류 발생 횟수 전체를 세지는 않습니다.
`errseq_sample()`은 관찰 시작점을 만들고, `errseq_check()`는 cursor를 바꾸지 않은 채 새 오류를 확인하며, `errseq_check_and_advance()`는 검사와 cursor 전진을 함께 수행합니다. 오류 생산자는 `errseq_set()`으로 최신 오류를 기록합니다.
canonical error value는 atomic하게 다뤄지지만 공유 cursor 갱신은 API가 보호하지 않습니다. 여러 task가 cursor 하나를 공유하면 lock으로 갱신을 직렬화하고, 흔한 무변경 경로에서는 먼저 lock 없는 `errseq_check()`를 사용해 비용을 줄이는 것이 핵심입니다.