Стандартные потоки (stdin, stdout, stderr)
Стандартные потоки (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) описывал эту проблему на примере фотонабора: диагностические сообщения
попадали прямо в набираемый текст и портили вёрстку. Лекарство —
отдельный канал, который по умолчанию всегда смотрит на экран.
Вехи после этого:
- 1979, Version 7 Unix — библиотека stdio Ритчи с объектами
stdin,
stdout,stderrв языке C. Названия, которые мы используем сегодня, —
оттуда. - 1988, POSIX.1 (IEEE Std 1003.1) — поведение потоков стандартизовано. С
тех пор это не «традиция Unix», а формальная спецификация (позже —
ISO/IEC 9945). - 1990-е — наши дни — модель пережила все смены эпох. Windows, изначально
чужая этой идее, всё равно имеетCONIN$/CONOUT$и полноценно понимает
перенаправление вcmdи PowerShell. - 2011 — методология 12-factor app (Адам Уиггинс, Heroku) объявляет:
приложение не должно управлять файлами логов, оно обязано писать события в
stdout, а среда сама решит, куда их деть. Docker и Kubernetes построили
сбор логов ровно на этом.
Пятьдесят с лишним лет — и ни одной замены. Это редкость даже для
инженерии.
Что это такое
Поток (stream) — это последовательность байтов, у которой есть направление и
нет заранее известной длины. Не файл, не сообщение, не запись — просто
байты, которые кончатся когда-нибудь. Программа либо тянет их (читает), либо
толкает (пишет).
Операционная система при запуске любого процесса открывает три таких потока:
| Дескриптор | Имя | Направление | Куда смотрит по умолчанию |
|---|---|---|---|
| 0 | stdin | чтение | клавиатура терминала |
| 1 | stdout | запись | экран терминала |
| 2 | stderr | запись | экран терминала |
Ключевой фокус в том, что программа не знает, что на другом конце.
Утилита grep пишет в дескриптор 1 совершенно одинаково, независимо от того,
уйдёт результат на экран, в файл, в другую программу или по сети. Решение
принимает не программа, а тот, кто её запускает. Это называется
инверсией управления вводом-выводом: право выбора источника и приёмника
отдано наружу.
Отсюда и разделение stdout/stderr, которое поначалу кажется избыточным. Оба
потока пишут, оба по умолчанию идут на экран — зачем два? Затем, что у них
разная семантика:
- 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 на терминал — построчно (line-buffered): увидел
\n, сбросил; - stdout в файл или канал — блоками (обычно 4 или 8 КБ, в glibc
BUFSIZ=
8192); - stderr — не буферизуется вообще, каждое сообщение уходит немедленно.
Почему логи «перепутались»
Если направить 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
выдаёт другое».
Где встречается в обычной жизни
- Скачивание файла с прогресс-баром. Бегущие проценты в
curlилиwget
идут в stderr, а сам файл — в stdout. Поэтомуcurl URL > file.zipкладёт
в архив ровно архив, а не архив вперемешку с процентами. - «Отправь мне лог, когда упало». То, что ты пересылаешь в поддержку, —
это почти всегда содержимое stderr, которое кто-то заботливо перенаправил
в файл. - Тишина у запланированной задачи. Скрипт запускается по расписанию
(cron на Linux, launchd на macOS), падает — и ты об этом не знаешь. Потому
что терминала нет, stderr некуда показывать. Ровно поэтому в конфигурации
launchd есть поляStandardOutPathиStandardErrorPath. - Копирование текста из консоли. Когда ты выделяешь мышкой вывод команды —
ты выделяешь stdout и stderr вперемешку, потому что на экране они
склеены. Отличить их глазами нельзя: терминал не помечает, откуда строка. - Ввод пароля. Приглашение «Password:» печатается в stderr (чтобы не
попасть в перенаправленный вывод), а сам пароль читается вообще не из
stdin, а напрямую с терминала — чтобы его нельзя было подать из файла.
Где встречается в IT и бизнесе
- Логи контейнеров. Docker и Kubernetes ничего не знают про файлы логов
внутри контейнера.docker logsиkubectl logsпоказывают ровно stdout и
stderr процесса номер 1. Вся индустрия сбора логов последние десять лет
стоит на этом соглашении. - CI/CD. GitHub Actions, GitLab CI, Jenkins собирают вывод каждого шага из
потоков, а решение «шаг упал или нет» принимают по коду возврата. Лог в
веб-интерфейсе — это буквально склеенные stdout и stderr. - Обработка данных (ETL). Классическая конвейерная связка
cat data.csv | jq ... | sort | gzip > out.gzобрабатывает файл, который не
влезает в оперативную память, потому что через память проходит только
текущий кусок. - Интеграция разнородных программ. Когда надо связать код на Python с
утилитой на C и скриптом на bash — потоки остаются самым дешёвым
интерфейсом: не нужен ни общий язык, ни библиотека, ни сетевой порт. - MCP и инструменты для ИИ-агентов. Транспорт stdio в Model Context
Protocol — это ровно тот же механизм: агент запускает сервер как
подпроцесс и разговаривает с ним JSON-сообщениями через stdin/stdout.
Протоколу 2024 года, а транспорту — пятьдесят.
Почему это выжило
Потоки — это соглашение об интерфейсе, которое ничего не требует от участников: ни общего языка, ни версии библиотеки, ни формата. Только «байты идут слева направо». Любой более умный интерфейс — RPC, брокеры сообщений, gRPC — требует, чтобы обе стороны согласились на общую схему. Чем меньше договорённость, тем дольше она живёт.
Кто пользуется
Честный ответ — практически все, кто пишет серверный софт. Несколько
конкретных масштабов:
- Docker (с 2013 года, компания Docker Inc.) — сбор логов через потоки
стал индустриальной нормой; счёт контейнерных запусков идёт на миллиарды. - Kubernetes (Google, открыт в 2014, сейчас под CNCF) — механизм
kubectl logsи все агрегаторы вроде Fluent Bit читают потоки. - Heroku — платформа, сформулировавшая 12-factor в 2011; её модель
«логи как поток событий» разошлась по всей отрасли. - GNU Coreutils — около сотни утилит (
cat,sort,wc,head), каждая
из которых устроена как «stdin → stdout». Установлены примерно на каждом
Linux-сервере планеты. - systemd и journald (Леннарт Поттеринг, 2010) — служба пишет в stdout,
journald подхватывает и складывает в бинарный журнал с метаданными.
Точных цифр по нагрузке тут не бывает: механизм слишком низкоуровневый, чтобы
его кто-то считал отдельно. Он просто есть везде.
Альтернативы и конкуренты
Логирование прямо в файл (библиотеки logging, log4j, winston).
Плюсы: полный контроль над форматом, ротацией, уровнями важности,
структурированный JSON из коробки.
Минусы: приложение начинает отвечать за инфраструктуру — ротацию, права,
переполнение диска; в контейнере файл вообще исчезает вместе с ним.
syslog (RFC 5424).
Плюсы: единая точка сбора для всей машины, уровни важности, отправка по сети,
десятилетия совместимости.
Минусы: нужен демон и настройка; UDP-вариант теряет сообщения молча; формат
сообщения жёстче, чем «произвольные байты».
Именованные каналы (FIFO) и Unix-сокеты.
Плюсы: живут отдельно от процесса, позволяют соединять программы,
запущенные независимо; сокеты умеют двусторонний обмен.
Минусы: требуют явного создания и уборки, сложнее обработка обрывов, нет
автоматического закрытия при завершении процесса.
Брокеры сообщений (Kafka, RabbitMQ, NATS).
Плюсы: доставка между машинами, сохранение истории, много читателей на один
поток, гарантии доставки.
Минусы: отдельная инфраструктура, которую надо поднимать, мониторить и
обновлять; несопоставимая сложность ради задачи «передать вывод дальше».
Когда НЕ стоит использовать
Когда нужен двусторонний диалог без тупика. Если процесс A пишет в B и
ждёт ответа, а B делает то же самое, оба могут заснуть в ожидании — классический
deadlock (взаимная блокировка) на буферах каналов. Тут нужны сокеты или
явный протокол с признаком конца сообщения.
Когда вывода очень много и он критичен. stderr не буферизуется: каждая
строка — системный вызов. Сто тысяч строк в секунду в stderr заметно тормозят
программу. Для нагруженного логирования нужен буферизованный писатель или
отдельный поток-логгер.
Когда данные бинарные и адресат — человек. Смешивать бинарный stdout с
текстовым stderr на одном терминале — верный способ испортить настройки
терминала управляющими последовательностями. И никогда не стоит писать в
потоки секреты: вывод скрипта почти наверняка окажется в логе CI, в journald
или в чьём-то чате.
Связанные понятия
- Файловый дескриптор — целое число, которым ядро обозначает открытый
канал внутри процесса. - Pipe (конвейер) — буфер в ядре, соединяющий выход одной программы со
входом другой. - Exit code (код возврата) — число 0–255, которым программа сообщает
вызвавшему, удалось ли дело. /dev/null— псевдоустройство, поглощающее всё записанное; способ
сказать «выброси это».tee— утилита, раздваивающая поток: и в файл, и дальше по конвейеру.- SIGPIPE — сигнал, который получает программа, если пишет в канал,
читателя у которого больше нет (отсюда «Broken pipe»). - Буферизация ввода-вывода — накопление байтов перед записью ради
скорости; главная причина «неправильного» порядка строк. - 12-factor app — свод из двенадцати правил для облачных приложений; XI
пункт требует обращаться с логами как с потоком событий.
Литература и источники
- Брайан Керниган, Роб Пайк. «The UNIX Programming Environment» (1984, en;
рус. «UNIX. Программное окружение») — до сих пор лучшее объяснение
философии конвейеров, написанное теми, кто был внутри Bell Labs. - У. Ричард Стивенс, Стивен Раго. «Advanced Programming in the UNIX
Environment» (3-е изд., 2013, en) — исчерпывающе про дескрипторы,dup2,
буферизацию stdio. Справочник на всю жизнь. - Dennis M. Ritchie. «The Evolution of the Unix Time-sharing System»
(1979/1984, en) — первоисточник об истории потоков и каналов; ищи по
названию, текст выложен на архивных страницах Bell Labs. - Спецификация POSIX, The Open Group Base Specifications —
https://pubs.opengroup.org/onlinepubs/9699919799/ — раздел проstdin,
stdout,stderrи перенаправление в оболочке. - Wikipedia: «Стандартные потоки» —
https://ru.wikipedia.org/wiki/Стандартные_потоки — короткая сводка с
таблицей дескрипторов. - The Twelve-Factor App, пункт XI «Logs» — https://12factor.net/logs —
две страницы, изменившие подход к логированию в индустрии. - Видеозапись интервью Дуга Макилроя об изобретении конвейеров — искать
в YouTube по запросу «Doug McIlroy pipes Unix oral history».
Где встретилось у меня
Вчера понятие всплыло в работе над самим генератором этого дайджеста.
В инструкции для генерации прямо прописано: если данных за день нет — сообщить
об этом в stderr и завершиться, не выдумывая тему. Плюс оркестратор запускается
по расписанию через launchd, где терминала нет вовсе, и оба потока приходится
явно направлять в файлы логов — иначе о неудачном запуске никто бы не узнал.
Классическая иллюстрация того, ради чего stderr пятьдесят лет назад и отделили
от stdout.
Краткое резюме
- Каждой программе система бесплатно выдаёт три канала: вход (0), выход
результата (1) и выход диагностики (2). Программа не знает и не должна
знать, что на другом конце. - Разделение stdout и stderr — не педантизм, а практика: результат идёт
дальше по конвейеру, жалобы идут человеку. Правило простое: если строка не
должна попасть в перенаправленный файл — это stderr. - Конвейеры работают параллельно и с обратным давлением, поэтому цепочка из
десяти утилит обрабатывает терабайт, не занимая память. - Буферизация — источник почти всех сюрпризов: stderr уходит сразу, stdout
копится блоками, и в общем логе порядок может нарушиться. - Механизму больше пятидесяти лет, и он остался фундаментом современного
облака: логи Docker и Kubernetes, шаги CI/CD и даже stdio-транспорт MCP —
это те же самые дескрипторы 1 и 2.