使用带有bash的命名管道 - 数据丢失问题

时间:2010-11-27 08:08:26

标签: linux bash named-pipes data-loss

在网上进行了一些搜索,找到了使用命名管道的简单“教程”。但是当我对后台工作做任何事情时,我似乎失去了很多数据。

[[编辑:找到一个更简单的解决方案,请参阅回帖。所以我提出的问题现在是学术性的 - 如果有人想要一个工作服务器]]

使用Ubuntu 10.04和Linux 2.6.32-25-generic#45-Ubuntu SMP Sat Oct 16 19:52:42 UTC 2010 x86_64 GNU / Linux

GNU bash,版本4.1.5(1)-release(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

输出不一致。 我经常没有得到所有成功的回声。 我获得最多的新文本回声与成功相呼应,有时更少。

如果我删除'&amp;'从'feed'看,它似乎有效,但我被阻塞,直到输出被读取。因此,我想让子流程被阻止,而不是主流程。

目的是编写一个简单的作业控制脚本,这样我最多可以并行运行10个作业并将其余部分排队等待以后处理,但可靠地知道它们确实运行了。

下面的完整职位经理:

function jq_manage
{
  export __gn__="$1"

  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__="$1"
  shift
  export __jN__="$1"
  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 个答案:

答案 0 :(得分:26)

您的问题是if声明如下:

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

正在发生的事情是您的作业队列服务器每次在循环周围打开和关闭管道。这意味着一些客户端在尝试写入管道时出现“管道损坏”错误 - 也就是说,管道读取器在编写器打开后就会消失。

要解决此问题,请更改服务器中的循环,为整个循环打开管道一次:

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

通过这种方式,管道打开一次并保持打开状态。

您需要注意在循环中运行的内容,因为循环内的所有处理都会将stdin附加到命名管道。您需要确保从其他位置重定向循环内的所有进程的stdin,否则它们可能会使用管道中的数据。

编辑:现在的问题是,当最后一个客户端关闭管道时,您正在读取EOF,您可以使用jilles方法复制文件描述符,或者您也可以确保您也是客户端并保持管道的写入侧打开:

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

这将使管道的写入侧在fd 3上保持打开。对于此文件描述符,与stdin一样适用。您将需要关闭它,以便任何子进程不继承它。它可能比stdin更重要,但它会更清晰。

答案 1 :(得分:6)

正如其他答案中所述,您需要始终保持fifo打开以避免丢失数据。

然而,一旦所有作者在fifo打开后离开(所以有一个作家),读取立即返回(poll()返回POLLHUP)。清除此状态的唯一方法是重新打开fifo。

POSIX没有为此提供解决方案,但至少Linux和FreeBSD会这样做:如果读取开始失败,请在保持原始描述符打开的同时再次打开fifo。这是有效的,因为在Linux和FreeBSD中,“hangup”状态是特定打开文件描述的本地状态,而在POSIX中,它是fifo的全局状态。

这可以在这样的shell脚本中完成:

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

答案 2 :(得分:1)

像camh&amp;丹尼斯威廉姆森说不要破坏管道。

现在我有更小的例子,直接在命令行上:

服务器:

(
  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&);

发送到管道的所有客户端数据都会被读取,但是客户端的选项二可能需要在读取所有数据之前启动服务器几次。

但是虽然read等待管道中的数据开始,但是一旦推送了数据,它就会永远读取空字符串。

任何阻止这种情况的方法?

再次感谢您的任何见解。

答案 3 :(得分:1)

对于那些可能感兴趣的人,[[重新编辑]]在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
}

感谢大家的帮助。

答案 4 :(得分:0)

一方面问题比我想象的还要糟糕: 现在似乎在我的更复杂的例子(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__="$1";  shift   # job group name (the pool within which to allocate $__jmax__ jobs)
  __jmax__="$1";    shift   # maximum of job numbers to run concurrently

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

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

像魅力一样工作。 没有插座或管道。 简单。

答案 5 :(得分:0)

  

最多并行运行10个作业并将其余作业排队等待以后处理,但可靠地知道它们确实运行

您可以使用GNU Parallel执行此操作。你不需要这个脚本。

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

您可以设置max-procs“作业点数。并行运行最多N个作业。”可以选择设置要使用的CPU核心数。您可以将已执行作业列表保存到日志文件中,但这是测试版功能。