在子进程中重定向 STDOUT

Posted

技术标签:

【中文标题】在子进程中重定向 STDOUT【英文标题】:Redirecting STDOUT in child process 【发布时间】: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 语句。现在编辑了帖子。问题子消息打印在子日志以及父母日志中。我不希望它打印在父母日志中 谁能帮我解决这个问题 为什么不让父进程和子进程打印到单独的文件? 【参考方案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】:

看来原代码的意图如下:

    当脚本从终端启动时,向终端提供聚合父子输出 此外,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)

【讨论】:

以上是关于在子进程中重定向 STDOUT的主要内容,如果未能解决你的问题,请参考以下文章

在win32中重定向标准输出不会重定向标准输出

放置在子文件夹中时,错误文档在 .htaccess 中重定向

在 csh 中重定向 stderr

Linux中重定向管道和grep命令总结

Linux中重定向管道和grep命令总结

Linux中重定向管道和grep命令总结