요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
================
Circular Buffers
================
:Author: David Howells <dhowells@redhat.com>
:Author: Paul E. McKenney <paulmck@linux.ibm.com>
Linux provides a number of features that can be used to implement circular
buffering. There are two sets of such features:
(1) Convenience functions for determining information about power-of-2 sized
buffers.
(2) Memory barriers for when the producer and the consumer of objects in the
buffer don't want to share a lock.
To use these facilities, as discussed below, there needs to be just one
producer and just one consumer. It is possible to handle multiple producers by
serialising them, and to handle multiple consumers by serialising them.
.. Contents:
(*) What is a circular buffer?
(*) Measuring power-of-2 buffers.
(*) Using memory barriers with circular buffers.
- The producer.
- The consumer.
What is a circular buffer?
==========================
First of all, what is a circular buffer? A circular buffer is a buffer of
fixed, finite size into which there are two indices:
(1) A 'head' index - the point at which the producer inserts items into the
buffer.
(2) A 'tail' index - the point at which the consumer finds the next item in
the buffer.
Typically when the tail pointer is equal to the head pointer, the buffer is
empty; and the buffer is full when the head pointer is one less than the tail
pointer.
The head index is incremented when items are added, and the tail index when
items are removed. The tail index should never jump the head index, and both
indices should be wrapped to 0 when they reach the end of the buffer, thus
allowing an infinite amount of data to flow through the buffer.
Typically, items will all be of the same unit size, but this isn't strictly
required to use the techniques below. The indices can be increased by more
than 1 if multiple items or variable-sized items are to be included in the
buffer, provided that neither index overtakes the other. The implementer must
be careful, however, as a region more than one unit in size may wrap the end of
the buffer and be broken into two segments.
Measuring power-of-2 buffers
============================
Calculation of the occupancy or the remaining capacity of an arbitrarily sized
circular buffer would normally be a slow operation, requiring the use of a
modulus (divide) instruction. However, if the buffer is of a power-of-2 size,
then a much quicker bitwise-AND instruction can be used instead.
Linux provides a set of macros for handling power-of-2 circular buffers. These
can be made use of by::
#include <linux/circ_buf.h>
The macros are:
(#) Measure the remaining capacity of a buffer::
CIRC_SPACE(head_index, tail_index, buffer_size);
This returns the amount of space left in the buffer[1] into which items
can be inserted.
(#) Measure the maximum consecutive immediate space in a buffer::
CIRC_SPACE_TO_END(head_index, tail_index, buffer_size);
This returns the amount of consecutive space left in the buffer[1] into
which items can be immediately inserted without having to wrap back to the
beginning of the buffer.
(#) Measure the occupancy of a buffer::
CIRC_CNT(head_index, tail_index, buffer_size);
This returns the number of items currently occupying a buffer[2].
(#) Measure the non-wrapping occupancy of a buffer::
CIRC_CNT_TO_END(head_index, tail_index, buffer_size);
This returns the number of consecutive items[2] that can be extracted from
the buffer without having to wrap back to the beginning of the buffer.
Each of these macros will nominally return a value between 0 and buffer_size-1,
however:
(1) CIRC_SPACE*() are intended to be used in the producer. To the producer
they will return a lower bound as the producer controls the head index,
but the consumer may still be depleting the buffer on another CPU and
moving the tail index.
To the consumer it will show an upper bound as the producer may be busy
depleting the space.
(2) CIRC_CNT*() are intended to be used in the consumer. To the consumer they
will return a lower bound as the consumer controls the tail index, but the
producer may still be filling the buffer on another CPU and moving the
head index.
To the producer it will show an upper bound as the consumer may be busy
emptying the buffer.
(3) To a third party, the order in which the writes to the indices by the
producer and consumer become visible cannot be guaranteed as they are
independent and may be made on different CPUs - so the result in such a
situation will merely be a guess, and may even be negative.
Using memory barriers with circular buffers
===========================================
By using memory barriers in conjunction with circular buffers, you can avoid
the need to:
(1) use a single lock to govern access to both ends of the buffer, thus
allowing the buffer to be filled and emptied at the same time; and
(2) use atomic counter operations.
There are two sides to this: the producer that fills the buffer, and the
consumer that empties it. Only one thing should be filling a buffer at any one
time, and only one thing should be emptying a buffer at any one time, but the
two sides can operate simultaneously.
The producer
------------
The producer will look something like this::
spin_lock(&producer_lock);
unsigned long head = buffer->head;
/* The spin_unlock() and next spin_lock() provide needed ordering. */
unsigned long tail = READ_ONCE(buffer->tail);
if (CIRC_SPACE(head, tail, buffer->size) >= 1) {
/* insert one item into the buffer */
struct item *item = buffer[head];
produce_item(item);
smp_store_release(buffer->head,
(head + 1) & (buffer->size - 1));
/* wake_up() will make sure that the head is committed before
* waking anyone up */
wake_up(consumer);
}
spin_unlock(&producer_lock);
This will instruct the CPU that the contents of the new item must be written
before the head index makes it available to the consumer and then instructs the
CPU that the revised head index must be written before the consumer is woken.
Note that wake_up() does not guarantee any sort of barrier unless something
is actually awakened. We therefore cannot rely on it for ordering. However,
there is always one element of the array left empty. Therefore, the
producer must produce two elements before it could possibly corrupt the
element currently being read by the consumer. Therefore, the unlock-lock
pair between consecutive invocations of the consumer provides the necessary
ordering between the read of the index indicating that the consumer has
vacated a given element and the write by the producer to that same element.
The Consumer
------------
The consumer will look something like this::
spin_lock(&consumer_lock);
/* Read index before reading contents at that index. */
unsigned long head = smp_load_acquire(buffer->head);
unsigned long tail = buffer->tail;
if (CIRC_CNT(head, tail, buffer->size) >= 1) {
/* extract one item from the buffer */
struct item *item = buffer[tail];
consume_item(item);
/* Finish reading descriptor before incrementing tail. */
smp_store_release(buffer->tail,
(tail + 1) & (buffer->size - 1));
}
spin_unlock(&consumer_lock);
This will instruct the CPU to make sure the index is up to date before reading
the new item, and then it shall make sure the CPU has finished reading the item
before it writes the new tail pointer, which will erase the item.
Note the use of READ_ONCE() and smp_load_acquire() to read the
opposition index. This prevents the compiler from discarding and
reloading its cached value. This isn't strictly needed if you can
be sure that the opposition index will _only_ be used the once.
The smp_load_acquire() additionally forces the CPU to order against
subsequent memory references. Similarly, smp_store_release() is used
in both algorithms to write the thread's index. This documents the
fact that we are writing to something that can be read concurrently,
prevents the compiler from tearing the store, and enforces ordering
against previous accesses.
Further reading
===============
See also Documentation/memory-barriers.txt for a description of Linux's memory
barrier facilities.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
Circular Buffers 개요
1-34저자는 David Howells <dhowells@redhat.com>와 Paul E. McKenney <paulmck@linux.ibm.com>입니다.
Linux는 circular buffering을 구현하는 데 사용할 수 있는 기능을 두 묶음으로 제공합니다.
- 크기가 power-of-2인 buffer의 상태 정보를 계산하는 convenience function
- Buffer의 object를 생산하는 producer와 소비하는 consumer가 lock을 공유하지 않으려 할 때 사용하는 memory barriers
아래의 기능은 producer가 정확히 하나이고 consumer도 정확히 하나일 때 사용할 수 있습니다. Producer가 여러 개라면 producer끼리 serialize하고, consumer가 여러 개라면 consumer끼리 serialize하여 처리할 수 있습니다.
문서는 다음 내용을 다룹니다.
- Circular buffer의 정의
- Power-of-2 buffer의 측정
- Circular buffer와 memory barrier의 사용: producer와 consumer
Circular buffer란 무엇인가
35-62Circular buffer는 크기가 고정되고 유한하며 다음 두 index를 갖는 buffer입니다.
- `head` index: producer가 item을 buffer에 삽입하는 위치
- `tail` index: consumer가 다음 item을 찾는 위치
일반적으로 tail pointer와 head pointer가 같으면 buffer가 비어 있습니다. Head pointer가 tail pointer보다 하나 앞, 즉 circular 공간에서 tail 바로 전 위치에 있으면 buffer가 가득 찬 상태입니다.
Item을 추가할 때 head index를 증가시키고 제거할 때 tail index를 증가시킵니다. Tail은 head를 넘어서는 안 되며, 두 index 모두 buffer 끝에 도달하면 0으로 wrap해야 합니다. 이렇게 하면 유한한 buffer를 통해 양에 제한 없이 data를 흘려보낼 수 있습니다.
보통 모든 item의 unit size가 같지만 아래 기법을 쓰기 위한 필수 조건은 아닙니다. 여러 item이나 가변 크기 item을 넣으려면 어느 index도 다른 index를 추월하지 않는 범위에서 index를 1보다 크게 증가시킬 수 있습니다.
다만 크기가 한 unit보다 큰 영역은 buffer 끝에서 wrap되어 두 segment로 나뉠 수 있으므로 구현자가 주의해야 합니다.
Power-of-2 buffer 측정
63-76임의 크기의 circular buffer에서 occupancy나 남은 capacity를 계산하려면 보통 느린 modulus, 즉 divide instruction이 필요합니다. Buffer 크기가 power-of-2이면 훨씬 빠른 bitwise-AND instruction으로 대신할 수 있습니다.
Linux의 power-of-2 circular buffer macro를 사용하려면 다음 header를 include합니다.
#include <linux/circ_buf.h>
CIRC_SPACE 및 CIRC_CNT macro
77-109제공되는 macro는 다음과 같습니다.
CIRC_SPACE(head_index, tail_index, buffer_size);
CIRC_SPACE_TO_END(head_index, tail_index, buffer_size);
CIRC_CNT(head_index, tail_index, buffer_size);
CIRC_CNT_TO_END(head_index, tail_index, buffer_size);
| Macro | 측정값 | wrap 처리 |
|---|---|---|
| CIRC_SPACE | Item을 삽입할 수 있도록 buffer에 남은 전체 공간[1] | 전체 circular 공간 |
| CIRC_SPACE_TO_END | 즉시 삽입할 수 있는 연속 공간[1] | Buffer 시작으로 wrap하지 않는 구간 |
| CIRC_CNT | 현재 buffer를 차지하는 item 수[2] | 전체 circular occupancy |
| CIRC_CNT_TO_END | 즉시 추출할 수 있는 연속 item 수[2] | Buffer 시작으로 wrap하지 않는 구간 |
Producer, consumer, 제3자 관점의 값
110-133각 macro는 명목상 0부터 `buffer_size-1` 사이의 값을 반환하지만, 관찰 주체에 따라 의미가 달라집니다.
- `CIRC_SPACE*()`는 producer용입니다. Producer는 head index를 제어하지만 다른 CPU의 consumer가 buffer를 비우며 tail을 옮길 수 있으므로 producer에게는 lower bound를 반환합니다. Consumer가 보면 producer가 공간을 소모하고 있을 수 있으므로 upper bound입니다.
- `CIRC_CNT*()`는 consumer용입니다. Consumer는 tail index를 제어하지만 다른 CPU의 producer가 buffer를 채우며 head를 옮길 수 있으므로 consumer에게는 lower bound를 반환합니다. Producer가 보면 consumer가 buffer를 비우고 있을 수 있으므로 upper bound입니다.
- 제3자는 서로 독립적이며 다른 CPU에서 수행될 수 있는 producer와 consumer의 index write가 어떤 순서로 보일지 보장받지 못합니다. 따라서 제3자가 얻는 결과는 추정치에 불과하고 음수가 될 수도 있습니다.
Circular buffer와 memory barrier
134-150Circular buffer에 memory barrier를 함께 사용하면 다음 작업을 피할 수 있습니다.
- Buffer 양쪽 끝의 접근을 하나의 lock으로 통제하는 것. 이를 피하면 buffer를 채우는 작업과 비우는 작업을 동시에 수행할 수 있습니다.
- Atomic counter operation을 사용하는 것
Buffer를 채우는 producer와 비우는 consumer라는 두 측면이 있습니다. 어느 시점이든 producer는 하나, consumer도 하나여야 하지만 양쪽은 동시에 동작할 수 있습니다.
Producer algorithm
151-191Producer는 대략 다음과 같이 동작합니다.
spin_lock(&producer_lock);
unsigned long head = buffer->head;
/* The spin_unlock() and next spin_lock() provide needed ordering. */
unsigned long tail = READ_ONCE(buffer->tail);
if (CIRC_SPACE(head, tail, buffer->size) >= 1) {
/* insert one item into the buffer */
struct item *item = buffer[head];
produce_item(item);
smp_store_release(buffer->head,
(head + 1) & (buffer->size - 1));
/* wake_up() will make sure that the head is committed before
* waking anyone up */
wake_up(consumer);
}
spin_unlock(&producer_lock);
`smp_store_release()`는 새 item의 내용이 consumer에게 공개하는 head index보다 먼저 write되도록 CPU에 지시합니다. 이어 `wake_up()`은 변경된 head index가 consumer를 깨우기 전에 commit되도록 합니다.
단, `wake_up()`은 실제로 무언가를 깨우지 않는 한 어떤 barrier도 보장하지 않으므로 ordering을 이것에 의존할 수 없습니다.
Array에는 항상 element 하나를 비워 둡니다. 따라서 producer가 consumer가 읽는 중인 element를 손상하려면 최소 두 element를 생산해야 합니다. 연속된 consumer 호출 사이의 unlock-lock pair가 consumer가 특정 element를 비웠음을 나타내는 index read와 producer가 같은 element에 수행하는 write 사이에 필요한 ordering을 제공합니다.
Consumer algorithm
192-232Consumer는 대략 다음과 같이 동작합니다.
spin_lock(&consumer_lock);
/* Read index before reading contents at that index. */
unsigned long head = smp_load_acquire(buffer->head);
unsigned long tail = buffer->tail;
if (CIRC_CNT(head, tail, buffer->size) >= 1) {
/* extract one item from the buffer */
struct item *item = buffer[tail];
consume_item(item);
/* Finish reading descriptor before incrementing tail. */
smp_store_release(buffer->tail,
(tail + 1) & (buffer->size - 1));
}
spin_unlock(&consumer_lock);
`smp_load_acquire()`는 새 item을 읽기 전에 head index가 최신인지 확인하게 합니다. 이어 `smp_store_release()`는 item을 지우는 새 tail pointer를 쓰기 전에 CPU가 item 읽기를 끝내도록 보장합니다.
상대편 index를 읽을 때 producer는 `READ_ONCE()`, consumer는 `smp_load_acquire()`를 사용합니다. 이는 compiler가 cache한 값을 버리고 다시 load하는 일을 막습니다. 상대편 index를 정확히 한 번만 사용한다고 확신할 수 있다면 이 조치는 엄밀히는 필요하지 않습니다.
`smp_load_acquire()`는 추가로 이후 memory reference와의 CPU ordering을 강제합니다. 두 algorithm 모두 자기 thread의 index를 쓸 때 `smp_store_release()`를 사용합니다.
이 표기는 동시에 읽힐 수 있는 대상에 write한다는 사실을 문서화하고, compiler가 store를 찢어서 수행하지 못하게 하며, 이전 access에 대한 ordering을 강제합니다.
Further reading
233-237Linux memory barrier 기능에 대한 설명은 `Documentation/memory-barriers.txt`도 참고하십시오.
요약과 해설
circular-buffers.rst:1-237Head는 producer가 쓸 위치이고 tail은 consumer가 읽을 위치입니다. 두 값이 같으면 비어 있고, 항상 한 칸을 비워 둠으로써 가득 찬 상태를 모호함 없이 표현합니다.
Buffer 크기를 power-of-2로 제한하면 modulus 대신 bitwise AND로 index를 wrap하고 `CIRC_SPACE*`와 `CIRC_CNT*`로 남은 공간과 occupancy를 빠르게 계산할 수 있습니다.
단일 producer와 단일 consumer는 서로 다른 lock을 사용하면서 `smp_store_release()`와 `smp_load_acquire()`로 item data와 index 공개 순서를 연결할 수 있습니다. 여러 producer 또는 consumer가 필요하면 각 집단 내부를 별도로 serialize해야 합니다.