# 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*).