Буферизация, кэш и модели ввода-вывода
Почему программа не ждёт диск на каждый байт и как ОС прячет медлительность устройств.
Буферизация — накопление данных в памяти, чтобы обмениваться с устройством крупными порциями; дисковый кэш — хранение недавно прочитанных блоков в памяти, чтобы не читать их с диска повторно.
Зачем буферизация
Диск медленный, а каждый системный вызов дорог. Если писать файл по байту, это тысячи дорогих обращений к диску. Решение — буфер: данные копятся в памяти и сбрасываются на диск разом, когда буфер заполнится. Один большой write вместо тысячи маленьких — огромная экономия.
Та же идея при чтении: ОС читает с диска не запрошенный байт, а целый блок «с запасом», рассчитывая на пространственную локальность. Следующие чтения соседних байтов попадут уже в буфер.
Дисковый кэш (page cache)
ОС держит недавно использованные блоки диска в оперативной памяти — это дисковый кэш. Если программа просит блок, который уже в кэше, диск не трогают вовсе — отдают из памяти. Поэтому второе чтение того же файла обычно мгновенно. Свободную RAM ОС охотно занимает под кэш — отсюда «вся память занята» в мониторе, хотя это полезная занятость.
cache = {} # блок -> данные в памяти
disk_reads = 0
def read_block(num):
global disk_reads
if num in cache:
return "из кэша (быстро)"
disk_reads += 1
cache[num] = f"данные блока {num}"
return "с диска (медленно)"
requests = [10, 11, 10, 12, 11, 10]
for r in requests:
print(f"Запрос блока {r}: {read_block(r)}")
print(f"Запросов всего: {len(requests)}")
print(f"Реальных чтений с диска: {disk_reads}")
print(f"Сэкономлено обращений к диску: {len(requests) - disk_reads}")
Вывод:
Запрос блока 10: с диска (медленно) Запрос блока 11: с диска (медленно) Запрос блока 10: из кэша (быстро) Запрос блока 12: с диска (медленно) Запрос блока 11: из кэша (быстро) Запрос блока 10: из кэша (быстро) Запросов всего: 6 Реальных чтений с диска: 3 Сэкономлено обращений к диску: 3
Цена кэша: данные могут потеряться
У буферизации и кэша есть оборотная сторона: если данные ещё в буфере и не записаны на диск, а тут отключили питание — они пропадут. Поэтому есть операция flush/sync — принудительный сброс буфера на диск. Базы данных делают sync в нужные моменты, чтобы гарантировать сохранность.
Модели ввода-вывода
Когда программа запрашивает ввод-вывод (например, читает из сети или с диска), у неё есть выбор, как ждать результата.
Блокирующий ввод-вывод
Программа вызывает read и ждёт, пока данные не придут. Поток «замирает» — ничего другого не делает. Просто в написании, но неэффективно, если ждать приходится долго (например, ответа по сети).
Неблокирующий ввод-вывод
Вызов сразу возвращает управление: «данных пока нет, зайди позже». Программа может заняться чем-то ещё и периодически проверять готовность. Сложнее, но поток не простаивает.
Асинхронный ввод-вывод
Программа говорит «начни читать и сообщи, когда будет готово», и продолжает работу. Когда данные придут, ОС уведомит (через callback или событие). Так работают высоконагруженные серверы, обслуживающие тысячи соединений одним потоком.
| Модель | Поток ждёт? | Сложность |
| Блокирующий | да, замирает | простая |
| Неблокирующий | нет, опрашивает | средняя |
| Асинхронный | нет, уведомят | высокая |
Полезные команды
free -h # память: сколько занято кэшем (buff/cache)
sync # сбросить все буферы на диск
iostat # статистика ввода-вывода по устройствам
Итог
- Буферизация копит данные в памяти, чтобы обмениваться с диском крупными порциями.
- Дисковый кэш хранит недавние блоки в RAM — повторное чтение идёт без диска.
- Кэш рискует потерей данных при сбое питания; для надёжности есть flush/sync.
- Блокирующий ввод-вывод прост, но поток ждёт; неблокирующий и асинхронный не простаивают.
- Асинхронный ввод-вывод позволяет одному потоку обслуживать тысячи соединений.