【问题标题】:os.execute without inheriting parent's fdsos.execute 不继承父级的 fds
【发布时间】:2011-06-17 15:53:29
【问题描述】:

我有一个类似于此处描述的问题: Prevent fork() from copying sockets

基本上,在我的 Lua 脚本中,我生成了另一个脚本:

  • 无论哪种方式都不需要与我的脚本通信
  • 在我的脚本完成后继续运行
  • 是第 3 方程序,我无法控制其代码

问题是我的 Lua 脚本打开了一个 TCP 套接字来监听特定端口,并且在它退出并且 尽管 一个显式的 server:close() 之后,孩子(或者更具体地说是它的孩子)持有socket 并保持端口打开(处于 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 确认)阻塞,直到它退出。

如何以最简单的方式解决这个问题,最好不要添加任何额外的依赖项。

【问题讨论】:

    标签: sockets shell lua exec fork


    【解决方案1】:

    如果您只想在整个程序结束时保留s:close,我不确定您是否能够这样做。您可能会通过将其移至os.execute 之前成功,因为无论如何您都是breaking(但您可能不会在您的实际程序中这样做)。 为清楚起见编辑:实际问题是,在这种情况下,您生成子进程的唯一位置是使用 os.execute(),并且您无法控制 sleep 的子环境,其中一切都继承自主程序,包括套接字和文件描述符。

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

    因此,一个建议是获取luaposix,并使用其posix.fork()posix.exec() 功能,并在forked 子进程中调用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。

    附:当 exec 失败时,print('exec failed with rc: ' .. rc); 行曾抱怨过类型问题。我实际上不知道lua,所以你必须解决这个问题。 :) 此外,fork() 可能会失败,返回 -1。可能也应该在你的主代码中检查它的完整性。

    【讨论】:

    • 好的,虽然我暗地里希望它不会出现在fork()ing,但这是一个进步。 luaposix 作为一个 dep 很好,因为它内置在目标平台 - OpenWrt 上的解释器中。我们的示例sleep 占用端口的问题已经消失,但已替换为另外两个: - 如果您在fork 之后删除break 并将exec 发送到端口s:accept(),则不再超时并且它意味着非阻塞。 - 如果您将sleep 替换为ping,它的输出将与脚本输出相同的pty,这是不可取的。这些可能超出了原始问题的范围,但它们是相关的。
    • 抱歉,myopenid 在那里停了一会儿。根据您的其他问题,我对代码玩得更多,我认为这超出了我的 lua 知识。我在execp() 之前尝试过io.output('/dev/null')io.stdout:close(),但没有成功。以下是一些相关信息:stackoverflowlua manual。在wait() 上,我检查了luaposix 的源代码,它没有实现WNOHANG,所以这可能是非阻塞的问题。如果没有其他人对此作出回应,您可能会针对这些问题发布另一个 Q。
    • 我在这里找到了解决标准输出问题的方法: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())
    • 现在我只需要弄清楚为什么在fork()/exec() 之后不遵守s:accept() 的超时(通过strace 检查)并且执行被阻止,直到客户端连接.这就是 os.execute 单行变成复杂功能的方式,只是因为我想要控制套接字。没有更简单的方法吗?
    • 我不知道。当调用os.execute(即system())时,它仍然会在该调用中执行fork/exec/wait,但是您将失去对子proc继承的环境的控制,因此您没有机会关闭文件/socket 描述符等。如果有办法解决它,那将是特定于 lua 的东西。不知道为什么它忘记了你的超时。如果 lua 可以使用select(带有所有参数),您可以使用它的超时值,并检查您的s 何时变为可读(客户端连接)。另一个想法是如果setsockoptsocket 中,它有一个O_NONBLOCK 选项。
    【解决方案2】:

    我找到了一个更简单的解决方案,它利用os.execute(cmd)shell 中运行cmd 的事实,事实证明,它能够关闭文件描述符,如下所示:


    例如(在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 语法的出色帮助。

    【讨论】:

    • 干得好!有志者事竟成 :-) 很高兴你回来更新了。
    【解决方案3】:

    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 常量,因此您可能还需要手动添加它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-08-04
      • 1970-01-01
      • 2012-05-16
      • 2015-06-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多