Python守护程序吃了我的syslog消息

2020-08-31 17:44:05

在测试对Python程序的修改时,我注意到一件奇怪的事情:程序应该发送给syslog的一些消息丢失了。

#!/usr/bin/env python3#需要以下第三方Python包:#python-daemon:https://pypi.python.org/pypi/python-daemon#pylockfile:https://github.com/openstack/pylockfile导入日志。处理程序将sys导入后台进程导入daemon.pidfile作为pidlockfile导入daemon.runner从锁文件导入alreadyLocked from lockfile import LockTimeout def Main():pidpath=";/tmp/Simple-daemon.pid";logger。GetLogger(";Simple-daemon";)记录器。SetLevel(日志记录。信息)格式化程序=记录。格式化程序(";%(Asctime)s%(Module)s[%(Process)d]%(Level Name)s:%(Message)s";,";%Y-%m-%d%H:%M:%S&34;)处理程序=日志记录。操纵员。SysLogHandler(地址=";/dev/log";)处理程序。SetForMatter(格式化程序)记录器。AddHandler(处理程序)记录器。Info(";开始。";)#创建pidfile PIDF=pidlockfile.。TimeoutPIDLockFile(pidpath,10)#删除之前调用if守护进程留下的所有过时PID文件。跑步者。Is_pidfile_stale(PIDF):记录器。警告(";正在删除过时的PID锁定文件%s";,PIDF。路径)PIDF。BREAK_LOCK()daemon_context=daemon。DaemonContext(pidfile=PIDF,umask=0o22)try:daemon_context。Open()Except(AlreadyLocked,LockTimeout):记录器。关键(";无法锁定pidfile%s";,pidpath)系统。退出(%1)记录器。信息(#34;守护。";)记录器。信息(";另一条消息。";)如果__名称__==";__Main__";:Main()。

要重现该错误,请运行程序并grep字符串“simple-daemon”的系统日志。你会看到“开始”和“另一条信息”但不是“守护神”留言。

我在几个发行版上尝试了这一点。我无法在DebianStretch上重现该问题,但在CentOS、Arch Linux和Debian Buster上重现了该问题。我希望能找到消息“守护神”。在复制系统上,日志中唯一的字符串是:

8月28日16:30:16计算机2020-08-28 16:30:16 Simple-daemon[26333]信息:启动。8月28日16:30:16计算机2020-08-28 16:30:16 Simple-daemon[26335]信息:另一条消息。

在程序上运行strace并查找访问syslog的syscall,我发现:

套接字(AF_UNIX,SOCK_DGRAM|SOCK_CLOEXEC,0)=3connect(3,{sa_family=AF_UNIX,sun_path=";/dev/log";},10)=0#...。稍后:sendto(3,";<;14>;2020-08-27 06:16:45 Simple-daemon[109680]INFO:Daemonized.\0";,64,0,NULL,0)=-1\f25 EBADF(错误文件描述符)关闭(3)=-1\f25 EBADF(错误文件描述符)。

为什么此程序尝试写入然后关闭无效的filedescriptor?了解python-daemon包的作用并阅读logging.handlers.SysLogHandler之后,答案会变得更加明显。

默认情况下,python-daemon关闭所有打开的文件描述符。其中包括Python对象引用的文件描述符。系统日志的文件描述符被SysLogHandler对象的套接字成员引用。第一次写入系统日志将数据写入已经关闭的文件描述符,这触发了“关闭套接字并重新连接”序列。

我第一次看到这个问题是在CentOS 7上的Python2.7中。这里的straceoutput更加令人好奇:

Sendto(3,";<;14>;2020-08-27 06:43:12 Simple-daemon[3708]info:daemonized.\0";,62,0,null,0)=-1\f25 EBADF(错误文件描述符)套接字(AF_UNIX,SOCK_DGRAM,0)=3close(3)=0connect(3,{sa_family=AF_UNIX,sun_path=";/dee。},10)=-1\f25 EBADF(错误文件描述符)关闭(3)=-1\f25 EBADF(错误文件描述符)写入(2,";回溯(最近一次调用):\n";,35)=35。

在这里,写入失败后,SysLogHandler立即调用socketSystem调用,而不关闭旧套接字。然后,它关闭刚刚创建的套接字,并尝试连接到关闭的套接字!这是bug的汇合点:我一直在讨论的悬挂文件描述符bug,以及Python问题17981(“SysLogHandler在使用之前关闭连接”)。

该行创建套接字并将其分配给self.socket。作为副作用,它会导致旧的self.socket未被引用,并且可用于垃圾收集。当旧的self.socket被垃圾收集时,它的.close方法会被调用,因为Python不知道它已经关闭了。不幸的是,旧的和新的Socket对象都使用filedescriptor 3,因此新对象现在引用一个关闭的文件描述符。

Python问题17981的解决方案是在出错时关闭套接字。对象的.close方法将在创建新套接字之前调用,而不是在之后调用。这个修复是在2013年5月提交的,但它还没有发布到CentOS7。尽管17981版是一个转移视线的问题,但真正的问题是对文件描述符的悬而未决的引用的对象。

此示例程序中最简单的修复方法是告诉DaemonContext不要关闭syslog文件描述符:

在使用os.close这样的函数之前,应该仔细考虑。此系统调用作用于底层文件描述符,该描述符可能与期望管理其生存期的File对象相关联。