在不中断管道写入的情况下清空文件

ban*_*nab 14 bash io-redirection

我有一个程序,我将其输出重定向到日志文件:

./my_app > log
Run Code Online (Sandbox Code Playgroud)

我想不时(按需)清除(即清空)日志并尝试各种方法,例如

cat "" > log
Run Code Online (Sandbox Code Playgroud)

然而,原始管道似乎总是被破坏,程序不再将其输出重定向到日志文件。

有没有办法做到这一点?

更新

请注意,我无法修改生成输出的应用程序。它只是将它输出到标准输出,我想将它保存在日志中,以便我可以在需要时检查它,并在需要时清除它。但是我不需要重新启动应用程序。

gol*_*cks 13

此问题的另一种形式出现在日志定期轮换的长时间运行的应用程序中。即使您移动原始日志(例如,mv log.txt log.1)并在任何实际日志记录发生之前立即用同名文件替换它,如果进程保持文件打开,它最终将写入log.1(因为这可能仍然是打开的 inode) 或什么都没有。

处理这个问题的常用方法(系统记录器本身就是这样工作的)是在进程中实现一个信号处理程序,该处理程序将关闭并重新打开其日志。然后,当您想要移动或清除(通过删除)日志时,请立即将该信号发送到进程。

这是 bash 的一个简单演示——请原谅我粗糙的 shell 技能(但如果您要编辑它以获得最佳实践等,请确保您首先了解该功能并编辑之前测试您的修订版):

#!/bin/bash

trap sighandler INT

function sighandler () {
    touch log.txt
    exec &> log.txt
}

echo $BASHPID
exec &> log.txt

count=0;
while [ $count -lt 60 ]; do
    echo "$BASHPID Count is now $count"
    sleep 2
    ((count++))
done          
Run Code Online (Sandbox Code Playgroud)

通过分叉到后台开始:

> ./test.sh &
12356
Run Code Online (Sandbox Code Playgroud)

请注意,它向终端报告其 PID,然后开始登录到log.txt. 你现在有 2 分钟的时间来玩。等待几秒钟,然后尝试:

> mv log.txt log.1 && kill -s 2 12356
Run Code Online (Sandbox Code Playgroud)

简单的kill -2 12356在这里也可能对你有用。信号 2 是 SIGINT(这也是 Ctrl-C 的作用,因此您可以在前台尝试并从另一个终端移动或删除日志文件),这trap应该是陷阱。去检查;

> cat log.1
12356 Count is now 0
12356 Count is now 1
12356 Count is now 2
12356 Count is now 3
12356 Count is now 4
12356 Count is now 5
12356 Count is now 6
12356 Count is now 7
12356 Count is now 8
12356 Count is now 9
12356 Count is now 10
12356 Count is now 11
12356 Count is now 12
12356 Count is now 13
12356 Count is now 14
Run Code Online (Sandbox Code Playgroud)

现在让我们看看log.txt即使我们移动了它,它是否仍在写入:

> cat log.txt
12356 Count is now 15
12356 Count is now 16
12356 Count is now 17
12356 Count is now 18
12356 Count is now 19
12356 Count is now 20
12356 Count is now 21
Run Code Online (Sandbox Code Playgroud)

请注意,它一直在停止的地方继续前进。如果您不想保留记录,只需通过删除它来清除日志

> rm -f log.txt && kill -s 2 12356
Run Code Online (Sandbox Code Playgroud)

查看:

> cat log.txt
12356 Count is now 29
12356 Count is now 30
12356 Count is now 31
12356 Count is now 32
12356 Count is now 33
12356 Count is now 34
12356 Count is now 35
12356 Count is now 36
Run Code Online (Sandbox Code Playgroud)

仍在继续。

不幸的是,您不能在执行的子进程的 shell 脚本中执行此操作,因为如果它在前台,则 bash 自己的信号处理程序 ( traps) 将被挂起,并且如果您将其 fork 到后台,则无法重新分配其输出。即,这是您必须在应用程序中实现的内容。

然而...

如果您不能修改应用程序(例如,因为您没有编写它),我有一个 CLI 实用程序可以用作中介。您还可以在用作日志管道的脚本中实现一个简单的版本:

#!/bin/bash

trap sighandler INT

function sighandler () {
    touch log.txt
    exec 1> log.txt
}

echo "$0 $BASHPID"
exec 1> log.txt

count=0;
while read; do
    echo $REPLY
done  
Run Code Online (Sandbox Code Playgroud)

让我们称之为pipetrap.sh。现在我们需要一个单独的程序来测试,模拟你想要记录的应用程序:

#!/bin/bash

count=0
while [ $count -lt 60 ]; do
    echo "$BASHPID Count is now $count"
    sleep 2
    ((count++))
done           
Run Code Online (Sandbox Code Playgroud)

那将是test.sh

> (./test.sh | ./pipetrap.sh) &
./pipetrap.sh 15859
Run Code Online (Sandbox Code Playgroud)

这是两个具有独立 PID 的独立进程。要清除test.sh的输出,该输出正通过pipetrap.sh

> rm -f log.txt && kill -s 2 15859
Run Code Online (Sandbox Code Playgroud)

查看:

>cat log.txt
15858 Count is now 6
15858 Count is now 7
15858 Count is now 8
Run Code Online (Sandbox Code Playgroud)

15858, test.sh, 仍在运行,并且正在记录其输出。在这种情况下,不需要修改应用程序。

  • 如果你*不能*在你的应用程序中实现一个信号处理程序(因为你不能修改它),你可以使用这种技术通过信号陷阱来管理日志——请参阅**“但是.. .”** (2认同)

Sté*_*las 6

TL; 博士

追加模式打开日志文件:

cmd >> log
Run Code Online (Sandbox Code Playgroud)

然后,您可以安全地使用以下命令截断它:

: > log
Run Code Online (Sandbox Code Playgroud)

细节

使用类似 Bourne 的 shell,可以通过 3 种主要方式打开文件进行写入。在只写( >)、读+写( <>) 或追加(和只写>>)模式下。

在前两个中,内核会记住您的当前位置(我指的是打开的文件描述,由所有复制或继承它的文件描述符共享)进入文件。

当你这样做时:

cmd > log
Run Code Online (Sandbox Code Playgroud)

log由 shell 以只写模式打开cmd.

cmd(它的初始进程由 shell 和所有可能的子进程产生)在写入它们的 stdout 时,写入它们在该文件上共享的打开文件描述所保持的当前光标位置。

例如,如果cmd最初 writes zzz,则该位置将位于文件中的字节偏移量 4,并且下一次cmd或其子文件写入文件时,无论文件在间隔内是增长还是缩小,数据都将写入该位置.

如果文件缩小了,例如它被截断了

: > log
Run Code Online (Sandbox Code Playgroud)

cmdwrites xx,这些xx将被写入 offset 4,并且前 3 个字符将被 NUL 字符替换。

$ exec 3> log # open file on fd 3.
$ printf zzz >&3
$ od -c log
0000000   z   z   z
0000003
$ printf aaaa >> log # other open file description -> different cursor
$ od -c log
0000000   z   z   z   a   a   a   a
0000007
$ printf bb >&3 # still write at the original position
$ od -c log
0000000   z   z   z   b   b   a   a
0000007
$ : > log
$ wc log
0 0 0 log
$ printf x >&3
$ od -c log
0000000  \0  \0  \0  \0  \0   x
0000006
Run Code Online (Sandbox Code Playgroud)

这意味着您不能截断以只写模式打开的文件(这与read+write相同),就像您这样做一样,在文件上打开文件描述符的进程将在开头留下 NUL 字符文件(那些,除了在 OS/X 上,通常不会占用磁盘空间,但它们会变成稀疏文件)。

相反(您会注意到大多数应用程序在写入日志文件时都会这样做),您应该以追加模式打开文件:

cmd >> log
Run Code Online (Sandbox Code Playgroud)

或者

: > log && cmd >> log
Run Code Online (Sandbox Code Playgroud)

如果你想从一个空文件开始。

在追加模式下,所有写入都在文件末尾进行,无论上次写入的位置如何:

$ exec 4>> log
$ printf aa >&4
$ printf x >> log
$ printf bb >&4
$ od -c log
0000000   a   a   x   b   b
0000005
$ : > log
$ printf cc >&4
$ od -c log
0000000   c   c
0000002
Run Code Online (Sandbox Code Playgroud)

这也更安全,因为如果两个进程错误地打开(以这种方式)文件(例如,如果您启动了同一个守护程序的两个实例),它们的输出不会相互覆盖。

在最新版本的 Linux 上,您可以通过查看以下内容来检查当前位置以及文件描述符是否以追加模式打开/proc/<pid>/fdinfo/<fd>

$ cat /proc/self/fdinfo/4
pos:        2
flags:      0102001
Run Code Online (Sandbox Code Playgroud)

或与:

$ lsof +f G -p "$$" -ad 4
COMMAND  PID USER   FD   TYPE  FILE-FLAG DEVICE SIZE/OFF     NODE NAME
zsh     4870 root    4w   REG 0x8401;0x0 252,18        2 59431479 /home/chazelas/log
~# lsof +f g -p "$$" -ad 4
COMMAND  PID USER   FD   TYPE FILE-FLAG DEVICE SIZE/OFF     NODE NAME
zsh     4870 root    4w   REG   W,AP,LG 252,18        2 59431479 /home/chazelas/log
Run Code Online (Sandbox Code Playgroud)

这些标志对应于传递给系统调用的O ..._ 标志open

$ gcc -E - <<< $'#include <fcntl.h>\nO_APPEND O_WRONLY' | tail -n1
02000 01
Run Code Online (Sandbox Code Playgroud)

O_APPEND是 0x400 或八进制 02000)

所以外壳的>>打开与文件O_WRONLY|O_APPEND(和0100000这里是O_LARGEFILE这是不相关的这个问题),同时>O_WRONLY唯一的(并且<>O_RDWR唯一的)。

如果您执行以下操作:

sudo lsof -nP +f g | grep ,AP
Run Code Online (Sandbox Code Playgroud)

要搜索用 打开的文件O_APPEND,您会发现当前打开的大多数日志文件都可以写入您的系统。

  • @Mvorisek,那是重定向不产生输出的命令的输出:`:`。如果没有命令,shell 之间的行为会有所不同。 (2认同)