Использование именованных каналов с bash-проблемой потери данных

сделал поиск в интернете, нашел простые "учебники" для использования именованных каналов. Однако, когда я что-то делаю с фоновыми заданиями, я, кажется, теряю много данных.

[[Edit: найдено гораздо более простое решение, см. ответ на сообщение. Поэтому вопрос, который я выдвинул, теперь Академический-в случае, если вам может понадобиться сервер работы]]

использование Ubuntu 10.04 с Linux 2.6.32-25-generic #45-Ubuntu SMP Sat 16 октября 19:52: 42 UTC 2010 x86_64 GNU/Linux

GNU bash, версия 4.1.5 (1)-выпуск (x86_64-pc-linux-gnu).

моя функция bash:

function jqs
{
  pipe=/tmp/__job_control_manager__
  trap "rm -f $pipe; exit"  EXIT SIGKILL

  if [[ ! -p "$pipe" ]]; then
      mkfifo "$pipe"
  fi

  while true
  do
    if read txt <"$pipe"
    then
      echo "$(date +'%Y'): new text is [[$txt]]"

      if [[ "$txt" == 'quit' ]]
      then
    break
      fi
    fi
  done
}

я запускаю это в фоновом режиме:

> jqs&
[1] 5336

и теперь я кормлю его:

for i in 1 2 3 4 5 6 7 8
do
  (echo aaa$i > /tmp/__job_control_manager__ && echo success$i &)
done

вывод является противоречивым. Я часто не получаю Эхо успеха. Я получаю максимум столько новых текстовых Эхо, сколько Эхо успеха, иногда меньше.

если я удалю " & " из "ленты", это, кажется, работает, но я заблокирован, пока не будет прочитан вывод. Поэтому я хочу, чтобы подпроцессы получили заблокирован, но не основной процесс.

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

работа менеджер ниже:

function jq_manage
{
  export __gn__=""

  pipe=/tmp/__job_control_manager_"$__gn__"__
  trap "rm -f $pipe"    EXIT
  trap "break"      SIGKILL

  if [[ ! -p "$pipe" ]]; then
      mkfifo "$pipe"
  fi

  while true
  do
    date
    jobs
    if (($(jobs | egrep "Running.*echo '%#_Group_#%_$__gn__'" | wc -l) < $__jN__))
    then
      echo "Waiting for new job"
      if read new_job <"$pipe"
      then
    echo "new job is [[$new_job]]"

    if [[ "$new_job" == 'quit' ]]
    then
      break
    fi

    echo "In group $__gn__, starting job $new_job"
    eval "(echo '%#_Group_#%_$__gn__' > /dev/null; $new_job) &"
      fi
    else
      sleep 3
    fi
  done
}

function jq
{
  # __gn__ = first parameter to this function, the job group name (the pool within which to allocate __jN__ jobs)
  # __jN__ = second parameter to this function, the maximum of job numbers to run concurrently

  export __gn__=""
  shift
  export __jN__=""
  shift

  export __jq__=$(jobs | egrep "Running.*echo '%#_GroupQueue_#%_$__gn__'" | wc -l)
  if (($__jq__ '<' 1))
  then
    eval "(echo '%#_GroupQueue_#%_$__gn__' > /dev/null; jq_manage $__gn__) &"
  fi

  pipe=/tmp/__job_control_manager_"$__gn__"__

  echo $@ >$pipe
}

вызов

jq <name> <max processes> <command>
jq abc 2 sleep 20

начнет процесс. Эта часть отлично работает. Начни второй, хорошо. По одному кажется, работают хорошо. Но запуск 10 в цикле, похоже, теряет систему, как в простой пример выше.

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

С уважением, Алена.

6 ответов


ваша проблема if заявление ниже:

while true
do
    if read txt <"$pipe"
    ....
done

происходит то, что ваш сервер очереди заданий открывает и закрывает канал каждый раз вокруг цикла. Это означает, что некоторые клиенты получают ошибку "сломанной трубы", когда они пытаются написать в трубу - то есть читатель трубы уходит после того, как писатель открывает его.

чтобы исправить это, измените цикл на сервере, откройте канал один раз для всего цикла:

while true
do
    if read txt
    ....
done < "$pipe"

готово таким образом, труба открывается один раз и остается открытой.

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

Edit: теперь проблема в том, что вы получаете EOF при чтении, когда последний клиент закрывает канал, вы можете используйте метод jilles для обмана дескрипторов файлов, или вы можете просто убедиться, что Вы тоже являетесь клиентом и держите сторону записи открытой:

while true
do
    if read txt
    ....
done < "$pipe" 3> "$pipe"

это будет держать сторону записи трубы открытой на fd 3. Такое же предостережение применяется к этому файловому дескриптору, как и к stdin. Вам нужно будет закрыть его, чтобы любые дочерние процессы не унаследовали его. Это, вероятно, имеет меньшее значение, чем с stdin, но это было бы чище.


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

однако, как только все писатели ушли после того, как fifo был открыт (так что был писатель), читает возвращение немедленно (и poll() возвращает POLLHUP). Единственный способ очистить это состояние-снова открыть fifo.

POSIX не предоставляет решения для этого, но, по крайней мере, Linux и FreeBSD делают: если чтение начинается с сбоя, снова откройте fifo, сохраняя исходный дескриптор открыть. Это работает, потому что в Linux и FreeBSD состояние "зависания" локально для конкретного описания открытого файла, в то время как в POSIX оно глобально для fifo.

Это можно сделать скрипт вроде этого:

while :; do
    exec 3<tmp/testfifo
    exec 4<&-
    while read x; do
        echo "input: $x"
    done <&3
    exec 4<&3
    exec 3<&-
done

как camh & Деннис Уильямсон говорят, Не ломайте трубу.

Теперь у меня есть меньшие примеры, прямо в командной строке:

сервер:

(
  for i in {0,1,2,3,4}{0,1,2,3,4,5,6,7,8,9};
  do
    if read s;
      then echo ">>$i--$s//";
    else
      echo "<<$i";
    fi;
  done < tst-fifo
)&

клиент:

(
  for i in {%a,#b}{1,2}{0,1};
  do
    echo "Test-$i" > tst-fifo;
  done
)&

может заменить ключевую строку на:

    (echo "Test-$i" > tst-fifo&);

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

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

любой способ остановить это?

Спасибо за любые идеи еще раз.


только для тех, кто может быть заинтересован, [[повторно отредактировано]] после комментариев camh и jilles, вот две новые версии сценария тестового сервера.

обе версии теперь работают точно так, как надеялись.

версия camh для управления трубой:

function jqs    # Job queue manager
{
  pipe=/tmp/__job_control_manager__
  trap "rm -f $pipe; exit"  EXIT TERM

  if [[ ! -p "$pipe" ]]; then
      mkfifo "$pipe"
  fi

  while true
  do
    if read -u 3 txt
    then
      echo "$(date +'%Y'): new text is [[$txt]]"

      if [[ "$txt" == 'quit' ]]
      then
    break
      else
        sleep 1
        # process $txt - remember that if this is to be a spawned job, we should close fd 3 and 4 beforehand
      fi
    fi
  done 3< "$pipe" 4> "$pipe"    # 4 is just to keep the pipe opened so any real client does not end up causing read to return EOF
}

версия jille для управления трубами:

function jqs    # Job queue manager
{
  pipe=/tmp/__job_control_manager__
  trap "rm -f $pipe; exit"  EXIT TERM

  if [[ ! -p "$pipe" ]]; then
      mkfifo "$pipe"
  fi

  exec 3< "$pipe"
  exec 4<&-

  while true
  do
    if read -u 3 txt
    then
      echo "$(date +'%Y'): new text is [[$txt]]"

      if [[ "$txt" == 'quit' ]]
      then
    break
      else
        sleep 1
        # process $txt - remember that if this is to be a spawned job, we should close fd 3 and 4 beforehand
      fi
    else
      # Close the pipe and reconnect it so that the next read does not end up returning EOF
      exec 4<&3
      exec 3<&-
      exec 3< "$pipe"
      exec 4<&-
    fi
  done
}

спасибо всем за помощь.


С одной стороны, проблема хуже, чем я думал: Теперь, кажется, есть случай в моем более сложном примере (jq_manage), когда одни и те же данные читаются снова и снова из канала (даже если в него не записываются новые данные).

С другой стороны, я нашел простое решение (редактировать следующий комментарий Денниса):

function jqn    # compute the number of jobs running in that group
{
  __jqty__=$(jobs | egrep "Running.*echo '%#_Group_#%_$__groupn__'" | wc -l)
}

function jq
{
  __groupn__="";  shift   # job group name (the pool within which to allocate $__jmax__ jobs)
  __jmax__="";    shift   # maximum of job numbers to run concurrently

  jqn
  while (($__jqty__ '>=' $__jmax__))
  do
    sleep 1
    jqn
  done

  eval "(echo '%#_Group_#%_$__groupn__' > /dev/null; $@) &"
}

работает как шарм. Ни розетки, ни трубы. Простой.


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

вы можете сделать это с помощью GNU Parallel. Вам не понадобится этот сценарий.

http://www.gnu.org/software/parallel/man.html#options

вы можете установить max-procs " количество рабочих мест. Запуск до N заданий параллельно."Есть вариант установить количество ядер процессора, которые вы хотите использовать. Вы можете сохранить список выполненных заданий в файл журнала, но это бета-функция.