os.execute没有继承父级的fds

时间:2011-01-29 06:57:59

标签: sockets shell lua exec fork

我遇到类似于此处描述的问题: 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 个答案:

答案 0 :(得分:4)

我找到了一个更简单的解决方案,利用os.execute(cmd)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语法的精彩帮助。

答案 1 :(得分:2)

如果您只想在整个程序结束时保留s:close,我不确定您是否能够这样做。你可能会在os.execute之前将其移动到成功,因为无论如何你都是break(但你可能不会在你的真实程序中这样做)。 为了清晰起见而编辑:实际问题是,在这种情况下,您唯一产生子流程的地方是使用os.execute(),而您无法控制睡眠的子环境,其中所有内容都从主程序继承而来,包括套接字和文件描述符。

因此,在POSIX上执行此操作的规范方法是使用fork(); close(s); exec();而不是system()(aka,os.execute)作为system() / os.execute将在执行期间挂起当前进程状态,并且在子进程中被阻止时您将无法关闭它。

因此,建议抓住luaposix,并使用其posix.fork()posix.exec()功能,并在s:close() ed子进程中调用fork 。不应该这么糟糕,因为您已经依赖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。为了完整性,也可以在主代码中检查它。

答案 2 :(得分:1)

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

Stock Lua没有fcntl功能,但可以使用前面答案中介绍的lua posix模块添加它。举个例子,你必须改变这样的开头:

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

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