Стандартные потоки (stdin, stdout, stderr)

21 сентября 2026 · ~15 мин чтения

unix потоки консоль логирование основы

Стандартные потоки (stdin, stdout, stderr)

Стандартные потоки — три канала ввода-вывода, которые операционная система
открывает для каждой программы ещё до того, как та начала работать: вход
(stdin), выход с результатом (stdout) и выход с сообщениями о проблемах
(stderr). Программа не знает и не должна знать, что на другом конце —
клавиатура, файл, сеть или другая программа.

История

Всё начинается в Bell Labs. В 1969–1970 годах Кен Томпсон и Деннис Ритчи
строят Unix — маленькую операционную систему после провала громоздкого
проекта Multics. Одна из их центральных идей: всё есть файл. Диск — файл,
терминал — файл, принтер — файл. Программа работает не с устройством, а с
номером — «файловым дескриптором» (file descriptor, небольшое целое число,
которым ядро обозначает открытый канал).

Раз так, то почему бы не открыть каждой программе пару дескрипторов заранее?
Так появились дескриптор 0 (вход) и 1 (выход). Программе не нужно
ничего открывать: она просто читает из нуля и пишет в единицу.

Второй кирпич положил Дуглас Макилрой. Ещё в 1964 году он написал
внутреннюю записку с идеей: должен быть способ соединять программы «как
садовые шланги» — выход одной прямо во вход другой. Идея пролежала почти
десять лет. В 1973 году Макилрой додавил Томпсона, и тот, по легенде, за
одну ночь реализовал конвейеры (pipes) — они появились в Version 3 Unix.
Наутро все системные утилиты переписали так, чтобы они умели читать из stdin и
писать в stdout. Символ | как знак конвейера закрепился примерно тогда же
(ранние варианты синтаксиса были другими, и Макилрой в воспоминаниях
описывает несколько промежуточных нотаций).

Третий поток — stderr, дескриптор 2 — появился позже, примерно в середине
1970-х (Version 6 / Version 7 Unix). Причина была насквозь практической. Когда
вывод программы перенаправляли в файл, сообщения об ошибках уезжали туда же —
и человек сидел перед пустым молчащим терминалом, не понимая, что всё
сломалось. Ритчи в статье «The Evolution of the Unix Time-sharing System»
(1979) описывал эту проблему на примере фотонабора: диагностические сообщения
попадали прямо в набираемый текст и портили вёрстку. Лекарство —
отдельный канал, который по умолчанию всегда смотрит на экран.

Вехи после этого:

Пятьдесят с лишним лет — и ни одной замены. Это редкость даже для
инженерии.

Что это такое

Поток (stream) — это последовательность байтов, у которой есть направление и
нет заранее известной длины. Не файл, не сообщение, не запись — просто
байты, которые кончатся когда-нибудь. Программа либо тянет их (читает), либо
толкает (пишет).

Операционная система при запуске любого процесса открывает три таких потока:

Дескриптор Имя Направление Куда смотрит по умолчанию
0 stdin чтение клавиатура терминала
1 stdout запись экран терминала
2 stderr запись экран терминала

Ключевой фокус в том, что программа не знает, что на другом конце.
Утилита grep пишет в дескриптор 1 совершенно одинаково, независимо от того,
уйдёт результат на экран, в файл, в другую программу или по сети. Решение
принимает не программа, а тот, кто её запускает. Это называется
инверсией управления вводом-выводом: право выбора источника и приёмника
отдано наружу.

Отсюда и разделение stdout/stderr, которое поначалу кажется избыточным. Оба
потока пишут, оба по умолчанию идут на экран — зачем два? Затем, что у них
разная семантика:

Явные пары для сравнения:

stdout vs stderr. Первый — результат, второй — комментарий к работе. Если
сомневаешься, куда писать строку, задай вопрос: «хочу ли я, чтобы эта строка
попала в файл, если пользователь перенаправит вывод?» Если нет — это stderr.

Поток vs файл. Файл можно перемотать, прочитать с конца, узнать размер.
Поток — нельзя: байты приходят один раз и в одну сторону. Именно поэтому
конвейер из десяти программ не требует ни гигабайта памяти, ни временных
файлов.

Потоки vs аргументы командной строки. Аргументы — это настройки
(«как делать»), потоки — данные («что обрабатывать»). Путаница здесь —
типичная ошибка новичка: передавать мегабайт данных аргументом вместо stdin.
У аргументов есть жёсткий лимит длины (в macOS и Linux — порядка сотен
килобайт-мегабайтов), у потока лимита нет.

Аналогии из жизни

Кухня ресторана. Повар ставит готовое блюдо в окно выдачи (stdout) — его
забирает официант и несёт дальше. Если что-то пошло не так (кончился соус,
пригорело), повар кричит об этом в зал шефу через отдельное окошко (stderr).
Блюдо и жалоба идут по разным маршрутам, и никто не подаёт клиенту тарелку с
запиской «закончился базилик».

Где ломается: на реальной кухне оба сообщения в итоге проходят через одного
человека — официанта, и порядок гарантирован. В Unix stdout и stderr —
физически разные каналы, и если оба направить в один файл, порядок строк может
перепутаться из-за буферизации (об этом ниже). Аналогия не передаёт эту
асимметрию.

Заводской конвейер. Цех получает заготовку, что-то с ней делает,
передаёт дальше (конвейер = pipe). Брак снимают с ленты и складывают в
отдельный лоток. Каждый цех не знает ни первого, ни последнего звена, он знает
только «беру слева, кладу справа». Можно переставлять цеха местами и собирать
новую линию без перестройки завода.

Где ломается: по конвейеру едет сам предмет — он существует в одном
экземпляре. В потоке едут байты, которые можно продублировать (tee),
выбросить в никуда (/dev/null) или сгенерировать бесконечно (yes).
Физической сохранности вещества тут нет.

Диспетчер такси по рации. Диспетчер принимает заявки (stdin), выдаёт
адреса водителям (stdout), а экстренные сообщения («авария на Садовом») идёт
по аварийному каналу (stderr), который слушают все и всегда.

Где ломается: рация — широковещательная и двунаправленная, любой может
ответить. Стандартный поток строго однонаправленный: в stdout нельзя «ответить
назад». Для диалога нужны два потока или сокет.

Как это работает

Разберём по шагам, что происходит, когда ты набираешь в терминале команду.

Шаг 1. Оболочка разбирает строку. Ты пишешь:

grep ERROR app.log | sort | uniq -c > report.txt 2> errors.txt

Оболочка (bash, zsh) видит три команды, два конвейера и два перенаправления.

Шаг 2. Оболочка создаёт каналы. Для каждого | вызывается системный вызов
pipe() — ядро создаёт буфер в памяти (обычно 64 КБ в Linux) с двумя концами:
читающим и пишущим.

Шаг 3. Ветвление и подмена дескрипторов. Для каждой команды оболочка
делает fork() (создаёт копию процесса), а в копии — самое интересное:
вызывает dup2(), чтобы подменить дескрипторы. Дескриптор 1 у grep
перестаёт указывать на терминал и начинает указывать на пишущий конец первого
канала. Дескриптор 0 у sort — на читающий конец. И так далее. Только после
этого вызывается exec(), который загружает саму программу.

Критически важная деталь: подмена происходит до запуска программы. Когда
код grep начинает выполняться, всё уже подключено. Именно поэтому grep не
содержит ни строчки про конвейеры — он о них не знает.

grep ──1──> [pipe] ──0──> sort ──1──> [pipe] ──0──> uniq ──1──> report.txt
  │                          │                        │
  2                          2                        2
  └──────────────────────────┴────────────────────────┴──> errors.txt

Шаг 4. Все работают одновременно. Это не последовательность «grep
закончил — начал sort». Все три процесса запускаются сразу и работают
параллельно. Когда буфер канала заполняется, а читатель не успевает, ядро
просто усыпляет писателя — это называется backpressure (обратное
давление)
. Никто не переполнит память.

Шаг 5. Конец потока. Когда grep завершается, ядро закрывает его конец
канала. sort получает при чтении ноль байт — это и есть EOF (end of file,
конец файла). Он понимает, что данных больше не будет, досортировывает и
завершается сам. Цепочка схлопывается слева направо.

Шаг 6. Код возврата. Отдельно от потоков каждая программа возвращает число
0–255. Ноль — успех, остальное — вид неудачи. Потоки несут данные, код
возврата несёт вердикт.

Теперь про главную ловушку — буферизацию. Библиотека stdio не пишет в
систему каждый байт (это было бы очень дорого), она копит их в буфере. Режим
выбирается автоматически:

Почему логи «перепутались»

Если направить stdout и stderr в один файл (cmd > log.txt 2>&1), строки могут лечь не в хронологическом порядке: stderr улетает мгновенно, а stdout ждёт, пока накопится 8 КБ. Причина не в программе и не в диске. Лечится принудительным сбросом буфера в коде (flush), либо запуском через stdbuf -oL (Linux) / script (macOS), либо в Python — флагом -u.

И ещё одно следствие того же механизма: многие программы меняют поведение,
заметив, что stdout — не терминал. ls отключает колонки и цвет, git log
не запускает пейджер, curl прячет прогресс-бар. Проверяется это вызовом
isatty(). Полезно помнить, когда скрипт «в консоли работает, а в cron
выдаёт другое».

Где встречается в обычной жизни

Где встречается в IT и бизнесе

Почему это выжило

Потоки — это соглашение об интерфейсе, которое ничего не требует от участников: ни общего языка, ни версии библиотеки, ни формата. Только «байты идут слева направо». Любой более умный интерфейс — RPC, брокеры сообщений, gRPC — требует, чтобы обе стороны согласились на общую схему. Чем меньше договорённость, тем дольше она живёт.

Кто пользуется

Честный ответ — практически все, кто пишет серверный софт. Несколько
конкретных масштабов:

Точных цифр по нагрузке тут не бывает: механизм слишком низкоуровневый, чтобы
его кто-то считал отдельно. Он просто есть везде.

Альтернативы и конкуренты

Логирование прямо в файл (библиотеки logging, log4j, winston).
Плюсы: полный контроль над форматом, ротацией, уровнями важности,
структурированный JSON из коробки.
Минусы: приложение начинает отвечать за инфраструктуру — ротацию, права,
переполнение диска; в контейнере файл вообще исчезает вместе с ним.

syslog (RFC 5424).
Плюсы: единая точка сбора для всей машины, уровни важности, отправка по сети,
десятилетия совместимости.
Минусы: нужен демон и настройка; UDP-вариант теряет сообщения молча; формат
сообщения жёстче, чем «произвольные байты».

Именованные каналы (FIFO) и Unix-сокеты.
Плюсы: живут отдельно от процесса, позволяют соединять программы,
запущенные независимо; сокеты умеют двусторонний обмен.
Минусы: требуют явного создания и уборки, сложнее обработка обрывов, нет
автоматического закрытия при завершении процесса.

Брокеры сообщений (Kafka, RabbitMQ, NATS).
Плюсы: доставка между машинами, сохранение истории, много читателей на один
поток, гарантии доставки.
Минусы: отдельная инфраструктура, которую надо поднимать, мониторить и
обновлять; несопоставимая сложность ради задачи «передать вывод дальше».

Когда НЕ стоит использовать

Когда нужен двусторонний диалог без тупика. Если процесс A пишет в B и
ждёт ответа, а B делает то же самое, оба могут заснуть в ожидании — классический
deadlock (взаимная блокировка) на буферах каналов. Тут нужны сокеты или
явный протокол с признаком конца сообщения.

Когда вывода очень много и он критичен. stderr не буферизуется: каждая
строка — системный вызов. Сто тысяч строк в секунду в stderr заметно тормозят
программу. Для нагруженного логирования нужен буферизованный писатель или
отдельный поток-логгер.

Когда данные бинарные и адресат — человек. Смешивать бинарный stdout с
текстовым stderr на одном терминале — верный способ испортить настройки
терминала управляющими последовательностями. И никогда не стоит писать в
потоки секреты: вывод скрипта почти наверняка окажется в логе CI, в journald
или в чьём-то чате.

Связанные понятия

Литература и источники

Где встретилось у меня

Вчера понятие всплыло в работе над самим генератором этого дайджеста.
В инструкции для генерации прямо прописано: если данных за день нет — сообщить
об этом в stderr и завершиться, не выдумывая тему. Плюс оркестратор запускается
по расписанию через launchd, где терминала нет вовсе, и оба потока приходится
явно направлять в файлы логов — иначе о неудачном запуске никто бы не узнал.
Классическая иллюстрация того, ради чего stderr пятьдесят лет назад и отделили
от stdout.

Краткое резюме