9
20
2018
8

永远不要 tail -f 管道

运维同事为了收集日志,配置程序将日志写入一个命名管道。然后他在外边拿 tail -f 去读,结果发生了灵异事件。通过 strace 可以看到,tail 进程读取了日志,但是却并没有再输出来。但是如果不启动输出日志的程序,而是在实例启动之后再进去往管道里写数据,却又是可以立即得到 tail 的输出的。

很奇怪的行为,一群人在那里研究半天,猜测是不是环境变量造成的啊,是不是放后台组执行造成的啊,是不是 XYZ 啊。——典型的「霰弹枪式」除错法

我当时也被带沟里了。于是跑去尝试复现,接着去读 tail 的源码。花了好久才明白这是一个很简单的问题:tail -f 的语义首先是 tail 这个词本身——先读文件最末尾的数据(默认是十行),然后再是 -f 选项的语义,即在文件更新时接着读取数据并输出。所以,当程序往里边写日志时,管道写端一直没关闭,tail 就一直读不到文件结束,也就无法确定最后十行是什么。当他们测试的时候,因为使用的是 echo shell 命令,打开文件、写入数据、关闭。这样 tail 一下子就读取到了文件末尾,然后把数据输出来了。接下来就是边读边输出了。

其实这种使用方法本身就很奇怪了,以至于这个执行流是兼容许多系统的 tail 的各种分支里,最最不常规、无可奈何的那一个分支。你都用管道了,cat 一下嘛。如果怕遇到管道被 reopen 的情况,就在 while true 里 cat 就好。

这个事件中,我也是见识了很多人解决问题的奇怪思路:「我猜猜猜。猜对了哦耶,猜错了,哎呀编程好难啊,Linux 系统好难啊……」猜你妹啊!你长的是大脑又不是骰子,用逻辑一步一步地取得结果不好吗!

有一个小游戏——猜数字。比如甲确定一个 1 到 1000 之内的整数,然后乙来猜。每当乙给出一个猜测时,甲回应猜对了,还是过大或者过小。如果乙知道什么叫二进制的话,乙可以保证在十次之内猜中的。

计算机系统和编程世界里,最棒的一点是确定性和逻辑性。虽然经常也不是像上例那样完全确定的,但至少比起人类社会要容易确定得多。特别是在有源码的时候。所以解决问题的路线也很简单,顺着问题的症状一路回溯,确认然后排除那些没有问题的部分,逐步缩小问题所在的范围,直接你看见它。就跟上边的猜数字游戏或者地毯式搜索一样。每一次猜测都是带着排除一部分没有问题的地方而去,而不是明明有证据表明某个地方不可能有问题,你还偏偏怀疑问题在那里,做无用功。

就像调查一个凶案,这些人放着有作案嫌疑的人不管,非要费劲地去调查那些有相当好的不在场证明的人。

Arch Linux 中文社区这边也有很多这种人。出了问题描述不清楚症状。新手嘛,没经验也没学习过如何描述事实,讲不清楚也没什么,引导对方获取截屏、日志,逐步排查问题就好了嘛。可就是有些热心人,喜欢提出自己的猜测。重点是:都不尝试证实猜测是否属实,就急着上解决方案。结果就是,我询问细节事实的消息没人理,求助者试试这个,试试那个,最终问题能否解决,就跟买彩票能否中奖一样,全凭运气。

Category: Linux | Tags: Arch Linux fifo linux 社群
9
11
2017
3

等连上互联网之后再来找我吧

最近公司弄了 Wi-Fi 登录。就是那个叫 captive portal 的东西。

Android 早就会在连接 Wi-Fi 时检测网络是不是要登录了,为此 Google 弄了个 /generate_204 的 URL。小米、高通、USTC、v2ex 也都提供了这个东西,方便广大中国大陆 Android 用户使用。(我发现我的 Android 使用的是高通的地址,没有用 Google 的。)

但我使用的 Arch Linux 自行开发的 netctl 网络管理工具没这种功能。火狐倒是不知道什么时候加上了,不过使用的地址 http://detectportal.firefox.com/success.txt 是返回 200 的。

所以我启动火狐就可以看到要登录的提示了。然而问题是,其它程序不知道要登录啊。像 offlineimap、openvpn、rescuetime 这种还好,会自己重试。可每次网络需要登录的时候 dcron 就会给我发一堆邮件告诉我我的 git pull 都失败了……当然还有我老早就注意到的 pkgstats,经常会因为启动过早而无法发送统计数据。

所以呢,得想个办法,等连上互联网之后再跑那些脚本啊服务什么的。

检测是不是连好了很简单,不断尝试就可以了。但我需要一个系统级的 Condition 对象来通知等待方可以继续了。然而我只知道 Linux 有提供信号量。难道要自己弄共享内存来用么?

#archlinux-cn 问了一下,farseerfc 说试试命名管道。我想了想,还真可以。只有读端的时候进程就会阻塞,一旦有写端就能成功打开了。当然没有读端的打开写端会打不开,不过没关系,反正这进程也不能退出,得一直拿着这个文件描述符。

没想到很少用到的命名管道有意想不到的用法呢。我以前还为了不阻塞而专门写了篇文章呢。

于是负责检测网络连通的 check-online 和等待网络连好的 wait-online 都写好了。

check-online 应当是个服务。那就交给 systemd 吧。然后……systemd 不是有个 network-online.target 么?正好可以让 check-online 来达成这个目标呢,多合适呀。

于是服务写好了。测试了几天,大成功!不仅 wait-online 很好地工作了,而且我发现 openvpn 和 pkgstats 自动排到 network-online.target 后边去了。nginx 的 OSCP staple 经常因为 DNS 失败而无法成功,我也可以在联好网之后去 reload 一下它了。(不是强依赖,我可不希望连不上网的时候我本地的 wiki 也访问不了。)

整个项目就叫作 wait-online,在 GitHub 上,欢迎送小星星哦~Arch Linux 包可以从 [archlinuxcn] 仓库 安装 wait-online-git 包。

10
14
2011
9

通过命名管道进行异步通信

需求是这样子的:一个程序要提供一个IPC接口,接收异步的命令。这个接口应该尽量简单,能像/proc下的文件那样通过写入数据来通信,所以我选中了命名管道。读取命名管道很简单,像普通文件那样打开然后读取就可以了。但这样做的问题是,在没有写者的时候open会阻塞。man 2 open下找到了两个标志位:O_ASYNCO_NONBLOCK。我被排在前面的O_ASYNC骗了,它只是读写时使用信号进行异步操作,open依旧阻塞。继续向后翻,才看到O_NONBLOCK,还特意注明了Neither the open() nor any subsequent operations on the file descriptor which is returned will cause the calling process to wait.

试了试,发现open并不像读写时那样在将阻塞时返回EWOULDBLOCK错误,而是返回了一个可用的文件描述符。既然文件描述符都有了,接下来自然毫无悬念地select了。完整的演示代码如下:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import os
import time
import select

fd = os.open('test', os.O_NONBLOCK | os.O_RDONLY)
while True:
  if not select.select([fd], [], [], 1)[0]:
    print('waiting...')
  else:
    got = os.read(fd, 1024).decode().rstrip()
    if not got:
      os.close(fd)
      fd = os.open('test', os.O_NONBLOCK | os.O_RDONLY)
    else:
      print('got', got)
Category: Linux | Tags: linux python fifo 异步

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com