Linux非阻塞FIFO(按需记录)

44

我希望能够按需记录程序的输出。例如,将输出记录到终端,但是另一个进程可以随时连接当前的输出。

经典的方法是:

myprogram 2>&1 | tee /tmp/mylog

并按需

tail /tmp/mylog

然而,即使在未使用时,这将创建一个不断增长的日志文件,直到驱动器空间耗尽。因此,我的尝试是:

mkfifo /tmp/mylog
myprogram 2>&1 | tee /tmp/mylog

并且按需执行

cat /tmp/mylog

现在我可以随时读取 /tmp/mylog。但是,任何输出都会阻塞程序,直到 /tmp/mylog 被读取。我希望这个 fifo 可以刷新任何未被读回的输入数据。如何实现?


虽然有几个答案可以绕过非阻塞FIFO日志记录问题(使用logrotate、screen等),对于大多数目的来说这些方法都很有效,但原始问题似乎无法用简单的bash魔法解决。因此,也许正确的答案是“无法完成”。赏金将授予实现小型缺失工具的答案。 - dronus
1
似乎魔法确实存在;请看我的回答。 - Piotr Dobrogost
10个回答

58

受到您的问题启发,我编写了一个简单的程序,可让您执行以下操作:

$ myprogram 2>&1 | ftee /tmp/mylog

它类似于tee,但是将stdin克隆到stdout和命名管道(现在必须这样)而不阻塞。这意味着如果您想以这种方式记录日志,可能会丢失日志数据,但我认为在您的情况下可以接受。 诀窍在于阻止SIGPIPE信号并忽略对损坏的fifo进行写入时出现的错误。当然,这个示例可以以各种方式进行优化,但是到目前为止,我想它已经做到了。

/* ftee - clone stdin to stdout and to a named pipe 
(c) racic@stackoverflow
WTFPL Licence */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int readfd, writefd;
    struct stat status;
    char *fifonam;
    char buffer[BUFSIZ];
    ssize_t bytes;
    
    signal(SIGPIPE, SIG_IGN);

    if(2!=argc)
    {
        printf("Usage:\n someprog 2>&1 | %s FIFO\n FIFO - path to a"
            " named pipe, required argument\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    fifonam = argv[1];

    readfd = open(fifonam, O_RDONLY | O_NONBLOCK);
    if(-1==readfd)
    {
        perror("ftee: readfd: open()");
        exit(EXIT_FAILURE);
    }

    if(-1==fstat(readfd, &status))
    {
        perror("ftee: fstat");
        close(readfd);
        exit(EXIT_FAILURE);
    }

    if(!S_ISFIFO(status.st_mode))
    {
        printf("ftee: %s in not a fifo!\n", fifonam);
        close(readfd);
        exit(EXIT_FAILURE);
    }

    writefd = open(fifonam, O_WRONLY | O_NONBLOCK);
    if(-1==writefd)
    {
        perror("ftee: writefd: open()");
        close(readfd);
        exit(EXIT_FAILURE);
    }

    close(readfd);

    while(1)
    {
        bytes = read(STDIN_FILENO, buffer, sizeof(buffer));
        if (bytes < 0 && errno == EINTR)
            continue;
        if (bytes <= 0)
            break;

        bytes = write(STDOUT_FILENO, buffer, bytes);
        if(-1==bytes)
            perror("ftee: writing to stdout");
        bytes = write(writefd, buffer, bytes);
        if(-1==bytes);//Ignoring the errors
    }
    close(writefd); 
    return(0);
}

你可以使用以下标准命令进行编译:

$ gcc ftee.c -o ftee

你可以运行以下命令进行快速验证:

$ ping www.google.com | ftee /tmp/mylog

$ cat /tmp/mylog

此外,请注意 - 这不是多路复用器。一次只能有一个进程执行$ cat /tmp/mylog


2
你想要实现的目标相当不寻常,这种方法适用于构建应用程序的情况。对于大多数场景,“tail -f logfile.log”就足够了。 - racic
6
我认为对于任何能够生成大量调试输出但在没有问题的情况下并不重要的长期运行程序,都非常有用。例如考虑单一用途的嵌入式设备。如果系统长时间无人看管运行,那么日志文件就不是很有用了。也许还有一个只读文件系统来保护嵌入式功能免受文件系统丢失和电源故障的影响。因此,日志文件就没有意义了。 - dronus
1
@racic:哇!!!现在真的很少看到C语言了。如果可以的话,我会给你10分的,所以我也+1了你的评论。 - J. M. Becker
你能让错误信息更加晦涩吗?我读了三遍代码后仍然可以隐约猜测出是什么导致了这些错误。 - anon
RushPL,TechZilla:谢谢,希望它能为您正常工作。@Evi1M4chine:我猜你应该熟悉**perror()**函数的描述。这是链接,我已经为您谷歌了:http://pubs.opengroup.org/onlinepubs/009695399/functions/perror.html - racic
显示剩余6条评论

12

这是一个(非常)古老的帖子,但最近我遇到了类似的问题。实际上,我需要将stdin克隆到stdout,并将其复制到一个非阻塞的管道中。第一个答案中提议的ftee确实有所帮助,但对我的用例来说太不稳定了。这意味着如果我没有及时处理数据,我就会丢失可以处理的数据。

我面临的情况是我有一个进程(some_process),它聚合一些数据并每三秒钟将其结果写入stdout。简化后的设置如下(在实际设置中,我使用的是命名管道):

some_process | ftee >(onlineAnalysis.pl > results) | gzip > raw_data.gz

现在,raw_data.gz必须被压缩并且必须完整。ftee很好地完成了这项工作。但是我中间使用的管道太慢了,无法抓取刷新出来的数据,但如果它可以获得数据,它足够快地处理所有内容,这已经通过常规的tee进行了测试。然而,如果未命名管道发生任何事情,常规tee会阻塞,而我想随时接入,因此tee不是一个选择。回到主题:当我在中间加入一个缓冲区时,情况就变得更好了,结果为:

some_process | ftee >(mbuffer -m 32M| onlineAnalysis.pl > results) | gzip > raw_data.gz

但是这仍然会丢失我可以处理的数据。因此,我前进并将之前提出的免费版扩展为带缓冲的版本(bftee)。它仍具有所有相同的特性,但在写入失败时使用一个(低效?)内部缓冲区。如果缓冲区已满,则仍会丢失数据,但对于我的情况而言效果很好。像往常一样,还有很大的改进空间,但由于我从这里复制了代码,所以我想将其分享给可能需要它的人。

/* bftee - clone stdin to stdout and to a buffered, non-blocking pipe 
    (c) racic@stackoverflow
    (c) fabraxias@stackoverflow
    WTFPL Licence */

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <signal.h>
    #include <unistd.h>

    // the number of sBuffers that are being held at a maximum
    #define BUFFER_SIZE 4096
    #define BLOCK_SIZE 2048

    typedef struct {
      char data[BLOCK_SIZE];
      int bytes;
    } sBuffer;

    typedef struct {
      sBuffer *data;  //array of buffers
      int bufferSize; // number of buffer in data
      int start;      // index of the current start buffer
      int end;        // index of the current end buffer
      int active;     // number of active buffer (currently in use)
      int maxUse;     // maximum number of buffers ever used
      int drops;      // number of discarded buffer due to overflow
      int sWrites;    // number of buffer written to stdout
      int pWrites;    // number of buffers written to pipe
    } sQueue;

    void InitQueue(sQueue*, int);              // initialized the Queue
    void PushToQueue(sQueue*, sBuffer*, int);  // pushes a buffer into Queue at the end 
    sBuffer *RetrieveFromQueue(sQueue*);       // returns the first entry of the buffer and removes it or NULL is buffer is empty
    sBuffer *PeakAtQueue(sQueue*);             // returns the first entry of the buffer but does not remove it. Returns NULL on an empty buffer
    void ShrinkInQueue(sQueue *queue, int);    // shrinks the first entry of the buffer by n-bytes. Buffer is removed if it is empty
    void DelFromQueue(sQueue *queue);          // removes the first entry of the queue

    static void sigUSR1(int);                  // signal handled for SUGUSR1 - used for stats output to stderr
    static void sigINT(int);                   // signla handler for SIGKILL/SIGTERM - allows for a graceful stop ?

    sQueue queue;                              // Buffer storing the overflow
    volatile int quit;                         // for quiting the main loop

    int main(int argc, char *argv[])
    {   
        int readfd, writefd;
        struct stat status;
        char *fifonam;
        sBuffer buffer;
        ssize_t bytes;
        int bufferSize = BUFFER_SIZE;

        signal(SIGPIPE, SIG_IGN);
        signal(SIGUSR1, sigUSR1);
        signal(SIGTERM, sigINT);
        signal(SIGINT,  sigINT);

        /** Handle commandline args and open the pipe for non blocking writing **/

        if(argc < 2 || argc > 3)
        {   
            printf("Usage:\n someprog 2>&1 | %s FIFO [BufferSize]\n"
                   "FIFO - path to a named pipe, required argument\n"
                   "BufferSize - temporary Internal buffer size in case write to FIFO fails\n", argv[0]);
            exit(EXIT_FAILURE);
        }

        fifonam = argv[1];
        if (argc == 3) {
          bufferSize = atoi(argv[2]);
          if (bufferSize == 0) bufferSize = BUFFER_SIZE;
        }

        readfd = open(fifonam, O_RDONLY | O_NONBLOCK);
        if(-1==readfd)
        {   
            perror("bftee: readfd: open()");
            exit(EXIT_FAILURE);
        }

        if(-1==fstat(readfd, &status))
        {
            perror("bftee: fstat");
            close(readfd);
            exit(EXIT_FAILURE);
        }

        if(!S_ISFIFO(status.st_mode))
        {
            printf("bftee: %s in not a fifo!\n", fifonam);
            close(readfd);
            exit(EXIT_FAILURE);
        }

        writefd = open(fifonam, O_WRONLY | O_NONBLOCK);
        if(-1==writefd)
        {
            perror("bftee: writefd: open()");
            close(readfd);
            exit(EXIT_FAILURE);
        }

        close(readfd);


        InitQueue(&queue, bufferSize);
        quit = 0;

        while(!quit)
        {
            // read from STDIN
            bytes = read(STDIN_FILENO, buffer.data, sizeof(buffer.data));

            // if read failed due to interrupt, then retry, otherwise STDIN has closed and we should stop reading
            if (bytes < 0 && errno == EINTR) continue;
            if (bytes <= 0) break;

            // save the number if read bytes in the current buffer to be processed
            buffer.bytes = bytes;

            // this is a blocking write. As long as buffer is smaller than 4096 Bytes, the write is atomic to a pipe in Linux
            // thus, this cannot be interrupted. however, to be save this should handle the error cases of partial or interrupted write none the less.
            bytes = write(STDOUT_FILENO, buffer.data, buffer.bytes);
            queue.sWrites++;

            if(-1==bytes) {
                perror("ftee: writing to stdout");
                break;
            }

            sBuffer *tmpBuffer = NULL;

            // if the queue is empty (tmpBuffer gets set to NULL) the this does nothing - otherwise it tries to write
            // the buffered data to the pipe. This continues until the Buffer is empty or the write fails.
            // NOTE: bytes cannot be -1  (that would have failed just before) when the loop is entered. 
            while ((bytes != -1) && (tmpBuffer = PeakAtQueue(&queue)) != NULL) {
               // write the oldest buffer to the pipe
               bytes = write(writefd, tmpBuffer->data, tmpBuffer->bytes);

               // the  written bytes are equal to the buffer size, the write is successful - remove the buffer and continue
               if (bytes == tmpBuffer->bytes) {
                 DelFromQueue(&queue);
                 queue.pWrites++;
               } else if (bytes > 0) {
                 // on a positive bytes value there was a partial write. we shrink the current buffer
                 //  and handle this as a write failure
                 ShrinkInQueue(&queue, bytes);
                 bytes = -1;
               }
            }
            // There are several cases here:
            // 1.) The Queue is empty -> bytes is still set from the write to STDOUT. in this case, we try to write the read data directly to the pipe
            // 2.) The Queue was not empty but is now -> bytes is set from the last write (which was successful) and is bigger 0. also try to write the data
            // 3.) The Queue was not empty and still is not -> there was a write error before (even partial), and bytes is -1. Thus this line is skipped.
            if (bytes != -1) bytes = write(writefd, buffer.data, buffer.bytes);

            // again, there are several cases what can happen here
            // 1.) the write before was successful -> in this case bytes is equal to buffer.bytes and nothing happens
            // 2.) the write just before is partial or failed all together - bytes is either -1 or smaller than buffer.bytes -> add the remaining data to the queue
            // 3.) the write before did not happen as the buffer flush already had an error. In this case bytes is -1 -> add the remaining data to the queue
            if (bytes != buffer.bytes)
              PushToQueue(&queue, &buffer, bytes);
            else 
              queue.pWrites++;
        }

        // once we are done with STDIN, try to flush the buffer to the named pipe
        if (queue.active > 0) {
           //set output buffer to block - here we wait until we can write everything to the named pipe
           // --> this does not seem to work - just in case there is a busy loop that waits for buffer flush aswell. 
           int saved_flags = fcntl(writefd, F_GETFL);
           int new_flags = saved_flags & ~O_NONBLOCK;
           int res = fcntl(writefd, F_SETFL, new_flags);

           sBuffer *tmpBuffer = NULL;
           //TODO: this does not handle partial writes yet
           while ((tmpBuffer = PeakAtQueue(&queue)) != NULL) {
             int bytes = write(writefd, tmpBuffer->data, tmpBuffer->bytes);
             if (bytes != -1) DelFromQueue(&queue);
           }
        }

        close(writefd);

    }


    /** init a given Queue **/
    void InitQueue (sQueue *queue, int bufferSize) {
      queue->data = calloc(bufferSize, sizeof(sBuffer));
      queue->bufferSize = bufferSize;
      queue->start = 0;
      queue->end = 0;
      queue->active = 0;
      queue->maxUse = 0;
      queue->drops = 0;
      queue->sWrites = 0;
      queue->pWrites = 0;
    }

    /** push a buffer into the Queue**/
    void PushToQueue(sQueue *queue, sBuffer *p, int offset)
    {

        if (offset < 0) offset = 0;      // offset cannot be smaller than 0 - if that is the case, we were given an error code. Set it to 0 instead
        if (offset == p->bytes) return;  // in this case there are 0 bytes to add to the queue. Nothing to write

        // this should never happen - offset cannot be bigger than the buffer itself. Panic action
        if (offset > p->bytes) {perror("got more bytes to buffer than we read\n"); exit(EXIT_FAILURE);}

        // debug output on a partial write. TODO: remove this line
        // if (offset > 0 ) fprintf(stderr, "partial write to buffer\n");

        // copy the data from the buffer into the queue and remember its size
        memcpy(queue->data[queue->end].data, p->data + offset , p->bytes-offset);
        queue->data[queue->end].bytes = p->bytes - offset;

        // move the buffer forward
        queue->end = (queue->end + 1) % queue->bufferSize;

        // there is still space in the buffer
        if (queue->active < queue->bufferSize)
        {
            queue->active++;
            if (queue->active > queue->maxUse) queue->maxUse = queue->active;
        } else {
            // Overwriting the oldest. Move start to next-oldest
            queue->start = (queue->start + 1) % queue->bufferSize;
            queue->drops++;
        }
    }

    /** return the oldest entry in the Queue and remove it or return NULL in case the Queue is empty **/
    sBuffer *RetrieveFromQueue(sQueue *queue)
    {
        if (!queue->active) { return NULL; }

        queue->start = (queue->start + 1) % queue->bufferSize;
        queue->active--;
        return &(queue->data[queue->start]);
    }

    /** return the oldest entry in the Queue or NULL if the Queue is empty. Does not remove the entry **/
    sBuffer *PeakAtQueue(sQueue *queue)
    {
        if (!queue->active) { return NULL; }
        return &(queue->data[queue->start]);
    }

    /*** Shrinks the oldest entry i the Queue by bytes. Removes the entry if buffer of the oldest entry runs empty*/
    void ShrinkInQueue(sQueue *queue, int bytes) {

      // cannot remove negative amount of bytes - this is an error case. Ignore it
      if (bytes <= 0) return;

      // remove the entry if the offset is equal to the buffer size
      if (queue->data[queue->start].bytes == bytes) {
        DelFromQueue(queue);
        return;
      };

      // this is a partial delete
      if (queue->data[queue->start].bytes > bytes) {
        //shift the memory by the offset
        memmove(queue->data[queue->start].data, queue->data[queue->start].data + bytes, queue->data[queue->start].bytes - bytes);
        queue->data[queue->start].bytes = queue->data[queue->start].bytes - bytes;
        return;
      }

      // panic is the are to remove more than we have the buffer
      if (queue->data[queue->start].bytes < bytes) {
        perror("we wrote more than we had - this should never happen\n");
        exit(EXIT_FAILURE);
        return;
      }
    }

    /** delete the oldest entry from the queue. Do nothing if the Queue is empty **/
    void DelFromQueue(sQueue *queue)
    {
        if (queue->active > 0) {
          queue->start = (queue->start + 1) % queue->bufferSize;
          queue->active--;
        }
    }

    /** Stats output on SIGUSR1 **/
    static void sigUSR1(int signo) {
      fprintf(stderr, "Buffer use: %i (%i/%i), STDOUT: %i PIPE: %i:%i\n", queue.active, queue.maxUse, queue.bufferSize, queue.sWrites, queue.pWrites, queue.drops);
    }

    /** handle signal for terminating **/
    static void sigINT(int signo) {
      quit++;
      if (quit > 1) exit(EXIT_FAILURE);
    }

这个版本增加了一个可选参数,用于指定要缓冲的管道块的数量。我的示例调用现在看起来像这样:

some_process | bftee >(onlineAnalysis.pl > results) 16384 | gzip > raw_data.gz

导致在丢弃发生之前需要缓冲16384个块。这会使用大约32 Mbyte的额外内存,但...谁在意呢?

当然,在实际环境中,我使用命名管道,以便根据需要进行连接和断开连接。它看起来像这样:

mkfifo named_pipe
some_process | bftee named_pipe 16384 | gzip > raw_data.gz &
cat named_pipe | onlineAnalysis.pl > results

此外,该进程对信号的反应如下: SIGUSR1 -> 将计数器打印到 STDERR SIGTERM,SIGINT -> 首先退出主循环并将缓冲区刷新到管道,第二个立即终止程序。

也许这会在将来帮助某些人... 祝好


9
似乎bash的<>重定向操作符(参见3.6.10 打开文件描述符以供读写)使得使用它打开的文件/管道的写入变为非阻塞的。 这样应该可以工作:
$ mkfifo /tmp/mylog
$ exec 4<>/tmp/mylog
$ myprogram 2>&1 | tee >&4
$ cat /tmp/mylog # on demend

在 #bash IRC 频道上,gniourf_gniourf 给出了解决方案。


实际上,这种方式可以避免fifo阻塞。tail在该fifo上无法工作,可能是因为它等待从未到来的EOFcat的输出有效,但似乎只提供自上次cat以来的任何输出。因此,我想知道使用了什么缓冲区(通常的管道/ fifo缓冲区?)以及它能持续多长时间,当它被填满时会发生什么。 - dronus
1
经过一段时间k后,缓冲区已满,程序暂停执行直到FIFO被排空。所以这并没有真正解决我的问题。 - dronus
cat命令是阻塞的。 - Tinmarino

9
然而,即使不使用,这也会创建一个不断增长的日志文件,直到驱动器用完空间。
为什么不定期轮换日志呢?甚至有一个程序可以帮你做到这一点,叫做“logrotate”。
还有一个生成日志消息并根据类型执行不同操作的系统,它被称为“syslog”。
你甚至可以将两者结合起来。让你的程序生成syslog消息,配置syslog将它们放在一个文件中,并使用logrotate确保它们不会填满磁盘。
如果你发现自己正在为一个小型嵌入式系统编写程序,并且程序的输出很重,那么你可以考虑各种技术。
远程syslog:将syslog消息发送到网络上的syslog服务器。
使用syslog中可用的严重性级别对消息进行不同的处理。例如,丢弃“INFO”,但记录和转发“ERR”或更高级别的消息。例如,到控制台。
在程序中使用信号处理程序,在HUP上重新读取配置,并按需在此方式下变化日志生成方式。
让你的程序在unix套接字上监听,并在打开时通过套接字将消息写入。甚至可以通过这种方式实现交互式控制台。
使用配置文件,提供精细的日志输出控制。

嗯,这是为一个小型嵌入式系统设计的,程序的输出非常庞大。因此,我希望只在需要时获取数据,而不依赖于常规运行工具来存储任何东西或者仅存储极少量的数据。 - dronus
我想要一个类似于'dmesg'的东西,只能存储极少量的信息。 - dronus
在嵌入式设备上,通用二进制BusyBox包含一个环形缓冲日志,可以由“logger”填充并由“logread”读取。 运作良好。 注意事项:只能使用一个全局日志。 - dronus

6

可以将日志传输到一个UDP套接字中。由于UDP是无连接的,因此不会阻塞发送程序。当然,如果接收器或网络无法跟上,则会丢失日志。

myprogram 2>&1 | socat - udp-datagram:localhost:3333

那么当您想观察日志记录时:
socat udp-recv:3333 -

有一些其他很酷的好处,比如可以同时附加多个听众或广播到多个设备。

如果使用localhost或127.0.0.1,是否保证不会在物理网络适配器上产生任何真实的网络流量? - grenix
我喜欢这个答案,只是要注意它可能会产生相当高的CPU负载,例如如果您将“yes”用作“myprogram”。另一方面,其他答案也可能出现这种情况。 - grenix

6

BusyBox通常用于嵌入式设备,可以通过以下方式创建一个RAM缓冲日志:

syslogd -C

可以填写的内容

logger

并且被读取

logread

功能相当不错,但只提供一个全局日志。


5
如果您能在嵌入式设备上安装screen,那么您可以在其中运行'myprogram'并分离它,随时重新附加以查看日志。类似于下面的内容:
$ screen -t sometitle myprogram
Hit Ctrl+A, then d to detach it.

每当您想查看输出时,请重新附加它:

$ screen -DR sometitle
Hit Ctrl-A, then d to detach it again.

这样,您就不必担心程序输出会占用磁盘空间。

1
更好的是,由于“screen”可以捕获多个屏幕,因此您可以使用“Ctrl-A ESC”进入“复制模式”,并使用箭头键向上滚动相当长的时间。如果完成了,请再次使用“ESC”退出该模式。 - dronus

3
给定的fifo方法存在问题,当管道缓冲区被填满且没有读取进程时,整个过程将挂起。为了使fifo方法工作,我认为您需要实现一个命名管道客户端-服务器模型,类似于BASH:从两个输入流中读取的最佳架构中提到的模型(请参见稍微修改后的代码,示例代码2)。
对于解决方法,您还可以使用while ... read结构而不是将stdout tee到命名管道,通过在while ... read循环内部实现计数机制来定期按指定行数覆盖日志文件。这将防止不断增长的日志文件(示例代码1)。
# sample code 1

# terminal window 1
rm -f /tmp/mylog
touch /tmp/mylog
while sleep 2; do date '+%Y-%m-%d_%H.%M.%S'; done 2>&1 | while IFS="" read -r line; do 
  lno=$((lno+1))
  #echo $lno
  array[${lno}]="${line}"
  if [[ $lno -eq 10 ]]; then
    lno=$((lno+1))
    array[${lno}]="-------------"
    printf '%s\n' "${array[@]}" > /tmp/mylog
    unset lno array
  fi
  printf '%s\n' "${line}"
done

# terminal window 2
tail -f /tmp/mylog


#------------------------


# sample code 2

# code taken from: 
# https://dev59.com/PlnUa4cB1Zd3GeqPY0Mb
# terminal window 1

# server
(
rm -f /tmp/to /tmp/from
mkfifo /tmp/to /tmp/from
while true; do 
  while IFS="" read -r -d $'\n' line; do 
    printf '%s\n' "${line}"
  done </tmp/to >/tmp/from &
  bgpid=$!
  exec 3>/tmp/to
  exec 4</tmp/from
  trap "kill -TERM $bgpid; exit" 0 1 2 3 13 15
  wait "$bgpid"
  echo "restarting..."
done
) &
serverpid=$!
#kill -TERM $serverpid

# client
(
exec 3>/tmp/to;
exec 4</tmp/from;
while IFS="" read -r -d $'\n' <&4 line; do
  if [[ "${line:0:1}" == $'\177' ]]; then 
    printf 'line from stdin: %s\n' "${line:1}"  > /dev/null
  else       
    printf 'line from fifo: %s\n' "$line"       > /dev/null
  fi
done &
trap "kill -TERM $"'!; exit' 1 2 3 13 15
while IFS="" read -r -d $'\n' line; do
  # can we make it atomic?
  # sleep 0.5
  # dd if=/tmp/to iflag=nonblock of=/dev/null  # flush fifo
  printf '\177%s\n' "${line}"
done >&3
) &
# kill -TERM $!


# terminal window 2
# tests
echo hello > /tmp/to
yes 1 | nl > /tmp/to
yes 1 | nl | tee /tmp/to
while sleep 2; do date '+%Y-%m-%d_%H.%M.%S'; done 2>&1 | tee -a /tmp/to


# terminal window 3
cat /tmp/to | head -n 10

3
如果您的进程写入任何日志文件,然后每隔一段时间就擦除文件并重新开始,以防止文件过大,或者使用logrotate
tail --follow=name --retry my.log

这就是你所需要的。你会得到与你的终端一样多的滚动条。

不需要任何非标准的东西。我没有尝试过处理小日志文件,但我们所有的日志都是这样轮换,我从未注意到丢失行。


1
这种方法的优点是,如果您在vi中打开日志,并且在后台删除了整个文件,则整个文件将保留在磁盘上,直到您完成为止。如果您使用less查看日志,您可以随着其追加而跟踪。 - teknopaul

0

为了跟随Fabraxias的脚步,我将分享一下我对racic代码的小修改。在我的一个使用场景中,我需要抑制对STDOUT的写入,因此我添加了另一个参数:swallow_stdout。如果它不是 0,那么对STDOUT的输出将被关闭。

由于我不是C编码人员,所以我在阅读代码时添加了注释,或许它们对其他人有用。

/* ftee - clone stdin to stdout and to a named pipe 
(c) racic@stackoverflow
WTFPL Licence */

// gcc /tmp/ftee.c -o /usr/local/bin/ftee

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int readfd, writefd;        // read & write file descriptors
    struct stat status;         // read file descriptor status
    char *fifonam;              // name of the pipe
    int swallow_stdout;         // 0 = write to STDOUT
    char buffer[BUFSIZ];        // read/write buffer
    ssize_t bytes;              // bytes read/written

    signal(SIGPIPE, SIG_IGN);   

    if(3!=argc)
    {
        printf("Usage:\n someprog 2>&1 | %s [FIFO] [swallow_stdout] \n" 
            "FIFO           - path to a named pipe (created beforehand with mkfifo), required argument\n"
            "swallow_stdout - 0 = output to PIPE and STDOUT, 1 = output to PIPE only, required argument\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    fifonam = argv[1];
    swallow_stdout = atoi(argv[2]);

    readfd = open(fifonam, O_RDONLY | O_NONBLOCK);  // open read file descriptor in non-blocking mode

    if(-1==readfd)  // read descriptor error!
    {
        perror("ftee: readfd: open()");
        exit(EXIT_FAILURE);
    }

    if(-1==fstat(readfd, &status)) // read descriptor status error! (?)
    {
        perror("ftee: fstat");
        close(readfd);
        exit(EXIT_FAILURE);
    }

    if(!S_ISFIFO(status.st_mode)) // read descriptor is not a FIFO error!
    {
        printf("ftee: %s in not a fifo!\n", fifonam);
        close(readfd);
        exit(EXIT_FAILURE);
    }

    writefd = open(fifonam, O_WRONLY | O_NONBLOCK); // open write file descriptor non-blocking
    if(-1==writefd) // write file descriptor error!
    {
        perror("ftee: writefd: open()");
        close(readfd);
        exit(EXIT_FAILURE);
    }

    close(readfd); // reading complete, close read file descriptor

    while(1) // infinite loop
    {
        bytes = read(STDIN_FILENO, buffer, sizeof(buffer)); // read STDIN into buffer
        if (bytes < 0 && errno == EINTR)
            continue;   // skip over errors

        if (bytes <= 0) 
            break; // no more data coming in or uncaught error, let's quit since we can't write anything

        if (swallow_stdout == 0)
            bytes = write(STDOUT_FILENO, buffer, bytes); // write buffer to STDOUT
        if(-1==bytes) // write error!
            perror("ftee: writing to stdout");
        bytes = write(writefd, buffer, bytes); // write a copy of the buffer to the write file descriptor
        if(-1==bytes);// ignore errors
    }
    close(writefd); // close write file descriptor
    return(0); // return exit code 0
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接