요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
Raw seqcount_t
seqlock.rst:41-90seqcount_t 자체는 여러 writer를 직렬화하지 않습니다. 외부 lock으로 한 번에 하나의 writer만 진입하게 해야 하고 write section 동안 preemption을 막아야 합니다. Reader가 hardirq 또는 softirq에서 실행될 수 있으면 writer는 각각 IRQ 또는 bottom half도 막아야 합니다.
seqcount_t foo_seqcount;
seqcount_init(&foo_seqcount);
/* 외부 lock으로 writer 직렬화, preemption disabled */
write_seqcount_begin(&foo_seqcount);
foo.a = new_a;
foo.b = new_b;
write_seqcount_end(&foo_seqcount);
/* reader */
do {
seq = read_seqcount_begin(&foo_seqcount);
a = foo.a;
b = foo.b;
} while (read_seqcount_retry(&foo_seqcount, seq));
Reader가 복사해야 할 데이터는 begin과 retry 사이에서 모두 읽어야 합니다. Retry가 false로 확인된 뒤 원본 구조체를 다시 참조하면 검증되지 않은 새 state가 섞입니다.
Lock 연계형 seqcount
seqlock.rst:94-141seqcount_LOCKNAME_t는 writer 직렬화에 사용하는 lock pointer를 초기화 시 연결합니다. Lockdep이 켜져 있으면 write_seqcount_begin()에서 그 lock을 실제로 보유했는지 검증합니다. Lockdep이 꺼진 build에서는 연계 정보가 storage나 runtime overhead를 만들지 않습니다.
- seqcount_spinlock_t
- seqcount_raw_spinlock_t
- seqcount_rwlock_t
- seqcount_mutex_t
- seqcount_ww_mutex_t
spinlock_t lock;
seqcount_spinlock_t seq;
spin_lock_init(&lock);
seqcount_spinlock_init(&seq, &lock);
spin_lock(&lock);
write_seqcount_begin(&seq);
/* update */
write_seqcount_end(&seq);
spin_unlock(&lock);
seqcount_latch_t와 NMI reader
seqlock.rst:146-158seqcount_latch_t는 보호 데이터 복사본 두 개를 두고 embedded counter의 짝수·홀수 값으로 reader가 볼 copy를 전환하는 multiversion mechanism입니다. Reader가 write-side critical section 자체를 interrupt할 수 있어도 writer가 수정 중인 copy가 아닌 다른 copy를 읽을 수 있습니다.
대표 사용처는 NMI handler처럼 writer가 reader의 interrupt를 완전히 막을 수 없는 경우입니다. Memory 사용량과 두 copy 갱신 순서를 부담하는 대신 self-interrupt 상황에서도 안전한 snapshot을 제공합니다.
seqlock_t writer와 기본 reader
seqlock.rst:163-207seqlock_t는 seqcount_t와 writer용 spinlock을 하나로 묶습니다. Writer serialization과 non-preemptibility를 primitive가 함께 제공합니다. Reader가 hardirq나 softirq에서 호출될 수 있으면 writer 쪽에서 IRQ 또는 bottom half를 disable하는 variant를 선택합니다.
static DEFINE_SEQLOCK(foo_seqlock);
write_seqlock(&foo_seqlock);
foo.a = new_a;
foo.b = new_b;
write_sequnlock(&foo_seqlock);
do {
seq = read_seqbegin(&foo_seqlock);
a = foo.a;
b = foo.b;
} while (read_seqretry(&foo_seqlock, seq));
정상 sequence reader는 writer를 막지 않습니다. 대신 writer가 겹치면 필요한 횟수만큼 retry합니다. Read section이 길거나 write burst가 심하면 반복 비용이 커질 수 있으므로 복사 범위를 최소화해야 합니다.
Locking reader와 conditional reader
seqlock.rst:208-241read_seqlock_excl()은 writer 또는 다른 locking reader와 배타적으로 동작합니다. rwlock의 shared read lock과 달리 locking reader도 한 명만 진입합니다. 재시도 없이 반드시 한 번에 읽어야 하는 느린 fallback에 사용할 수 있습니다.
int seq = 0; /* 짝수면 lockless 시도 */
do {
read_seqbegin_or_lock(&foo_seqlock, &seq);
snapshot = foo;
} while (need_seqretry(&foo_seqlock, seq));
done_seqretry(&foo_seqlock, seq);
Conditional reader는 처음에는 짝수 marker로 lockless read를 시도합니다. 실패하여 홀수 marker를 받으면 다음 반복을 locking read로 전환합니다. Write activity 급증으로 lockless reader가 계속 retry하는 starvation을 제한하는 방식입니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0
======================================
Sequence counters and sequential locks
======================================
Introduction
============
Sequence counters are a reader-writer consistency mechanism with
lockless readers (read-only retry loops), and no writer starvation. They
are used for data that's rarely written to (e.g. system time), where the
reader wants a consistent set of information and is willing to retry if
that information changes.
A data set is consistent when the sequence count at the beginning of the
read side critical section is even and the same sequence count value is
read again at the end of the critical section. The data in the set must
be copied out inside the read side critical section. If the sequence
count has changed between the start and the end of the critical section,
the reader must retry.
Writers increment the sequence count at the start and the end of their
critical section. After starting the critical section the sequence count
is odd and indicates to the readers that an update is in progress. At
the end of the write side critical section the sequence count becomes
even again which lets readers make progress.
A sequence counter write side critical section must never be preempted
or interrupted by read side sections. Otherwise the reader will spin for
the entire scheduler tick due to the odd sequence count value and the
interrupted writer. If that reader belongs to a real-time scheduling
class, it can spin forever and the kernel will livelock.
This mechanism cannot be used if the protected data contains pointers,
as the writer can invalidate a pointer that the reader is following.
.. _seqcount_t:
Sequence counters (``seqcount_t``)
==================================
This is the raw counting mechanism, which does not protect against
multiple writers. Write side critical sections must thus be serialized
by an external lock.
If the write serialization primitive is not implicitly disabling
preemption, preemption must be explicitly disabled before entering the
write side section. If the read section can be invoked from hardirq or
softirq contexts, interrupts or bottom halves must also be respectively
disabled before entering the write section.
If it's desired to automatically handle the sequence counter
requirements of writer serialization and non-preemptibility, use
:ref:`seqlock_t` instead.
Initialization::
/* dynamic */
seqcount_t foo_seqcount;
seqcount_init(&foo_seqcount);
/* static */
static seqcount_t foo_seqcount = SEQCNT_ZERO(foo_seqcount);
/* C99 struct init */
struct {
.seq = SEQCNT_ZERO(foo.seq),
} foo;
Write path::
/* Serialized context with disabled preemption */
write_seqcount_begin(&foo_seqcount);
/* ... [[write-side critical section]] ... */
write_seqcount_end(&foo_seqcount);
Read path::
do {
seq = read_seqcount_begin(&foo_seqcount);
/* ... [[read-side critical section]] ... */
} while (read_seqcount_retry(&foo_seqcount, seq));
.. _seqcount_locktype_t:
Sequence counters with associated locks (``seqcount_LOCKNAME_t``)
-----------------------------------------------------------------
As discussed at :ref:`seqcount_t`, sequence count write side critical
sections must be serialized and non-preemptible. This variant of
sequence counters associate the lock used for writer serialization at
initialization time, which enables lockdep to validate that the write
side critical sections are properly serialized.
This lock association is a NOOP if lockdep is disabled and has neither
storage nor runtime overhead. If lockdep is enabled, the lock pointer is
stored in struct seqcount and lockdep's "lock is held" assertions are
injected at the beginning of the write side critical section to validate
that it is properly protected.
For lock types which do not implicitly disable preemption, preemption
protection is enforced in the write side function.
The following sequence counters with associated locks are defined:
- ``seqcount_spinlock_t``
- ``seqcount_raw_spinlock_t``
- ``seqcount_rwlock_t``
- ``seqcount_mutex_t``
- ``seqcount_ww_mutex_t``
The sequence counter read and write APIs can take either a plain
seqcount_t or any of the seqcount_LOCKNAME_t variants above.
Initialization (replace "LOCKNAME" with one of the supported locks)::
/* dynamic */
seqcount_LOCKNAME_t foo_seqcount;
seqcount_LOCKNAME_init(&foo_seqcount, &lock);
/* static */
static seqcount_LOCKNAME_t foo_seqcount =
SEQCNT_LOCKNAME_ZERO(foo_seqcount, &lock);
/* C99 struct init */
struct {
.seq = SEQCNT_LOCKNAME_ZERO(foo.seq, &lock),
} foo;
Write path: same as in :ref:`seqcount_t`, while running from a context
with the associated write serialization lock acquired.
Read path: same as in :ref:`seqcount_t`.
.. _seqcount_latch_t:
Latch sequence counters (``seqcount_latch_t``)
----------------------------------------------
Latch sequence counters are a multiversion concurrency control mechanism
where the embedded seqcount_t counter even/odd value is used to switch
between two copies of protected data. This allows the sequence counter
read path to safely interrupt its own write side critical section.
Use seqcount_latch_t when the write side sections cannot be protected
from interruption by readers. This is typically the case when the read
side can be invoked from NMI handlers.
Check `write_seqcount_latch()` for more information.
.. _seqlock_t:
Sequential locks (``seqlock_t``)
================================
This contains the :ref:`seqcount_t` mechanism earlier discussed, plus an
embedded spinlock for writer serialization and non-preemptibility.
If the read side section can be invoked from hardirq or softirq context,
use the write side function variants which disable interrupts or bottom
halves respectively.
Initialization::
/* dynamic */
seqlock_t foo_seqlock;
seqlock_init(&foo_seqlock);
/* static */
static DEFINE_SEQLOCK(foo_seqlock);
/* C99 struct init */
struct {
.seql = __SEQLOCK_UNLOCKED(foo.seql)
} foo;
Write path::
write_seqlock(&foo_seqlock);
/* ... [[write-side critical section]] ... */
write_sequnlock(&foo_seqlock);
Read path, three categories:
1. Normal Sequence readers which never block a writer but they must
retry if a writer is in progress by detecting change in the sequence
number. Writers do not wait for a sequence reader::
do {
seq = read_seqbegin(&foo_seqlock);
/* ... [[read-side critical section]] ... */
} while (read_seqretry(&foo_seqlock, seq));
2. Locking readers which will wait if a writer or another locking reader
is in progress. A locking reader in progress will also block a writer
from entering its critical section. This read lock is
exclusive. Unlike rwlock_t, only one locking reader can acquire it::
read_seqlock_excl(&foo_seqlock);
/* ... [[read-side critical section]] ... */
read_sequnlock_excl(&foo_seqlock);
3. Conditional lockless reader (as in 1), or locking reader (as in 2),
according to a passed marker. This is used to avoid lockless readers
starvation (too much retry loops) in case of a sharp spike in write
activity. First, a lockless read is tried (even marker passed). If
that trial fails (odd sequence counter is returned, which is used as
the next iteration marker), the lockless read is transformed to a
full locking read and no retry loop is necessary::
/* marker; even initialization */
int seq = 0;
do {
read_seqbegin_or_lock(&foo_seqlock, &seq);
/* ... [[read-side critical section]] ... */
} while (need_seqretry(&foo_seqlock, seq));
done_seqretry(&foo_seqlock, seq);
API documentation
=================
.. kernel-doc:: include/linux/seqlock.h
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
Sequence counter의 consistency protocol
1-27Sequence counter는 lockless reader가 read-only retry loop를 돌고 writer starvation이 없는 reader-writer consistency mechanism이다. System time처럼 write가 드문 data에서 reader가 일관된 snapshot을 원하고 update와 겹치면 다시 읽을 수 있을 때 사용한다.
Reader는 critical section 시작에서 sequence count가 even인지 확인하고 data 전체를 section 안에서 복사한 뒤 끝에서 같은 count를 다시 읽는다. Count가 바뀌었으면 snapshot이 update와 겹친 것이므로 재시도한다.
Writer는 critical section 시작과 끝에서 sequence count를 증가시킨다. Update 중에는 odd라 reader가 진행하지 않고, 끝나면 다시 even이 되어 reader가 consistent snapshot을 얻을 수 있다.
Writer non-preemptibility와 pointer 금지
29-36Sequence counter write-side critical section은 read-side section에 의해 preempt되거나 interrupt되어서는 안 된다. Odd count를 남긴 writer가 중단되면 reader는 scheduler tick 전체를 spin한다. Real-time reader가 writer보다 높은 priority라면 writer가 다시 실행되지 못해 kernel livelock이 될 수 있다.
보호 data가 pointer를 포함하면 writer가 reader가 따라가는 pointer를 invalidate할 수 있으므로 이 mechanism을 사용할 수 없다.
Raw seqcount_t와 외부 writer lock
39-56seqcount_t는 raw counting mechanism이며 여러 writer를 직렬화하지 않는다. Write-side critical section은 반드시 외부 lock으로 serialize해야 한다.
그 lock이 preemption을 암묵적으로 disable하지 않으면 write section 전에 명시적으로 disable한다. Reader가 hardirq 또는 softirq에서 실행될 수 있다면 writer는 각각 interrupt 또는 bottom half도 disable해야 한다. Writer serialization과 non-preemptibility를 자동으로 다루려면 seqlock_t를 사용한다.
seqcount_t 초기화와 read/write path
58-89/* dynamic */
seqcount_t foo_seqcount;
seqcount_init(&foo_seqcount);
/* static */
static seqcount_t foo_seqcount = SEQCNT_ZERO(foo_seqcount);
/* C99 struct init */
struct {
.seq = SEQCNT_ZERO(foo.seq),
} foo;
/* writer: serialized, preemption disabled */
write_seqcount_begin(&foo_seqcount);
/* update snapshot fields */
write_seqcount_end(&foo_seqcount);
/* reader */
do {
seq = read_seqcount_begin(&foo_seqcount);
/* copy all protected fields */
} while (read_seqcount_retry(&foo_seqcount, seq));
seqcount_LOCKNAME_t와 lockdep 연계
92-121seqcount_LOCKNAME_t는 writer serialization에 쓰는 lock을 초기화 시 sequence counter와 연결한다. Lockdep은 write-side critical section이 올바른 lock으로 보호되는지 검증할 수 있다.
Lockdep이 꺼져 있으면 association은 storage와 runtime overhead가 없는 NOOP다. 켜져 있으면 struct seqcount에 lock pointer를 저장하고 write section 시작에 lock-held assertion을 삽입한다. Lock type이 preemption을 암묵적으로 막지 않으면 write function이 이를 보장한다.
- seqcount_spinlock_t
- seqcount_raw_spinlock_t
- seqcount_rwlock_t
- seqcount_mutex_t
- seqcount_ww_mutex_t
Sequence counter read/write API는 plain seqcount_t와 모든 seqcount_LOCKNAME_t variant를 같은 방식으로 받는다.
Associated-lock sequence counter 초기화
123-141/* dynamic */
seqcount_LOCKNAME_t foo_seqcount;
seqcount_LOCKNAME_init(&foo_seqcount, &lock);
/* static */
static seqcount_LOCKNAME_t foo_seqcount =
SEQCNT_LOCKNAME_ZERO(foo_seqcount, &lock);
/* C99 struct init */
struct {
.seq = SEQCNT_LOCKNAME_ZERO(foo.seq, &lock),
} foo;
LOCKNAME은 지원하는 lock 이름으로 바꾼다. Write path는 associated serialization lock을 획득한 context에서 plain seqcount_t와 같은 API를 쓰고 read path도 동일하다.
seqcount_latch_t와 NMI reader
144-158seqcount_latch_t는 embedded seqcount_t의 even/odd 값을 이용해 보호 data 사본 두 개 사이를 전환하는 multiversion concurrency-control mechanism이다. Reader가 자기 write-side critical section을 interrupt해도 안전하게 읽을 수 있다.
NMI handler에서 read side가 호출되는 경우처럼 reader interruption으로부터 writer를 보호할 수 없을 때 사용한다. 자세한 protocol은 write_seqcount_latch() 문서를 확인한다.
seqlock_t의 embedded spinlock
161-185seqlock_t는 seqcount_t mechanism에 writer serialization과 non-preemptibility를 위한 spinlock을 embedded한다. Reader가 hardirq나 softirq에서 실행될 수 있다면 writer는 각각 interrupt 또는 bottom half를 disable하는 variant를 사용한다.
/* dynamic */
seqlock_t foo_seqlock;
seqlock_init(&foo_seqlock);
/* static */
static DEFINE_SEQLOCK(foo_seqlock);
/* C99 struct init */
struct {
.seql = __SEQLOCK_UNLOCKED(foo.seql)
} foo;
seqlock_t write path
187-193write_seqlock(&foo_seqlock);
/* write-side critical section */
write_sequnlock(&foo_seqlock);
Reader 1: lockless retry loop
195-206일반 sequence reader는 writer를 block하지 않는다. Writer가 진행 중이거나 sequence가 바뀌면 재시도하며 writer는 reader를 기다리지 않는다.
do {
seq = read_seqbegin(&foo_seqlock);
/* read-side critical section */
} while (read_seqretry(&foo_seqlock, seq));
Reader 2: exclusive locking reader
208-217Locking reader는 writer 또는 다른 locking reader가 진행 중이면 기다리고, 자신이 진행 중일 때 writer 진입도 막는다. rwlock_t와 달리 reader 사이에도 exclusive라 한 번에 하나만 획득한다.
read_seqlock_excl(&foo_seqlock);
/* read-side critical section */
read_sequnlock_excl(&foo_seqlock);
Reader 3: retry 폭증 시 locking으로 전환
219-235Write activity가 급증해 lockless reader가 계속 retry하며 starvation되는 것을 막는 hybrid 방식이다. Even marker로 lockless read를 먼저 시도한다. 실패해 odd sequence가 다음 marker가 되면 다음 iteration은 full locking read로 전환되어 retry가 필요 없다.
int seq = 0; /* even marker */
do {
read_seqbegin_or_lock(&foo_seqlock, &seq);
/* read-side critical section */
} while (need_seqretry(&foo_seqlock, seq));
done_seqretry(&foo_seqlock, seq);
API 원본 위치
238-241Sequence counter와 sequential lock 전체 kernel-doc은 include/linux/seqlock.h에서 생성된다.
Reader retry protocol
seqlock.rst:7-37Sequence counter는 reader가 lock을 잡지 않고 일관된 snapshot을 얻는 reader-writer mechanism입니다. Writer가 드문 system time 같은 데이터에 적합합니다. Reader는 시작 counter와 종료 counter가 같은 짝수인지 확인하고, 다르거나 홀수이면 복사한 값을 버리고 다시 읽습니다.
Writer는 시작과 끝에서 각각 counter를 증가시킵니다. Write-side critical section이 reader에 의해 interrupt된 채 counter가 홀수로 남으면 reader가 writer의 재실행을 기다리며 spin합니다. 특히 real-time reader가 writer를 영원히 선점하면 livelock이 되므로 writer context는 reader에 의해 중단되지 않도록 설계해야 합니다.
보호 데이터에 pointer가 들어 있고 writer가 그 object를 해제할 수 있다면 일반 seqcount를 사용할 수 없습니다. Reader가 pointer를 따라가는 동안 writer가 lifetime을 끝낼 수 있기 때문입니다.