libc: FILE* v2 — буферизация потоков (вариант B+)
- единый ленивый буфер BUFSIZ=512 на чтение и запись с автопереключением направления (_F_DIROUT, _file_sync: запись сбрасывается write()-ом, readahead откатывается lseek-ом) - статическая таблица OPEN_MAX=8 слотов вместо malloc для FILE; _fclosall через atexit — exit() сбрасывает несброшенную запись - fread/fwrite: мелкое через буфер (memcpy), блоки >= BUFSIZ — мимо буфера одним syscall; горячие пути fgetc/fputc и сканер строк fgets (LDI до '\n') — на asm, SDCC на эти цепочки генерит ~90 инструкций с IX-фреймом - новое: ungetc (1 байт через hold, работает и на stdin), fprintf/vfprintf (vsprintf+fwrite), fflush(NULL) = все потоки - фиксы stdio-review: fwrite ставит _F_ERROR при короткой записи (issue 3), fgets(n=1) возвращает пустую строку (issue 4) - замер (MAME, HDD, 100 КБ): небуферизованная оценка ~144 с → fgetc 5 с (×29), fgets ~1 с; дизайн и отвергнутые варианты — docs/file-buffering-design.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
# FILE*: буферизация — анализ solid-c и варианты (2026-07-06)
|
||||
|
||||
Статус: **вариант B+ РЕАЛИЗОВАН 2026-07-06** (решение пользователя).
|
||||
Единый ленивый буфер BUFSIZ=512 на чтение/запись с автопереключением
|
||||
направления (_F_DIROUT), статическая таблица OPEN_MAX=8 слотов
|
||||
(без malloc для FILE), _fclosall через atexit, ungetc через hold,
|
||||
fprintf/vfprintf через vsprintf+fwrite, fflush(NULL) = все потоки.
|
||||
Внутренности: libc/file/_file.h (+_file_sync/_file_buf/_file_slots/
|
||||
_fclosall). Фактическая цена: filetest 7411→9929 Б _CODE (доля
|
||||
только-читающих потребителей ~+1.3 КБ, включая malloc); программы
|
||||
без FILE* не платят. Верификация: MAME filetest + fdmax + fbench.
|
||||
Ниже — исходный анализ, на основании которого принималось решение.
|
||||
|
||||
## Как сделано в solid-c (SRC/CLIB/STDIO.ASM)
|
||||
|
||||
Структура FILE — 14 байт, статический массив `_iob[8]` (без malloc
|
||||
для самих FILE; псевдопотоки stdout/stderr/stdaux/stdprn лежат ПЕРЕД
|
||||
массивом и адресуются отрицательными индексами — трюк, нам не нужен):
|
||||
|
||||
flags(2), level(2), curp(2), fd(2), buffer(2), hold(1), token(2), dummy(1)
|
||||
|
||||
Механика:
|
||||
|
||||
- **Буфер 512 Б, ленивый malloc** при первом буферизуемом fgetc/fputc
|
||||
(флаг `_F_BUF` = «буфер наш, free при fclose»). Программа без
|
||||
файлового I/O не платит ничего.
|
||||
- **Чтение** (`_fgetc`): `level == 0` → `read(fd, buffer, 512)`,
|
||||
`curp = buffer`; отдача — `*curp++`, `level--`; ставится `_F_IN`.
|
||||
- **Запись** (`_fputc`): `*curp++ = c`, `level++`; при `level == 512` —
|
||||
fflush (один `write` всего буфера); ставится `_F_OUT`.
|
||||
- **Полудуплекс**: fputc при взведённом `_F_IN` — ОШИБКА (не
|
||||
авто-flush); направление сбрасывает только fflush.
|
||||
- **fflush входного потока**: `lseek(fd, -level, SEEK_CUR)` — откат
|
||||
непрочитанного readahead, буфер инвалидируется. Выходного —
|
||||
`write(buffer, level)`.
|
||||
- **fseek/ftell** = fflush + голый lseek/ltell по fd (после flush
|
||||
позиция fd совпадает с логической позицией потока).
|
||||
- **ungetc**: буфер не пуст → `*--curp = c`; пуст/отсутствует → символ
|
||||
в поле `hold`, `curp` указывает на hold.
|
||||
- **fclosall через atexit** — сброс буферов при exit.
|
||||
- Консольные потоки минуют буфер (RST-вызовы напрямую).
|
||||
- **fread/fwrite — ПОБАЙТОВЫЙ цикл** через _fgetc/_fputc: большие блоки
|
||||
платят call+IY-доступ за каждый байт. Это слабое место порта.
|
||||
|
||||
## Варианты для нас
|
||||
|
||||
**A. Полный порт solid-c** (буфер на чтение и запись).
|
||||
Плюсы: ускоряются и писатели через fputc/fprintf. Минусы: полудуплекс
|
||||
(«запись после чтения без fflush — ошибка») — источник тонких багов;
|
||||
обязателен flush в exit (сцепка atexit+file); больше кода во всех
|
||||
модулях; наши блочные fread/fwrite пришлось бы защищать от деградации.
|
||||
|
||||
**B. Буферизовать ТОЛЬКО чтение (рекомендую).**
|
||||
FILE += `buf(2), level(2), curp(2), hold(2)`; буфер 512 Б лениво.
|
||||
|
||||
- fgetc: hold → буфер → refill. fgets остаётся циклом по fgetc
|
||||
(теперь дешёвым).
|
||||
- fread: сначала хвост буфера (memcpy), остаток ≥ 512 — прямой read()
|
||||
в ptr одним syscall (обходя буфер), мелкий остаток — refill.
|
||||
- Запись НЕ буферизуется — как сейчас: fputc = write(1 байт),
|
||||
fputs/fwrite = один write() на блок. Нечего терять при аварии,
|
||||
fflush остаётся no-op по данным, полудуплекса нет.
|
||||
- Согласование позиций: перед write/fseek/ftell на потоке с readahead —
|
||||
`lseek(fd, -(level), SEEK_CUR)` + инвалидация буфера (один общий
|
||||
хелпер `_file_sync`). ftell = lseek(0,CUR) − level (без syscall не
|
||||
выйдет — lseek и так syscall).
|
||||
- ungetc: через hold, работает и до первого заполнения буфера.
|
||||
|
||||
Плюсы: решает главную боль (парсеры), запись остаётся простой и
|
||||
надёжной, никакого flush-on-exit, r+ работает через _file_sync.
|
||||
Минусы: fputc-писатели остаются медленными (редкий паттерн — fputs/
|
||||
fwrite блочные и так быстрые).
|
||||
|
||||
**B+. Единый буфер на чтение И запись с АВТОпереключением направления
|
||||
(предложение 2026-07-06, кандидат в целевой дизайн).**
|
||||
Схема solid-c, но без ловушки: направление переключает сама библиотека.
|
||||
|
||||
- флаг направления в FILE: буфер сейчас «readahead» или «накопитель
|
||||
записи»;
|
||||
- fputc при направлении «чтение»: `_file_sync` (отмотка fd на -level,
|
||||
буфер пуст) → режим записи → накопление; сброс write() при
|
||||
заполнении;
|
||||
- fgetc при направлении «запись»: flush (write(buf, level)) → режим
|
||||
чтения → refill;
|
||||
- fwrite больших блоков: flush + прямой write мимо буфера; мелких —
|
||||
memcpy в буфер. fread симметрично;
|
||||
- fseek/ftell/fclose: flush-или-sync по направлению; ftell = позиция
|
||||
fd − level (чтение) / + level (запись);
|
||||
- **обязателен реестр открытых потоков**: поле next в FILE
|
||||
(регистрация в fopen, снятие в fclose) + _fclosall через atexit —
|
||||
стандарт требует flush всех потоков в exit(); без этого
|
||||
`fputs(...); exit(1);` теряет данные;
|
||||
- цена-семантика: ошибки записи становятся ОТЛОЖЕННЫМИ (вылезают при
|
||||
flush/fclose, не в момент fputc) — проверять результат fclose;
|
||||
- цена-код: ~+350–500 Б против ~+200–300 у B (тянется только
|
||||
использующими FILE*).
|
||||
|
||||
**C. Оставить небуферизованным** («большие файлы читаются целиком в
|
||||
EMM», паттерн mdview). Для приложений-парсеров среднего размера
|
||||
неудобно; отвергается самим существованием П2-пункта.
|
||||
|
||||
**D. Полная стандартная буферизация + setvbuf** — отвергнуто ранее
|
||||
решением file_star_design (минимальный FILE*).
|
||||
|
||||
## Что взять у solid-c при варианте B
|
||||
|
||||
- ленивый malloc 512 Б + флаг «буфер наш»;
|
||||
- откат readahead lseek'ом (механика их fflush-на-вход) — как
|
||||
`_file_sync` перед write/fseek/ftell;
|
||||
- ungetc с hold-байтом;
|
||||
- консольные потоки мимо буфера (у нас уже так).
|
||||
|
||||
Чего НЕ брать: побайтовые fread/fwrite, полудуплекс, статический
|
||||
`_iob[]` с отрицательными индексами, буферизацию записи.
|
||||
|
||||
## Лимит открытых файлов и статическая таблица FILE (2026-07-06)
|
||||
|
||||
Факты: solid-c — OPEN_MAX = 8, статический массив из 8 FILE-структур
|
||||
(+5 псевдопотоков перед ним), fdopen отвергает fd > 8. DSS-доки:
|
||||
FCB строятся «в рабочих областях ДОС», код ошибки 06h = «Too many
|
||||
open files» (наш EMFILE = 6 совпадает).
|
||||
|
||||
**ПОДТВЕРЖДЕНО тестом fdmax (MAME, DSS 1.71.57, 2026-07-06)**:
|
||||
пользователю доступно 8 манипуляторов, fd 2..9 (fd 1 держит шелл DSS
|
||||
под запущенный .exe). КРИТИЧНО: 9-й OPEN не возвращает 06h — он
|
||||
ВЕШАЕТ систему. Поэтому в libc/io добавлен предохранитель _fd_guard
|
||||
(счётчик в open/close, отказ EMFILE на 9-м open без захода в DSS) —
|
||||
таблица fopen и guard вместе закрывают и высокий, и низкий уровень.
|
||||
|
||||
Следствие для B+: вместо malloc-FILE + связного списка-реестра —
|
||||
**статическая таблица из 8 слотов** (свободный слот: flags == 0):
|
||||
- реестр для flush-on-exit бесплатен: _fclosall = цикл по таблице;
|
||||
- fopen без malloc — единственный отказ синхронен с отказом DSS
|
||||
(EMFILE), утечка «fclose без free» невозможна;
|
||||
- цена: ~112–128 Б BSS (в exe не входит), только у программ с fopen;
|
||||
- буферы НЕ статические — остаются ленивыми malloc 512 Б (потолок
|
||||
8×512 = 4 КБ heap в худшем случае);
|
||||
- объявить FOPEN_MAX 8 в stdio.h; если fdmax покажет лимит DSS < 8 —
|
||||
уменьшить таблицу.
|
||||
|
||||
## Семантика инвалидации (вариант B) — контрольный сценарий
|
||||
|
||||
Буфер — только кэш опережающего чтения. Правило: **любая запись и
|
||||
любой fseek обнуляют буфер; запись всегда идёт напрямую в файл после
|
||||
отмотки позиции** (`_file_sync`: `lseek(fd, -level, SEEK_CUR)` +
|
||||
`level = 0`). Буфер при записи НЕ патчится — write-through с правкой
|
||||
окна отвергнут как сложный ради редкого паттерна.
|
||||
|
||||
Сценарий «r+, чтение-запись-чтение» (обсуждено 2026-07-06):
|
||||
read 512 в буфер → 10×fgetc (логическая поз. 10, fd на 512) →
|
||||
первый fputc: sync отматывает fd на 10, буфер пуст, 10×write ложатся
|
||||
на 10..19 → fseek(0) → fgetc перечитывает буфер С ДИСКА и видит
|
||||
записанные байты. Протечка старой копии невозможна — она уничтожена
|
||||
в момент первой записи.
|
||||
|
||||
## Оценка/проверка
|
||||
|
||||
Бенчмарк до/после: цикл fgets по tests/seek/big.txt с замером ptime
|
||||
(тест tests/fbench), плюс filetest-регресс в MAME. Ожидание: чтение
|
||||
~512× меньше syscall'ов; код file-модулей +200–300 Б (тянется только
|
||||
использующими FILE*).
|
||||
Reference in New Issue
Block a user