труба, {список; } работает только с некоторыми программами

13

Нужны объяснения от опытных пользователей для такого непредсказуемого поведения:

ps -eF | { head -n 1;grep worker; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD
root       441     2  0     0     0   2 paź15 ?       00:00:00 [kworker/2:1H]

все выглядит хорошо, тогда как

ls -la / | { head -n 1;grep sbin; }

отображает только вывод из head

... Я думал stdout 2>&1и не работает ни для меня, это странно, какие-либо объяснения или предложить, как справиться с этим?

аст
источник
1
Последний должен распечатать все. То headи grepтам ничего не делать.
Иордания
да ты прав. Но вместо этого почему ps -eF работает, а ls -la / not?
аст

Ответы:

9

Я провел некоторые исследования с использованием, straceи, похоже, это связано с тем, как программа в левой части конвейера выполняет запись в терминал. Когда lsкоманда выполняется, она записывает все данные в один write(). Это приводит headк потреблению всего стандартного ввода.

С другой стороны ps, данные записываются в пакетном режиме, поэтому только первое из write()них используется head, а затем оно существует. Позже звонки write()перейдут к недавно порожденному grepпроцессу.

Это означает, что он не будет работать, если процесс, к которому вы пытаетесь grepподключиться, не будет выполнен в первую очередь write(), поскольку grepон не может увидеть все данные (он видит даже меньше, чем просто данные без первой строки).

Вот пример попытки выполнить поиск pid 1 в моей системе:

$ ps -eF | { head -n2; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD
root         1     0  0  1697  3768   2 Oct03 ?        00:00:03 /lib/systemd/systemd
$ ps -eF | grep '/lib/systemd/systemd$'
root         1     0  0  1697  3768   2 Oct03 ?        00:00:03 /lib/systemd/systemd
$ ps -eF | { head -n1; grep '/lib/systemd/systemd$'; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD

Ваш ps -eFпример работает только случайно.

jordanm
источник
большой и всеобъемлющей expalnation спасибо
аст
1
На самом деле это скорее состояние гонки. Просто медленнее выполнять несколько write()звонков. Если бы он headмедленно выполнял свой read()вызов (так, чтобы в буфере канала были все данные), он бы демонстрировал одинаковое поведение для обоих lsи ps.
Патрик
6

Это вызвано буферизацией в glibc. В случае lsвывода выводится в один внутренний буфер и как таковой передается просто в head. Для ps -eF, выход больше, и, как только он headзаканчивается, следующий grepполучает оставшиеся части (но не весь) вывод ps.

Вы можете избавиться от него, сняв буферизацию канала - например, с помощью sed -u(я не уверен, что это не расширение GNU):

$ ls -al / | sed -u "#" | { head -n 1; grep bin; }
total 76
drwxr-xr-x   2 root root  4096 Oct  2 21:52 bin
drwxr-xr-x   2 root root  8192 Oct  3 01:54 sbin
peterph
источник
4

Происходит то, что head -n 1читает больше 1 строки. Для оптимальной пропускной способности head считывает куски байтов, поэтому он может читать по 1024 байта за раз, а затем просматривать эти байты на предмет разрыва первой строки. Поскольку разрыв строки может произойти в середине этих 1024 байтов, остальные данные будут потеряны. Его нельзя вернуть на трубу. Таким образом, следующий процесс, который выполняется только получает байты 1025 и далее.

Ваша первая команда выполнена успешно, потому что kworkerпроцесс идет после того первого чанка, который headчитает.

Для того, чтобы это работало, headпридется читать по 1 символу за раз. Но это очень медленно, так что нет.
Единственный способ сделать что-то подобное эффективно - сделать так, чтобы один и тот же процесс выполнял и "head", и "grep".

Вот 2 способа сделать это:

echo -e '1\n2\n3\n4\n5' | perl -ne 'print if $i++ == 0 || /4/'

или

echo -e '1\n2\n3\n4\n5' | awk '{if (NR == 1 || /4/) print }'

Есть намного больше ...

Патрик
источник
да, я знал «способ awk», чтобы справиться с этой задачей, но мне было интересно, почему поведение было настолько непредсказуемым с {list; }. Спасибо за разъяснение, как это работает. Я впечатлен всем выше ответы
аст
2

Если вам нужна только первая или две строки, уловка следующего типа работает и позволяет избежать проблем с буферизацией, вызванных использованием двух разных команд для чтения выходного потока:

$ ps -eF   | { IFS= read -r x ; echo "$x" ; grep worker; }
$ ls -la / | { IFS= read -r x ; echo "$x" ; grep sbin; }

Он readвстроен в оболочку и не использует весь буфер ввода только для вывода одной строки, поэтому использование readоставляет все остальные выходные данные для следующей команды.

Если вы хотите подчеркнуть проблемы буферизации, показанные в ваших примерах, в которых используются две разные команды, добавьте sleepк ним a, чтобы устранить проблемы с синхронизацией, и разрешите команде слева сгенерировать все свои выходные данные, прежде чем команды справа попытаются прочитать любую из Это:

$ ps -eF   | { sleep 5 ; head -n 1 ; grep worker; }
$ ls -la / | { sleep 5 ; head -n 1 ; grep sbin; }

Теперь оба приведенных выше примера завершаются сбоем одинаково - headчтение всего буфера вывода производится только для создания одной строки, и этот буфер недоступен для следующего grep.

Вы можете увидеть проблему буферизации еще яснее, используя несколько примеров, которые нумеруют выходные строки, чтобы вы могли определить, какие строки отсутствуют:

$ ps -eF          | cat -n | { sleep 5 ; head -n 1 ; head ; }
$ ls -la /usr/bin | cat -n | { sleep 5 ; head -n 1 ; head ; }

Простой способ увидеть проблему буферизации - использовать seqсписок, который генерирует список. Мы можем легко определить, какие цифры пропали без вести:

$ seq 1 100000    | { sleep 5 ; head -n 1 ; head ; }
1

1861
1862
1863
1864
1865
1866
1867
1868
1869

Мое хитрое решение, использующее оболочку для чтения и вывода первой строки, работает правильно даже с добавленной задержкой сна:

$ seq 1 100000 | { sleep 5 ; IFS= read -r x ; echo "$x" ; head ; }
1
2
3
4
5
6
7
8
9
10
11

Ниже приведен полный пример, показывающий headпроблемы с буферизацией, показывающий, как headрасходуется весь буфер вывода только для того, чтобы каждый раз создавать его пять строк. Этот использованный буфер недоступен для следующей headкоманды в последовательности:

$ seq 1 100000 | { sleep 5 ; head -5 ; head -5 ; head -5 ; head -5 ; }
1
2
3
4
5

1861
1862
1863
1864
499
3500
3501
3502
3503
7
5138
5139
5140
5141

Глядя на число 1861выше, мы можем вычислить размер используемого буфера, headпосчитав seqвыходные данные от 1до 1860:

$ seq 1 1860 | wc -c
8193

Мы видим, что headбуферизация происходит за счет считывания целых 8 КБ (8 * 1024 байт) выходного потока канала за раз, даже для получения только нескольких строк собственного вывода.

Ян Д. Аллен
источник