Почему кольцевой буфер на N ячеек вмещает только N − 1 байт?
Обновлено Редакция GAW
В кольцевом буфере два индекса: head — куда производитель пишет следующий байт, tail — откуда потребитель читает. Документация ядра Linux описывает классическое правило: буфер пуст, когда указатели равны, и полон, когда голова на единицу «отстаёт» от хвоста. Значит, одна ячейка всегда остаётся пустой.
Почему нельзя заполнить все N
Возьмём буфер на 8 ячеек с индексами, которые заворачиваются по модулю 8:
| Записано байт | head | tail | head == tail | Состояние |
|---|---|---|---|---|
| 0 | 0 | 0 | да | пусто |
| 3 | 3 | 0 | нет | 3 байта |
| 7 | 7 | 0 | нет | полон по правилу N − 1 |
| 8 | 0 | 0 | да | неотличимо от «пусто» |
Если разрешить восьмой байт, head догонит tail, и потребитель решит, что данных нет, — потеряны все восемь байт. Поэтому запись разрешают, только пока .
Как вместить все N
Индексы не заворачивают, а просто увеличивают, и ячейку выбирают маской:
Это требует, чтобы N было степенью двойки и не больше половины диапазона индекса. Такой вариант и код для UART разобраны в статье «Кольцевой буфер для UART»; общий принцип очереди — в статье о FIFO.
Когда буфер всё же переполнился
Это значит, что данные приходят быстрее, чем их забирают. Варианты из документации SEGGER RTT: ждать места (нельзя в обработчике прерывания), отбросить новое сообщение целиком или записать столько, сколько влезет. Если же переполняется не программный буфер, а регистр приёмника, теряется байт ещё в железе — это ошибка переполнения. Почему схема с двумя индексами безопасна без запрета прерываний, объясняет статья «Прерывания и общие данные».
Похожие вопросы
Нельзя ли просто завести счётчик элементов?
Можно, но тогда счётчик меняют обе стороны — и обработчик прерывания, и основная программа, — и каждое его изменение приходится защищать запретом прерываний. Схема с двумя индексами, где каждый меняет только своя сторона, обходится без этого.
Что лучше при переполнении: терять новые байты или старые?
Зависит от данных. Для команд и пакетов обычно отбрасывают новое и ставят флаг ошибки — разборщик потом ресинхронизируется. Для журнала полезнее свежие записи. SEGGER RTT предлагает три режима: ждать места, отбросить сообщение целиком или записать сколько влезет.
Почему индекс uint8_t ограничивает буфер 128 байтами?
Со свободными индексами заполненность — разность head − tail по модулю 256. Если буфер на 256 ячеек заполнить целиком, разность станет 256, а в uint8_t это 0 — снова «пусто». Поэтому размер должен быть не больше половины диапазона индекса: до 128 для uint8_t, до 32 768 для uint16_t (наш вывод).
Источники
- The Linux Kernel documentation. Circular Buffers (Documentation/core-api/circular-buffers.rst) — буфер пуст, когда указатель головы равен указателю хвоста, и полон, когда голова на единицу меньше хвоста; в буфере всегда остаётся пустым один элемент; размер — степень двойки, индекс заворачивается маской size − 1; один производитель и один потребитель
- SEGGER Knowledge Base. RTT (Real Time Transfer) — указатель записи меняет только одна сторона, указатель чтения — только другая, поэтому гонок нет; при равенстве указателей буфер пуст; режимы при заполненном буфере: блокирующий (ждать места), skip (отбросить всё новое сообщение), trim (записать сколько влезет, остальное отбросить)
Нашли ошибку?
Смотрите также
- Кольцевой буфер UARTБуфер SPSC с маской, ISR приёма и передачи на AVR и STM32, ORE и DOR, размер
- FIFO (буфер «первым пришёл — первым ушёл»)Аппаратная очередь между периферией и программой
- Ошибка переполнения приёмникаНовый символ при непрочитанном буфере: ORE, DOR0, OE
- Прерывания и общие данныеISR на AVR и Cortex-M, атомарность, критические секции, кольцевой буфер