Learning Platform
Глоссарий Troubleshooting
Урок 10.01 · 22 мин
Начальный
File descriptorsstdinstdoutstderrRedirection/dev/null

Каждый процесс имеет три стандартных потока

Когда Linux kernel запускает процесс (через fork() + execve()), он автоматически открывает три file descriptors (FD):

  • FD 0: stdin — куда процесс читает входные данные (default: клавиатура)
  • FD 1: stdout — куда процесс пишет нормальный вывод (default: терминал)
  • FD 2: stderr — куда процесс пишет ошибки и diagnostic-сообщения (default: терминал)

В C это STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO (<unistd.h>). В Python — sys.stdin, sys.stdout, sys.stderr. В bash просто <, >, 2>.

Три FD каждого процесса

Это hardcoded стандарт POSIX — работает для всех процессов одинаково.

FD 0: stdinвходной потокПрограмма читает отсюда через read(0, ...) или scanf/input(). По умолчанию подключён к терминалу — ты вводишь с клавиатуры.
FD 1: stdoutнормальный выводПрограмма пишет через write(1, ...) или printf/print(). По умолчанию идёт на терминал.
FD 2: stderrошибки и диагностикаОтдельный канал ДЛЯ ОШИБОК. Это намеренная separation: даже если stdout перенаправлен в файл, ошибки видны на экране. Программы пишут туда через fprintf(stderr, ...) или sys.stderr.write().

Почему отдельные stdout и stderr? Чтобы можно было:

  • Перенаправить только результат (stdout) в файл, оставив ошибки на экране
  • Игнорировать ошибки (2>/dev/null), не теряя нормальный вывод
  • Логировать данные и errors раздельно

Базовые перенаправления

$ command > output.txt          # stdout -> output.txt (перезаписывает)
$ command >> output.txt         # stdout -> output.txt (append)
$ command 2> errors.txt         # stderr -> errors.txt
$ command < input.txt           # stdin ← input.txt
$ command < input > output      # перенаправить и stdin и stdout

Простой пример с ls:

$ ls /tmp /nonexistent
ls: cannot access '/nonexistent': No such file or directory
/tmp:
file1.txt
file2.txt

# stdout перенаправлен в файл, stderr на экран:
$ ls /tmp /nonexistent > out.txt
ls: cannot access '/nonexistent': No such file or directory

$ cat out.txt
/tmp:
file1.txt
file2.txt

# Сейчас наоборот: stderr в файл, stdout на экран:
$ ls /tmp /nonexistent 2> err.txt
/tmp:
file1.txt
file2.txt

$ cat err.txt
ls: cannot access '/nonexistent': No such file or directory

2>&1: объединить stderr со stdout

Самая используемая конструкция перенаправлений:

$ command > all.log 2>&1
# stdout идёт в all.log
# stderr идёт туда же (2>&1 = "stderr направить туда, куда сейчас идёт stdout=FD1")

Бypass shell-нотация: &1 означает “то место, куда указывает FD1 в данный момент”. Если FD1 указывает на all.log — stderr тоже пойдёт туда.

Order matters!

# [x] Оба в файл:
$ command > all.log 2>&1
# Шаги: (1) stdout -> all.log; (2) stderr -> копия stdout = all.log

# [X] Часто ожидают того же, но получают неожиданное:
$ command 2>&1 > all.log
# Шаги: (1) stderr -> копия stdout (которое сейчас = терминал!);
#       (2) stdout -> all.log.
# Итог: stderr НА ТЕРМИНАЛЕ, stdout в файле.

Это самый частый bug при работе с перенаправлениями. Запомни: сначала перенаправь stdout, потом объединяй stderr через 2>&1.

bash-only short: &>file

В bash есть синтаксический сахар:

$ command &> all.log
# Эквивалент: command > all.log 2>&1
# stdout И stderr в all.log

$ command &>> all.log
# То же, но append

Это bash-specific (не POSIX). На sh/dash//bin/sh (Debian default для скриптов) не работает. В CI используй > file 2>&1 для portable POSIX.

/dev/null: чёрная дыра

/dev/null — это special character device. Всё, что в него пишется — отбрасывается (/dev/null принимает любые писания и сразу их забывает). Читать из него = пустой поток (EOF immediately).

# Подавить stdout:
$ command > /dev/null

# Подавить stderr:
$ command 2> /dev/null

# Подавить обе:
$ command > /dev/null 2>&1
$ command &> /dev/null      # bash short

DE-сценарии:

# Запустить проверку, нас интересует только exit code:
$ if grep -q ERROR app.log 2>/dev/null; then
    echo "Errors found"
fi

# find с подавлением "Permission denied":
$ find / -name "*.cfg" 2>/dev/null

# Запустить cron-job без письма по email (без output вообще):
$ */5 * * * * /opt/etl/run.sh > /dev/null 2>&1

Cron отправляет email на каждый non-empty stdout/stderr. > /dev/null 2>&1 — стандартная фраза в crontabs для silent jobs.

/dev/stdin, /dev/stdout, /dev/stderr

Это виртуальные файлы, представляющие потоки. Полезны когда программа принимает «файл», а у тебя данные на stdin:

# Команда принимает file argument, но мы хотим из pipe:
$ echo "hello" | some_command /dev/stdin

# Или дублировать вывод:
$ command | tee /dev/stderr | another_command
# tee пишет в /dev/stderr (= терминал) и в stdout (для следующего process)

Дублирование FD через cp

# Прочесть из stdin как из файла:
$ md5sum /dev/stdin < input.txt
# md5sum обычно принимает имя файла; здесь читает из /dev/stdin (=input.txt после redirect)

Программирование: куда пишет программа

В Python:

import sys
print("normal output", file=sys.stdout)      # -> stdout
print("error message", file=sys.stderr)       # -> stderr
data = sys.stdin.read()                       # ← stdin

В bash-скрипте:

echo "normal output"                          # -> stdout
echo "error message" >&2                      # -> stderr (perlomane >&2 редирект)
read -r line                                  # ← stdin

Канон: диагностику пишет в stderr, результат в stdout. Это позволяет pipeline’у работать только с результатом:

# Скрипт логирует на stderr, выдаёт CSV на stdout:
$ ./extract.sh | jq '.users[]'
# jq получает только CSV (stdout); stderr-логи всё ещё видны на экране

Exit codes и pipes

Exit code последнего процесса в pipeline — это exit code всего pipeline (по умолчанию):

$ false | true
$ echo $?
0
# Хотя false вернул 1, true в конце вернул 0 -> весь pipeline 0

Это часто скрывает ошибки. В bash есть pipefail для исправления:

$ set -o pipefail
$ false | true
$ echo $?
1
# Теперь ненулевой exit code ЛЮБОЙ команды pipeline становится exit-code всего

В production-скриптах всегда set -euo pipefail — про это будет в модуле 18.

Под капотом: что делает shell

open/read/write: системные вызовы файлового I/O Pipes: механика анонимных каналов в ядре

Когда ты пишешь command > file.txt, shell:

  1. Делает fork() — создаёт дочерний процесс
  2. В дочернем процессе ДО execve(command):
    • open("file.txt", O_WRONLY|O_CREAT|O_TRUNC) — получает FD, скажем 3
    • dup2(3, 1) — копирует FD 3 в FD 1 (stdout теперь указывает на файл)
    • close(3) — закрывает оригинальный FD 3
  3. Делает execve(command) — программа стартует, у неё stdout уже на файл

Это происходит до запуска программы — программа не знает и не должна знать, что её stdout перенаправлен. Это магия POSIX-абстракции FD.

Для command 2>&1:

  1. fork()
  2. dup2(1, 2) — копировать FD 1 в FD 2 (stderr теперь как stdout)
  3. execve(...)

2>&1 буквально означает «dup2(1, 2)» в shell-нотации. & — это «FD-reference», не bash-background.

DE-сценарии

1. Лог-файл всех данных + ошибок

$ ./etl_job.sh > /var/log/etl/$(date +%Y%m%d).log 2>&1

Всё (stdout + stderr) идёт в один лог. Простое решение для не-критичных jobs.

2. Раздельные логи

$ ./etl_job.sh > /var/log/etl/output.log 2> /var/log/etl/errors.log

Раздельно — позволяет дифференцировать data и errors. Полезно для monitoring (если errors.log не пуст — alert).

3. Cron job с email-on-error

# В crontab — silent if successful, email on error:
0 2 * * * /opt/etl/run.sh > /var/log/etl/$(date +\%Y\%m\%d).log
# stdout идёт в файл, stderr остаётся для cron -> email если стnonempty

Cron отправит email только если stderr не пуст (то есть была ошибка).

4. Дебаг pipeline на чём-то типа strace

$ strace -e trace=openat -o syscalls.log -f ./script.sh
# stdout/stderr скрипта — обычным образом
# strace пишет syscalls в syscalls.log

-o file — strace-specific опция, перенаправляет own diagnostic вывод strace в файл.

Попробуй сам

  1. Раздельные стдин/стдаут/стдерр:
    ls /tmp /nonexistent > out.txt 2> err.txt
    cat out.txt
    cat err.txt
  2. Все в один файл:
    ls /tmp /nonexistent > all.log 2>&1
    cat all.log
  3. Подавить только stderr:
    ls /nonexistent 2>/dev/null
  4. Skript с echo to stderr:
    bash -c 'echo "OK" >&1; echo "BAD" >&2'
    bash -c 'echo "OK" >&1; echo "BAD" >&2' > out.txt 2> err.txt
    cat out.txt
    cat err.txt
  5. Подсчитай только stdout:
    ls /tmp /nonexistent 2>/dev/null | wc -l

macOS-различия

  • /dev/null, /dev/stdin, /dev/stdout, /dev/stderr — есть и на macOS.
  • &> (bash-only) — работает на macOS если shell это bash. На zsh (default на macOS 10.15+) &> тоже работает.
  • Базовые >, >>, 2>, 2>&1 — POSIX, работают везде.
  • pipefailset -o pipefail работает на bash 3.2+ (включая macOS default).
Проверка знанийKnowledge check
Объясни почему 'command 2>&1 > log.txt' и 'command > log.txt 2>&1' дают разный результат, и какой из них корректен для 'записать всё в log.txt'.
ОтветAnswer
Правильный — 'command > log.txt 2>&1'. Разница в порядке выполнения перенаправлений (left-to-right). Команда 'command > log.txt 2>&1': (1) Shell делает open('log.txt')->FD; dup2(FD, 1) — FD1 (stdout) теперь указывает на log.txt. (2) Затем '2>&1' = dup2(1, 2) — FD2 (stderr) копируется на ту же позицию что FD1, то есть тоже на log.txt. Результат: оба пишут в log.txt. Команда 'command 2>&1 > log.txt': (1) Сначала '2>&1' = dup2(1, 2). В момент выполнения этого FD1 указывает на ТЕРМИНАЛ (не успел перенаправиться). FD2 копируется на терминал. (2) Затем '> log.txt' = dup2(FD_log, 1). FD1 теперь на log.txt, но FD2 остался на терминале. Результат: stdout в log.txt, stderr на терминале. Bash интерпретирует перенаправления слева направо, и каждая операция dup2 'фиксирует' текущее значение target FD. Мнемоника: '> file 2>&1' = 'stdout сначала, потом ссылку для stderr'. Bash-shortcut '&>' гарантированно direct оба в файл — но это не POSIX, в /bin/sh не работает. Для portable scripts всегда '> file 2>&1' в правильном порядке.

Главное

  • 3 стандартных FD: 0 (stdin), 1 (stdout), 2 (stderr) — открываются для каждого процесса.
  • > file — stdout, 2> file — stderr, >> file — append, < file — stdin.
  • > file 2>&1 — оба в файл. Порядок важен! 2>&1 > file — НЕ то же самое.
  • /dev/null — отбрасывает всё. > /dev/null 2>&1 — silent (без вывода).
  • /dev/stdin, /dev/stdout, /dev/stderr — виртуальные файлы для FD.
  • bash-short &>file = > file 2>&1, но не POSIX.
  • Программа пишет результат в stdout, диагностику в stderr — это convention для clean pipelines.
  • set -o pipefail для правильных exit codes в pipeline (модуль 18).

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какие три file descriptors открываются для каждого процесса автоматически и какие у них номера?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5