os.execute без наследования родительских fds

У меня проблема, аналогичная описанной здесь: Запретить fork() копирование сокетов

По сути, внутри моего Lua-скрипта я создаю еще один скрипт, который:

  • в любом случае не требует связи с моим скриптом
  • продолжает работать после завершения моего скрипта
  • это сторонняя программа, код которой я не контролирую

Проблема в том, что мой сценарий Lua открывает сокет TCP для прослушивания определенного порта, и после его закрытия и несмотря на явный server:close() дочерний элемент (или, точнее, его дочерние элементы) удерживает сокет и сохраняет порт open (в состоянии 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) до тех пор, пока он не выйдет.

Как мне решить эту проблему самым простым способом, желательно без добавления каких-либо дополнительных зависимостей.


person koniu    schedule 29.01.2011    source источник


Ответы (3)


Я нашел гораздо более простое решение, в котором используется тот факт, что 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), чтобы она не перехватывала порт после выхода из моего скрипта. У него также есть бонус, заключающийся в том, что он заботится о перенаправлении stdout и stderr.

Это намного компактнее и проще, чем обход ограничений Lua, и не требует дополнительных зависимостей. Спасибо иду на #uclibc, где я получил блестящую помощь с окончательным синтаксисом оболочки от команды встроенного Linux.

person koniu    schedule 29.01.2011
comment
Отличная работа! Где есть желание, там и способ :-) Рад, что вы тоже вернулись с обновлением. - person Sdaz MacSkibbons; 30.01.2011

Я не уверен, сможете ли вы сделать это так, если хотите сохранить s:close только в конце всей программы. Вы можете преуспеть, переместив его до os.execute, так как вы все равно break делаете (но вы, вероятно, не делаете этого в своей реальной программе). Изменить для ясности: реальная проблема заключается в том, что единственное место, где вы порождаете подпроцесс в этом случае, — это использование os.execute(), и вы не можете контролировать дочернюю среду сна, в которой все унаследованы от основной программы, включая дескрипторы сокетов и файлов.

Таким образом, канонический способ сделать это в POSIX — использовать fork(); close(s); exec(); вместо system() (также известного как os.execute), поскольку system()/os.execute будут привязаны к текущему состоянию процесса во время выполнения, и вы не сможете закрыть его, пока он заблокирован. в подпроцессе.

Таким образом, предлагается взять luaposix и использовать его функции posix.fork() и posix.exec(), а также вызвать s:close() в дочерний процесс forked. Это не должно быть так плохо, поскольку вы уже используете внешний пакет, полагаясь на luasocket.


EDIT: вот сильно прокомментированный код, чтобы сделать это с помощью 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. Строка print('exec failed with rc: ' .. rc); однажды пожаловалась на проблему с типом при ошибке exec. Я на самом деле не знаю lua, так что вам придется это исправить. :) Кроме того, fork() может дать сбой, возвращая -1. Вероятно, следует проверить это и в вашем основном коде для полноты.

person Sdaz MacSkibbons    schedule 29.01.2011
comment
Хорошо, это прогресс, хотя я втайне надеялся, что до fork()ing дело не дойдет. luaposix хорош как деп, так как он встроен в интерпретатор на целевой платформе - OpenWrt. Проблема с нашим примером sleep, перехватывающая порт, исчезла, но была заменена еще двумя: - если вы уберете break после fork и отправите exec в порт s:accept(), больше не будет тайм-аута, и он должен быть неблокирующим. - если вы замените sleep на ping, его вывод будет иметь тот же pty, что и вывод скрипта, что нежелательно. Они могут выходить за рамки исходного вопроса, но связаны между собой. - person koniu; 29.01.2011
comment
Извините, myopenid ненадолго упал. Я больше играл с кодом в соответствии с вашими другими вопросами, и я думаю, что это выходит за рамки моих знаний lua. Я пробовал io.output('/dev/null') и io.stdout:close() до execp(), но безуспешно. Вот некоторая связанная информация: stackoverflow и руководство по lua. На wait() я проверил источник для luaposix, и он не реализует WNOHANG, так что это может быть проблемой для неблокировки. Если никто больше не ответит на это, вы можете опубликовать еще один вопрос по этим вопросам. - person Sdaz MacSkibbons; 29.01.2011
comment
Я нашел решение проблемы со стандартным выводом здесь: 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()) - person koniu; 29.01.2011
comment
Теперь мне просто нужно выяснить, почему после fork()/exec() тайм-аут на s:accept() не соблюдается (проверено с strace) и выполнение блокируется до тех пор, пока не подключится клиент. И вот как os.execute одна строка превращается в сложную функцию только потому, что я хочу иметь управляющий сокет. Нет ли более простого способа? - person koniu; 29.01.2011
comment
Не то, что я знаю о. При вызове os.execute (то есть system()) он по-прежнему выполняет fork/exec/wait в этом вызове, но вы теряете контроль над средой, унаследованной дочерним процессом, поэтому у вас нет возможности закрыть дескрипторы файлов/сокетов, и т. д. Если есть способ обойти это, это будет что-то специфичное для lua. Не знаю, почему он забывает ваш тайм-аут. Если lua может использовать select (со всеми аргументами), вы можете использовать его значение времени ожидания и проверить, когда ваш s станет доступным для чтения (подключение клиента). Другая идея заключается в том, что если setsockopt находится в socket, у него есть опция O_NONBLOCK. - person Sdaz MacSkibbons; 29.01.2011

Подход POSIX заключается в установке дескрипторов файлов с флагом FD_CLOEXEC с помощью fcntl ( 2). Если установлено, все подпроцессы не будут наследовать файловые дескрипторы, отмеченные этим флагом.

Стандартный Lua не имеет функции fcntl, но ее можно добавить с помощью модуля lua posix, как показано в предыдущие ответы. Чтобы взять ваш пример, вам нужно будет изменить запуск как таковой:

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

Обратите внимание, что я не нашел константу FD_CLOEXEC в исходниках luaposix, поэтому вам, возможно, придется добавить ее вручную.

person zimbatm    schedule 08.10.2011