不继承父进程文件描述符的os.execute函数

5
我有一个类似于这里描述的问题:Prevent fork() from copying sockets 基本上,在我的Lua脚本中,我正在生成另一个脚本,它:
  • 不需要与我的脚本进行通信
  • 在我的脚本完成后继续运行
  • 是第三方程序,我无法控制其代码
问题在于,我的Lua脚本打开了一个TCP套接字以侦听特定端口,并且在退出后,尽管显式使用了server:close(),但子进程(或更具体地说是其子进程)仍然保持套接字并保持端口处于打开状态(处于LISTEN状态),从而防止我的脚本再次运行。
以下是演示问题的示例代码:
require('socket')

print('listening')
s = socket.bind("*", 9999)
s:settimeout(1)

while true do
    print('accepting connection')
    local c = s:accept()
    if c then
            c:settimeout(1)
            local rec = c:receive()
            print('received ' .. rec)
            c:close()
            if rec == "quit" then break end
            if rec == "exec" then 
                    print('running ping in background')
                    os.execute('sleep 10s &')
                    break
            end     
    end
end
print('closing server')
s:close()

如果我运行上面的脚本并且执行echo quit | nc localhost 9999,一切都很好 - 程序退出并且端口关闭。
但是如果我执行echo exec | nc localhost 9999,程序会退出,但是由生成的sleep阻塞了端口(通过netstat -lpn确认),直到它退出。
我该如何以最简单的方式解决这个问题,最好不要添加任何额外的依赖项。
3个回答

4
我发现了一个更简单的解决方案,利用了 os.execute(cmd)shell 中运行 cmd 的事实,这个 shell 具有关闭文件描述符的能力,如下所示:
例如(在 ash 中测试):
    exec 3<&-                                      # closes fd3
    exec 3<&- 4<&-                                 # closes fd3 and fd4
    eval exec `seq 1 255 | sed -e 's/.*/&<\&-/'`   # closes all file descriptors 

在基于luasocket的示例中,只需要替换以下内容:
    os.execute('sleep 10s &')

使用:

    os.execute("eval exec `seq 1 255 | sed -e 's/.*/&<\\&-/'`; sleep 10s &")

这个命令会关闭所有文件描述符,包括我的服务器套接字,在执行实际命令(这里是sleep 10s)之前,这样它在我的脚本退出后不会占用端口。它还有一个额外的好处,就是处理stdoutstderr的重定向。
这比绕过Lua的限制要简洁得多,也不需要任何额外的依赖。感谢#uclibc,我从嵌入式Linux团队那里得到了一些关于最终shell语法的精彩帮助。

干得好!有志者,事竟成 :-) 很高兴你回来更新了。 - Sdaz MacSkibbons

2
我不确定如果你想在整个程序的结尾保留s:close,是否能够这样做。如果你要break,你可以把它移动到os.execute之前,这样可能会成功(但在你的真实程序中可能不是这样)。需要澄清的是:实际问题在于,在这种情况下,唯一创建子进程的地方是使用os.execute(),而你无法控制sleep的子环境,其中所有内容都从主程序继承,包括套接字和文件描述符。
因此,在POSIX上执行此操作的规范方法是使用fork(); close(s); exec();,而不是使用system()(也称为os.execute),因为system()/os.execute将在执行期间保持当前进程状态,并且您无法在子进程中阻塞时关闭它。
因此,建议使用luaposix,并使用其posix.fork()posix.exec()功能,在fork的子进程中调用s:close()。应该不会太麻烦,因为你已经依赖于luasocket这个外部包。
编辑:这里是使用luaposix的代码,有详细注释:
require('socket')
require('posix')

print('listening')
s = socket.bind("*", 9999)
s:settimeout(1)

while true do
    print('accepting connection')
    local c = s:accept()
    if c then
            c:settimeout(1)
            local rec = c:receive()
            print('received ' .. rec)
            c:close()
            if rec == "quit" then break end
            if rec == "exec" then
                    local pid = posix.fork()
                    if pid == 0 then
                        print('child: running ping in background')
                        s:close()
                        -- exec() replaces current process, doesn't return.
                        -- execp has PATH resolution
                        rc = posix.execp('sleep','60s');
                        -- exec has no PATH resolution, probably "more secure"
                        --rc = posix.exec('/usr/bin/sleep','60s');
                        print('exec failed with rc: ' .. rc);
                    else
                        -- if you want to catch the SIGCHLD:
                        --print('parent: waiting for ping to return')
                        --posix.wait( pid )
                        print('parent: exiting loop')
                    end
                    break;
            end
    end
end
print('closing server')
s:close()

在调用exec之前,子进程关闭了套接字,并且在父进程退出时,netstat -nlp输出显示系统正确地不再侦听9999端口。

P.S. 当exec失败时,print('exec failed with rc: ' .. rc);这一行会抱怨一个类型问题。我实际上不懂lua,所以你需要修复它。此外,fork()可能会失败,返回-1。为了完整性,你的主要代码也应该检查这个。


好的,尽管我暗自希望它不会涉及到 fork(),但这是进展。 luaposix 作为依赖项很好,因为它已经内置在目标平台的解释器中-OpenWrt。我们例子中 sleep 占用端口的问题已经解决了,但又出现了两个新问题:
  • 如果在 fork 后删除 break 并向端口发送 exec,则 s:accept() 不再超时,而它应该是非阻塞的。
  • 如果将 sleep 替换为 ping,则其输出将与脚本输出一起发送到相同的 pty,这是不希望看到的。 这些问题可能超出了原始问题的范围,但它们是相关的。
- koniu
抱歉,我的openid暂时出了点问题。我根据你的其他问题进一步尝试了代码,但我认为这已经超出了我的Lua知识范围。在execp()之前,我尝试过io.output('/dev/null')io.stdout:close(),但都没有成功。以下是一些相关信息:stackoverflowlua手册。关于wait(),我检查了luaposix的源代码,它没有实现WNOHANG,所以这可能会成为非阻塞的问题。如果没有其他人回复这个问题,你可以在这些问题上再发一个问题。 - Sdaz MacSkibbons
我在这里找到了一个解决stdout问题的方法:http://lua-users.org/wiki/HiddenFeatures。这将关闭标准文件:`local f=assert(io.open '/dev/null'); debug.setfenv(io.stdout, debug.getfenv(f)); f:close(); assert(io.stdout:close())`。 - koniu
现在我只需要弄清楚为什么在fork()/exec()之后,s:accept()的超时没有被尊重(使用strace检查),并且执行会一直阻塞直到客户端连接。这就是一个os.execute单行变成复杂函数的方式,因为我想要控制套接字。难道没有更简单的方法吗? - koniu
我不知道有没有解决方法。在调用 os.execute(即system())时,它仍然会进行 fork/exec/wait 操作,但您失去了对子进程继承的环境的控制,因此您没有机会关闭文件/套接字描述符等。如果有一种绕过它的方法,那么应该是与 Lua 有关的。我不知道为什么它会忘记你的超时时间。如果 Lua 可以使用 select(带有所有参数),则可以使用其超时值,并在您的 s 可读时(客户端连接)进行检查。另一个想法是,如果 setsockoptsocket 中,则有一个 O_NONBLOCK 选项。 - Sdaz MacSkibbons

1

POSIX的方法是使用fcntl (2)为您的文件描述符设置FD_CLOEXEC标志。当设置该标志时,所有子进程都不会继承带有该标志的文件描述符。

标准Lua没有fcntl功能,但可以通过lua posix模块添加它,正如在先前的答案中介绍的那样。以您的示例为例,您需要这样更改开始:

require('socket')
require('posix')
s = socket.bind("*", 9999)
posix.setfl(s, posix.FD_CLOEXEC)
s:settimeout(1)

请注意,我在luaposix源代码中没有找到FD_CLOEXEC常量,因此您可能需要手动添加它。

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