【问题标题】:Bash script lingers after exiting (issues with named pipe I/O)Bash 脚本在退出后仍然存在(命名管道 I/O 的问题)
【发布时间】:2017-06-08 09:25:41
【问题描述】:

总结

我已经为这个问题制定了解决方案。

基本上,被调用者 (wallpaper) 本身并没有退出,因为它正在等待另一个进程完成。

在 52 天的时间里,这种有问题的副作用越来越大,直到 10,000 多个延迟进程消耗了 10 多 GB 的 RAM,几乎使我的系统崩溃。

问题进程原来是从一个名为 log 的函数调用 printf,我已将它发送到后台并忘记了,因为它正在写入管道并挂起。

事实证明,写入命名管道的进程将阻塞,直到另一个进程出现并从中读取。

这反过来又将问题的要求从“我需要一种方法来阻止这些进程建立”改为“我需要一种更好的方法来绕过 FIFO I/O,而不是把它扔到后台”。


请注意,虽然问题已经解决,但我很乐意接受技术层面的详细回答。例如,为什么调用者脚本的 (wallpaper-run) 进程也会被复制,即使它只被调用一次,或者如何正确读取管道的状态信息,而不是依赖于 open 的未解之谜使用O_NONBLOCK 调用时失败。

原始问题如下。


问题

我有两个要循环运行的 bash 脚本。第一个 wallpaper-run 在无限循环中运行并调用第二个 wallpaper

它们是我的“桌面”的一部分,它是一堆组合在一起的 shell 脚本,用于扩充 dwm 窗口管理器。

壁纸运行:

log "starting wallpaper runner"

while true; do
    log "..."
    $scr/wallpaper
    sleep 900 # 15 minutes
done &

壁纸:

log "changing wallpaper"

# several utility functions ...

if [[ $1 ]]; then
    parse_arg $1
else
    load_random
fi

一些注意事项:

  • log 是从init 导出的函数,顾名思义,它记录一条消息。

  • init 在其前台调用 wallpaper-run(除其他外)(因此 while 循环在后台)

  • $scr也是由init定义的;它是所谓的“init-scripts”所在的目录

  • parse_argload_randomwallpaper 的本地对象

  • 尤其是图片通过feh程序加载到后台

  • 壁纸运行的加载方式如下:$mod/wallpaper-run

  • init由startx直接调用,并在运行wallpaper-run(和其他“模块”)之前启动dwm

现在问题是,由于某种原因,壁纸运行和壁纸都“徘徊”在内存中。也就是说,在循环的每次迭代之后,都会创建两个新的壁纸实例和壁纸运行实例,而“旧”实例不会被清理并陷入睡眠状态。这就像内存泄漏,但有拖延的进程而不是糟糕的内存管理。

在我的系统运行 52 天后,我发现了这个“进程泄漏”,因为系统内存不足。我必须杀死超过 10,000 个壁纸/运行实例才能使我的系统恢复正常工作。

我完全不知道为什么会这样。我认为这些脚本没有理由留在内存中,因为脚本退出应该意味着它的进程被清理了。

他们为什么徘徊和消耗资源?


更新 1

在 cmets 的一些帮助下(非常感谢 I'L'I),我将问题追溯到函数 log,它对 printf 进行后台调用(尽管我为什么选择这样做,但我不不记得了)。这是 init 中出现的函数:

log(){
    local pipe=$pipe_front
    if ! [[ -p $pipe ]]; then
        mkfifo $pipe
    fi
    printf ... >> $initlog
    printf ... > $pipe &
    printf ... &
    [[ $2 == "-g" ]] &&  notify-send "[DWM Init] $1"
    sleep 0.001
}

如你所见,这个函数写得很糟糕。我将它组合在一起是为了使其工作,而不是让它变得健壮。

第二个和第三个 printf 被发送到后台。我不记得我为什么这样做了,但大概是因为第一个 printf 一定是让日志挂起。

printf 行已被删节为“...”,因为它们相当复杂且与手头的问题无关(而且我用 40 分钟的时间做的事情比处理 Android 的垃圾文本更好输入接口)。特别是,诸如当前时间、调用进程的名称和传递的消息之类的东西会被打印出来,这取决于我们谈论的是哪个 printf。第一个具有最详细的信息,因为它被保存到一个丢失即时上下文的文件中,而 notify-send 行的详细信息最少,因为它将显示在桌面上。

整个管道崩溃是为了通过我为它编写的基本 shell 直接与 init 交互。

第三个 printf 是故意的;它打印到我在会话开始时登录的 tty。这样,如果 init 突然在我身上崩溃,我可以看到出错的日志。或者至少在它崩溃之前发生了什么

我将其包含在问题中,因为这是“泄漏”的根本原因。如果我能修复此功能,问题将得到解决。

该函数需要将消息记录到各自的来源并暂停,直到每次调用 printf 完成,但它也必须及时完成;无限期挂起和/或未能记录消息是不可接受的行为。


更新 2

在将log 函数(参见更新1)分离到测试脚本并设置模拟环境后,我将其归结为printf。

重定向到管道的 printf 调用,

printf "..." > $pipe

如果没有人在监听它,则挂起,因为它正在等待第二个进程获取管道的读取端并使用数据。这可能是我最初强制它们进入后台的原因,以便某个进程可以在某个时候从管道中读取数据,而在当前情况下,系统可以继续执行其他操作。

因此,调用 sleep 是一种未经过深思熟虑的黑客攻击,用于解决由于一个读取器试图同时读取多个写入器而导致的数据竞争问题。理论是,如果每个作者都必须等待 0.001 秒(尽管后台的 printf 与它后面的 sleep 无关),不知何故,这将使数据按顺序显示并修复错误。当然,回过头来看,这确实没什么用。

最终结果是几个后台进程挂在管道上,等待从中读取内容。

Prevent hanging of "echo STRING > fifo" when nothing...”的答案提供了导致产生此问题的错误的相同“解决方案”。显然不正确。然而,用户R.. 的一个有趣评论提到了一些关于 fifos 包含状态的内容,其中包括诸如哪些进程正在读取管道等信息。

存储状态?你的意思是读者的缺席/在场?这是fifo状态的一部分;任何将其存储在外面的尝试都是虚假的,并且会受到竞争条件的影响。

获取这些信息,如果没有读者就拒绝写是解决这个问题的关键。

但是,无论我在 Google 上搜索什么,我似乎都找不到任何关于读取管道状态的信息,即使在 C 中也是如此。如果需要,我非常愿意使用 C,但是一个 bash 解决方案(或现有的核心实用程序)将是首选。

所以现在问题变成了:我到底如何读取 FIFO 的状态信息,尤其是已经打开管道以进行读取和/或写入的进程?

【问题讨论】:

  • 注意:我是用手机输入的,我真的希望没有任何由自动更正引起的拼写错误
  • 你说log 调用wallpaper-run。是这样吗?
  • @我刚刚注意到这种模棱两可。现在修复它
  • 关于更新 2:我不确定是否应该更改问题的名称、创建一个新问题并从问题中链接到它,或者保留问题原样。我的判断告诉我保持原样,因为可以解决更新 2 中问题的答案将是有效解决问题所提出问题的答案。如果这违背了更频繁的用户会做的事情,我们深表歉意。
  • @hansaplast 我以简短的摘要作为问题的开头,并将问题的根本原因添加到问题标题中

标签: bash memory-leaks


【解决方案1】:

https://stackoverflow.com/a/20694422

上面链接的答案显示了一个 C 程序试图 open 一个带有 O_NONBLOCK 的文件。所以我尝试编写一个程序,如果 open 返回有效的文件描述符,则返回 0(成功),如果 open 返回 -1,则返回 1(失败)。

#include <fcntl.h>
#include <unistd.h>

int
main(int argc, char **argv)
{
    int fd = open(argv[1], O_WRONLY | O_NONBLOCK);

    if(fd == -1)
        return 1;

    close(fd);
    return 0;
}

我没有费心检查argv[1] 是否为空或打开是否因为文件不存在而失败,因为我只打算从保证给出正确参数的 shell 脚本中使用这个程序。

也就是说,程序完成了它的工作

$ gcc pipe-open.c
$ ./a.out ./pipe && echo "pipe has a reader" || echo "pipe has no reader"
$ ./a.out ./pipe && echo "pipe has a reader" || echo "pipe has no reader"

假设存在pipe,并且在第一次和第二次调用之间,另一个进程打开了管道(cat pipe),输出如下所示:

管道没有阅读器

管道有阅读器

如果管道有第二个写入器,程序也可以工作(即它会因为没有读取器而失败)

唯一的问题是,在关闭文件后,阅读器也关闭了管道的末端。并且删除对 close 的调用不会有任何好处,因为所有打开的文件描述符在 main 返回后都会自动关闭(控制转到退出,它遍历打开的文件描述符列表并一一关闭它们)。不好!

这意味着实际写入管道的唯一窗口是在其关闭之前,即来自 C 程序本身。

#include <fcntl.h>
#include <unistd.h>

int
write_to_pipe(int fd)
{
    char buf[1024];
    ssize_t nread;
    int nsuccess = 0;

    while((nread = read(0, buf, 1024)) > 0 && ++nsuccess)
        write(fd, buf, nread);

    close(fd);
    return nsuccess > 0 ? 0 : 2;
}

int
main(int argc, char **argv)
{
    int fd = open(argv[1], O_WRONLY | O_NONBLOCK);

    if(fd == -1)
        return 1;

    return write_to_pipe(fd);
}

调用:

$ echo hello world | ./a.out pipe
$ ret=$?
$ if [[ $ret == 1 ]]; then echo no reader
> elif [[ $ret == 2 ]]; then echo an error occurred trying to write to the pipe
> else echo success
> fi

输出条件与之前相同(第一次调用没有阅读器;第二次调用有):

没有读者

成功

另外,在终端读取管道时可以看到文本“Hello World”

最后,问题解决了。我有一个程序,它充当作家和管道之间的中间人,如果在调用时没有读取器连接到管道,或者如果有尝试写入管道和如果什么也没写,则表示失败。

最后一部分是新的。我认为将来知道是否没有写任何东西可能会很有用。

我以后可能会添加更多的错误检测,但是由于日志会在尝试写入之前检查管道是否存在,所以现在还可以

【讨论】:

    【解决方案2】:

    问题是您正在启动墙纸过程而没有检查上一次运行是否完成。因此,在 52 天内,4 * 24 * 52 = ~5000 实例可能会运行(但不确定您是如何找到 10000 个的)!是否可以使用flock 来确保一次只运行一个wallpaper 实例?

    看到这个帖子:Quick-and-dirty way to ensure only one instance of a shell script is running at a time

    【讨论】:

    • 它产生 10,000 个的原因是因为墙纸和墙纸运行都被复制了。我仍然不知道为什么后者被重复,但我已经追踪到日志功能的问题,我将在下一次编辑中添加到问题中。
    • 注意:自您回答以来,我已对该问题进行了两次重大更新。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-26
    • 2019-02-27
    • 1970-01-01
    相关资源
    最近更新 更多