【问题标题】:Redirecting STDOUT in child process在子进程中重定向 STDOUT
【发布时间】:2012-11-01 05:52:00
【问题描述】:

有一个通过 fork 产生多个子进程的父进程。我希望父进程和子进程的日志文件是分开的。问题是子进程 STDOUT 被重定向到父日志文件以及子日志文件。不确定我需要更改什么以避免子进程日志消息进入父日志文件。我也不明白在下面的 setEnvironment 函数中创建 OUT 和 ERR 文件句柄的目的。这是一个现有的代码,所以我保持原样。在父进程和子进程中,我将变量 $g_LOGFILE 设置为包含不同的文件名,以便创建单独的日志文件。我也在父进程和子进程中调用 setEnvironment 函数。我尝试在子进程中关闭 STDOUT、STDERR、STDIN 并调用 setenvironment,但它无法正常工作。

sub setEnvironment()
{   

  unless ( open(OUT, ">&STDOUT") )
   {
          print "Cannot redirect STDOUT";
          return 2;
    }
    unless ( open(ERR, ">&STDERR") )
    {
          print "Cannot redirect STDERR";
          return 2;
    }


  unless ( open(STDOUT, "|tee -ai $g_LOGPATH/$g_LOGFILE") )
  {
          print "Cannot open log file $g_LOGPATH/$g_LOGFILE");
          return 2;
   }
   unless ( open(STDERR, ">&STDOUT") )
   {
                print  "Cannot redirect STDERR");
                return 2 ;
    }
    STDOUT->autoflush(1);

} 


####################### Main Program ######################################

    $g_LOGFILE="parent.log";

  while ($file = readdir(DIR))
 {  
     my $pid = fork;
     if ( $pid ) {

        setEnvironment();
        #parent process code goes here
        printf "%s\n", "parent";
        next;
     }
     $g_LOGFILE="child.log";
     setEnvironment();
     #child code goes here
     printf "%s\n", "child";
     exit;
 }

wait for @pids

【问题讨论】:

  • 您正在打印来自父母和孩子的相同消息。难道你的意思是关闭在父级下运行的if ($pid) { ... } 块中的打印?
  • 刚刚在父子节点中有一些示例 printf 语句。现在编辑了帖子。问题子消息打印在子日志以及父母日志中。我不希望它打印在父母日志中
  • 谁能帮我解决这个问题
  • 为什么不让父进程和子进程打印到单独的文件?

标签: perl


【解决方案1】:

好的,我测试了这段代码。这是我的sample code。在我的代码中存在类似(不准确)的问题:所有消息都双重写入子日志文件。

所以我对你的问题的回答:

问题是子进程 STDOUT 被重定向到父日志文件以及子日志文件。

这是因为当您使用管道 (open(STDOUT, "|tee ...) 打开文件作为底层结果时,您的进程 fork() 创建子进程,然后将 exec 编程到您运行的程序中 (tee)。 Forking(for tee) 获取主进程的 STDOUT,因此tee 将写入父进程的日志文件。所以我认为你必须撤销对主进程使用 STDOUT 句柄。或者,第二种方法 - 删除 tee 的使用 - 这是最简单的方法。

我也不明白在下面的 setEnvironment 函数中创建 OUT 和 ERR 文件句柄的目的。

似乎这是某人对上述问题的解决方法。您可以grep -rE ' \bERR\b' . 在代码中搜索是否使用。可能有人想保存真正的 STDOUT 和 STDERR 以供进一步使用。

【讨论】:

    【解决方案2】:

    看来原代码的意图如下:

    1. 当脚本从终端启动时,向终端提供聚合父子输出
    2. 此外,parent.log 中提供父输出的副本,在child.log 中提供子输出的副本

    请注意,就 2. 而言,@Unk 的答案 是正确的,并且与使用 tee 的任何代码相比,移动部件更少,但 未能实现 1。

    如果实现 以上 1. 和 2. 都很重要,那么请使用您的原始代码并只需在以下位置添加以下 setEnvironment 方法的顶部:

    sub setEnvironment()
    {
        if ( fileno OUT )
        {
            unless ( open(STDOUT, ">&OUT") )
            {
                print "Cannot restore STDOUT";
                return 2;
            }
            unless ( open(STDERR, ">&ERR") )
            {
                print "Cannot restore STDERR";
                return 2;
            }
        }
        else
        {
            unless ( open(OUT, ">&STDOUT") )
            {
                print "Cannot redirect STDOUT";
                return 2;
            }
            unless ( open(ERR, ">&STDERR") )
            {
                print "Cannot redirect STDERR";
                return 2;
            }
        }
        unless ( open(STDOUT, "|tee -ai $g_LOGPATH/$g_LOGFILE") )
        ...
    

    顺便说一句,如果您的实际代码还没有这样做,请不要忘记将$pid 也添加到@pids

      ...
      my $pid = fork;
      if ( $pid ) {
          push @pids, $pid;
          ...
    

    为什么以及如何工作?我们只是想在将其重新连接到tee 之前立即临时恢复原始STDOUT,以便tee 将其作为标准输出继承并实际直接写入原始的STDOUT(例如您的终端)而不是通过父级的tee(这是子级的STDOUT 通常在此更改之前指向的位置)写入(在分叉子级的情况下),因为继承自paremnt 进程,这就是将那些child 行注入parent.log 的原因。)

    因此,在回答您的一个问题时,编写设置OUTERR 的代码的人必须牢记上述内容。 (我不禁想知道您原始代码中缩进的差异是否表明有人在过去 删除了,过去的代码类似于您现在必须添加回来的代码。)

    这就是你现在在一天结束时得到的:

    $ rm -f parent.log child.log
    $ perl test.pl
    child
    parent
    child
    parent
    parent
    child
    parent
    child
    parent
    $ cat parent.log
    parent
    parent
    parent
    parent
    parent
    $ cat child.log
    child
    child
    child
    child
    child
    

    【讨论】:

    • +1 用于确定原始代码的可能 intent (并纠正我的错字!)。出于某种原因,我曾假设意图是明确将 OUT 和 ERR 用于终端。
    【解决方案3】:

    您始终可以通过closing it first and then reopening 将 STDOUT 重定向到日志文件:

    close STDOUT;
    open STDOUT, ">", $logfile;
    

    这样做的一个小缺点是,一旦 STDOUT 被重定向,在脚本执行期间您将不会在终端上看到任何输出。

    如果您希望父进程和子进程具有不同的日志文件,只需在 fork() 之后执行此重定向到不同的日志文件,如下所示:

    print "Starting, about to fork...\n";
    if (fork()) {
        print "In master process\n";
        close STDOUT;
        open STDOUT, ">", "master.log";
        print "Master to log\n";
    } else {
        print "In slave process\n";
        close STDOUT;
        open STDOUT, ">", "slave.log";
        print "Slave to log\n";
    }
    

    我已经测试过这在 Linux 和 Windows 上可以正常工作。

    【讨论】:

    • open(STDOUT,... 已经 dup2 超过 fdes 1,因此在您给出的示例中,close STDOUT 是一个 noop。在open 之前关闭的唯一时间会有所不同(这是不受欢迎的!)是在打开管道open(STDOUT, "|... 时(对pipe 的调用将重用第一个可用的fdes,在这种情况下为1,它将作为一个继承管道的末端以及被孩子视为标准输出,即很可能会发生非常奇怪的事情。)
    • 正确,但明确的close STDOUT 让读者清楚了解真正的意图,并且不会影响其他任何内容。
    • 恕我直言,评论可以使其更加清晰,甚至更清晰。你认为你引入了清晰性,但实际上你引入了一个潜在的错误——同样,当有人将 ">", $logfile 更改为 "|... 时(应该是透明的,对吗?它肯定是在常规的 shell 脚本中),有人在很多地方不愉快的惊喜。永远不要关闭STDxxx 句柄,当open 本身可以期望它们在那里时,它们不是你的可以关闭的。示例:print "OK\n"; close STDOUT; open STDOUT, "|tee log.txt"; print "Still OK???"; -- 你期望它做什么,它真正做什么?
    【解决方案4】:
    #!/usr/bin/perl 
    
    use strict;
    use warnings;
    use utf8;
    use Capture::Tiny qw/capture_stdout/;
    
    my $child_log = 'clild.log';
    my $parent_log = 'parent.log';
    
    my  $stdout = capture_stdout {
            if(fork()){
                my  $stdout = capture_stdout {
                    print "clild\n";
                };
                open my $fh, '>', $child_log;
                print $fh $stdout;
                close $fh;
                exit;
            }
            print "parent\n";
       };
                open my $fh, '>', $parent_log;
                print $fh $stdout;
                close $fh;
    

    【讨论】:

      【解决方案5】:

      所有其他答案都是正确的(尤其是 PSIalt)——我只是希望我能用与问题中的代码非常接近的更正代码来回答。需要注意的关键事项:

      "|tee -ai..."

      tee 命令将其标准打印到其标准输出,同时也打印到给定文件。正如 PSIalt 所说,删除它是确保每个进程的输出只发送到正确文件的最简单方法。

      setEnvironment() 循环内的父级

      原始代码不断将 STDOUT 重定向回teeed 文件。因此重新捕获 STDOUT。鉴于我在下面的代码,如果您将setEnvironment 移至#parent process code goes here 上方,您会看到除了一个“Real STDOUT”和“Real STDERR”之外的所有内容都实际出现在 parent.log 中。

      选项

      理想的做法是消除对重定向 STDOUT / STDERR 进行日志记录的任何依赖。我将有一个专用的log($level, $msg) 函数并开始将所有代码移动到使用它。最初,如果它只是现有行为的一个外观是可以的 - 当您达到适当的代码覆盖阈值时,您可以简单地将其切换出来。

      如果它是一个基本脚本并且不会产生愚蠢的大日志,为什么不将所有内容打印到 STDOUT 并带有一些您可以使用 grep 的前缀(例如 'PARENT:' / 'CHILD:')?

      这有点超出了问题的范围,但请考虑使用更结构化的日志记录方法。我会考虑使用 CPAN 日志记录模块,例如Log::Log4perl。这样,父母和孩子可以简单地请求正确的日志类别,而不是搞乱文件句柄。其他优势:

      • 标准化输出
      • 允许即时重新配置 - 在运行但行为不端的系统上将日志记录级别从 ERROR 更改为 DEBUG
      • 轻松重定向输出 - 无需更改代码即可重新排列日志文件、旋转文件、重定向到套接字/数据库等...
      use strict;
      use warnings;
      
      our $g_LOGPATH = '.';
      our $g_LOGFILE = "parent.log";
      
      our @pids;
      
      setEnvironment();
      
      for ( 1 .. 5 ) {
          my $pid = fork;
          if ($pid) {
              #parent process code goes here
              printf "%s\n", "parent";
      
              print OUT "Real STDOUT\n";
              print ERR "Real STDERR\n";
      
              push @pids, $pid;
              next;
          }
          $g_LOGFILE = "child.log";
          setEnvironment();
      
          #child code goes here
          printf "%s\n", "child";
          exit;
      }
      
      wait for @pids;
      
      sub setEnvironment {
          unless ( open( OUT, ">&STDOUT" ) ) {
              print "Cannot redirect STDOUT";
              return 2;
          }
      
          unless ( open( ERR, ">&STDERR" ) ) {
              print "Cannot redirect STDERR";
              return 2;
          }
      
          unless ( open( STDOUT, '>>', "$g_LOGPATH/$g_LOGFILE" ) ) {
              print "Cannot open log file $g_LOGPATH/$g_LOGFILE";
              return 2;
          }
      
          unless ( open( STDERR, ">&STDOUT" ) ) {
              print "Cannot redirect STDERR";
              return 2;
          }
          STDOUT->autoflush(1);
      }
      

      child.log:

      child
      child
      child
      child
      child
      

      父日志:

      parent
      parent
      parent
      parent
      parent
      

      STDOUT 取自终端:

      Real STDOUT (x5 lines)
      

      STDERR 取自终端:

      Real STDERR (x5 lines)
      

      【讨论】:

        猜你喜欢
        • 2015-08-18
        • 2012-07-14
        • 2021-06-01
        • 2012-02-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-18
        相关资源
        最近更新 更多