Эхо атомарно при написании отдельных строк - PullRequest
16 голосов
/ 29 марта 2012

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

Основная проблема в том, что программа, которая производит вывод, запускается в фоновом режиме, поэтому моя функция, выполняющая чтение, может одновременно записывать в файл журнала. Может ли это быть проблемой? Эхо всегда пишет только одну строку, поэтому не должно быть сложно обеспечить атомарность. Однако я заглянул в Google и не нашел способа убедиться, что он действительно атомарный.

Вот текущий скрипт:

LOG_FILE=/path/to/logfile

write_log() {
  echo "$(date +%Y%m%d%H%M%S);$1" >> ${LOG_FILE}
}

write_output() {
  while read data; do
    write_log "Message from SUB process: [ $data ]"
  done
}

write_log "Script started"
# do some stuff
call_complicated_program 2>&1 | write_output &
SUB_PID=$!
#do some more stuff
write_log "Script exiting"
wait $SUB_PID

Как видите, скрипт может писать как самостоятельно, так и из-за перенаправленного вывода. Может ли это вызвать havok в файле?

Ответы [ 3 ]

28 голосов
/ 29 марта 2012

echo просто обертка вокруг write (это упрощение; подробности смотрите ниже в редактировании), поэтому, чтобы определить, является ли echo атомарным, полезно поискать в записи. Из одной спецификации UNIX :

Атомное / неатомарное: запись является атомарной, если все количество, записанное в одной операции, не чередуется с данными из любого другого процесса. Это полезно, когда несколько писателей отправляют данные одному читателю. Приложения должны знать, насколько большой запрос записи может быть выполнен атомарно. Этот максимум называется {PIPE_BUF}. В этом объеме IEEE Std 1003.1-2001 не говорится, являются ли запросы записи для более чем {PIPE_BUF} байтов атомарными, но требуется, чтобы записи {PIPE_BUF} или меньше байтов были атомарными.

Вы можете проверить PIPE_BUF в своей системе с помощью простой программы на Си. Если вы просто печатаете одну строку вывода, это не до смешного длинного, оно должно быть атомарным.

Вот простая программа для проверки значения PIPE_BUF:

#include <limits.h>
#include <stdio.h>

int main(void) {
  printf("%d\n", PIPE_BUF);

  return 0;
}

В Mac OS X это дает мне 512 (минимальное допустимое значение для PIPE_BUF). В Linux я получаю 4096. Поэтому, если ваши строки довольно длинные, убедитесь, что вы проверили их в рассматриваемой системе.

изменить, чтобы добавить : Я решил проверить реализацию echo в Bash, чтобы убедиться, что он будет печататься атомарно. Оказывается, echo использует putchar или printf в зависимости от того, используете ли вы опцию -e. Это буферизованные операции stdio, что означает, что они заполняют буфер и фактически записывают его только при достижении новой строки (в режиме буферизации строки), заполнении буфера (в режиме буферизации блока) или при явном сбросе выход с fflush. По умолчанию поток будет находиться в режиме буферизации строки, если это интерактивный терминал, и блокировать режим буферизации, если это любой другой файл. Bash никогда не устанавливает тип буферизации, поэтому для вашего файла журнала по умолчанию он должен блокировать режим буферизации. Затем конец встроенного echo 1035 *, Bash вызывает fflush для очистки выходного потока. Таким образом, вывод всегда будет сброшен в конце echo, но может быть сброшен раньше, если он не помещается в буфер.

Размер используемого буфера может быть BUFSIZ, хотя он может быть другим; BUFSIZ - это размер по умолчанию, если вы устанавливаете буфер явно, используя setbuf, но нет никакого портативного способа определить фактический размер вашего буфера. Также нет никаких портативных рекомендаций для того, что такое BUFSIZ, но когда я тестировал его на Mac OS X и Linux, он был в два раза больше PIPE_BUF.

Что все это значит? Поскольку все выходные данные echo буферизуются, на самом деле они не будут вызывать write до тех пор, пока не будет заполнен буфер или не будет вызван fflush. В этот момент вывод должен быть записан, и должна применяться гарантия атомарности, о которой я упоминал выше. Если размер буфера стандартного вывода больше PIPE_BUF, то PIPE_BUF будет наименьшей атомарной единицей, которую можно записать. Если PIPE_BUF больше, чем размер буфера стандартного вывода, тогда поток запишет буфер, когда буфер заполнится.

Таким образом, echo гарантирует только атомарную запись последовательностей, которые короче, чем меньший из PIPE_BUF и размер буфера stdout, который, скорее всего, BUFSIZ. В большинстве систем BUFSIZ больше PIPE_BUF.

tl; dr : echo будет атомарно выводить строки, если эти строки достаточно короткие. В современных системах вы, вероятно, безопасны до 512 байт, но невозможно определить предел переносимым образом.

0 голосов
/ 19 февраля 2019

Я попробовал подход от пользователя: пицца и не смог заставить его работать, как ответ от пользователя: Брайан Кэмпбелл . Дайте мне знать, если я делаю что-то работу, и я обновлю ответ. (И да, это ответ, потому что я фактически даю полную рабочую демонстрацию.)

базовый параллелизм

Это просто иллюстрирует проблему

$ for n in {1..5}; do (curl -svo /dev/null example.com 2>&1 &) done | grep GET
> GET / HTTP/1.1
>>  GET / HTTP/1.1
GET / HTTP/1.1
>>>  GET / HTTP/1.1
>>GET / HTTP/1.1

с использованием эха в каждой строке вывода

Это решает проблему, используя метод Брайана Кэмпбелла . (Обратите внимание, что длина линии, для которой это работает, ограничена.)

$ for n in {1..5}; do (curl -svo /dev/null example.com 2>&1 | while read; do echo "${REPLY}"; done &) done | grep GET
> GET / HTTP/1.1
> GET / HTTP/1.1
> GET / HTTP/1.1
> GET / HTTP/1.1
> GET / HTTP/1.1

перенаправление цикла for для добавления в стандартный вывод

Инстинкт должен сказать вам, что это не сработает, потому что он перенаправляет после того, как все выходные данные разветвленных завитков были объединены.

$ for n in {1..5}; do (curl -svo /dev/null example.com 2>&1 &) done >> /dev/stdout | grep GET
> GET / HTTP/1.1
> GET / HTTP/1.1
>> >GET / HTTP/1.1
 >  GET / HTTP/1.1
 GET / HTTP/1.1

перенаправление каждого локона для добавления в стандартный вывод

Я подозреваю, что этот сбой связан с тем, что весь контент каждого локона перенаправляется и его размер больше, чем то, что ядро ​​готово заблокировать. Я не нашел времени, чтобы подтвердить это, но то, что поделился Брайан Кэмпбелл, похоже, подтверждает это.

$ for n in {1..5}; do (curl -svo /dev/null example.com >>/dev/stdout 2>&1 &) done | grep GET
>>  GET / HTTP/1.1
GET / HTTP/1.1
> GET / HTTP/1.1
GET / HTTP/1.1
> GET / HTTP/1.1
0 голосов
/ 30 марта 2012

Нет принудительной блокировки файлов, но оператор >> безопасен, оператор> небезопасен. Таким образом, ваша практика безопасна.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...