Подтвердить что ты не робот

Ruby - fork, exec, detach.... у нас есть состояние гонки здесь?

Простой пример, который не работает на моей платформе (Ruby 2.2, Cygwin):

#!/usr/bin/ruby
backtt = fork { exec('mintty','/usr/bin/zsh','-i') }
Process.detach(backtt)
exit

Эта крошечная программа (когда она запускается из оболочки) должна охватывать окно терминала (mintty), а затем возвращает меня в приглашение оболочки.

Однако, пока он создает окно mintty, после этого у меня нет приглашения оболочки, и я не могу ничего вводить в вызывающей оболочке.

Но когда я вводю небольшую задержку перед отсоединением, либо используя "sleep", либо печатая что-то на stdout, он работает как ожидалось:

#!/usr/bin/ruby
backtt = fork { exec('mintty','/usr/bin/zsh','-i') }
sleep 1
Process.detach(backtt)
exit

Почему это необходимо?

Кстати, я хорошо знаю, что я мог (из оболочки) сделать

mintty /usr/bin/zsh -i &

или я мог бы использовать систему (...... &) внутри Ruby, но здесь дело не в этом. Меня особенно интересует поведение fork/exec/detach в Ruby. Любые идеи?

4b9b3361

Ответ 1

Проводка в качестве ответа, поскольку она слишком длинна для комментария

Хотя я не специалист в Ruby и вообще не знаю Cygwin, эта ситуация звучит очень знакомо мне, исходя из C/С++.

Этот script слишком короткий, поэтому родительский элемент родителя завершается, а внук пытается начать.

Что произойдет, если вы поместите сна после отсоединения и перед выходом?

Если моя теория верна, она тоже должна работать. Ваша программа выйдет до того, как произойдет (или достаточно) переключение потоков.

Я называю такие проблемы "прерывистым дрожанием руки". Хотя это психология, она описывает, что происходит.

Сон "отбрасывает временной срез", что приводит к переключению потоков,

Консольный вывод (любой ввод/вывод файлов) запускается в семафоры, что также приводит к переключению потоков.

Если моя идея правильная, она также должна работать, если вы не "спите", просто пересчитайте до 1e9 (в зависимости от скорости вычислений), потому что тогда превентивная многозадачность делает даже сам поток-переключатель не отказываясь от CPU.

Итак, это ошибка в программировании (IMHO: состояние гонки в этом случае философское), но будет сложно найти "кто". Существует много вещей.

Ответ 2

В соответствии с документацией:

Process::detach предотвращает этот , настраивая отдельный поток Ruby, единственным заданием которого является получение статуса процесса pid при его завершении.

NB: я не могу воспроизвести это поведение в любой из доступных мне операционных систем, а Im отправляю это как ответ только ради форматирования.

Так как Process.detach(backtt) прозрачно создает поток, я предлагаю вам попробовать:

#!/usr/bin/ruby
backtt = fork { exec('mintty','/usr/bin/zsh','-i') }
#                     ⇓⇓⇓⇓⇓
Process.detach(backtt).join
exit

Это не взломать каким-либо средним значением (как противоположно глупому sleep), так как вы, вероятно, знаете, что основная команда должна возвращать больше или меньше сразу. Я не гуру в cygwin, но у него могут быть некоторые проблемы с потоками, поэтому пусть этот поток обрабатывается.

Ответ 3

Я не являюсь ни рубином, ни парнем Cygwin, поэтому то, что я предлагаю здесь, может вообще не работать. В любом случае: я думаю, вы даже не попали в конкретную ошибку Ruby или Cygwin. В программе, называемой "старт", которую я написал на C много лет назад, я попал в ту же проблему. Вот комментарий с начала функции void daemonize_now():

/*
 * This is a little bit trickier than I expected: If we simply call
 * setsid(), it may fail! We have to fork() and exit(), and let our
 * child call setsid().
 *
 * Now the problem: If we fork() and exit() immediatelly, our child
 * will be killed before it ever had been run. So we need to sleep a
 * little bit. Now the question: How long? I don't know an answer. So
 * let us being killed by our child :-)
 */

Итак, стратегия такова: пусть родитель ждет на нем ребенка (это можно сделать непосредственно перед тем, как ребенок действительно имел шанс что-либо сделать), а затем позволить ребенку сделать отсоединяющую часть. Как? Позвольте ему создать новую группу процессов (она будет восстановлена ​​для процесса init). То, что setid() вызывает, я говорю в комментарии. Он будет работать примерно так (C-Syntax, вы должны иметь возможность искать правильное использование Ruby и самостоятельно применять необходимые изменения):

parentspid = getpid();
Fork = fork();
if (Fork) {
    if (Fork == -1) { // fork() failed
        handle error
    } else { // parent, Fork is the pid of the child
        int tmp; waitpid(0, &tmp, 0);
    }
} else { // child
    if (setsid() == -1) {
        handle error - possibly by doing nothing
        and just let the parent wait ...
    } else {
        kill(parentspid, SIGUSR1);
    }
    exec(...);
}

Вы можете использовать любой сигнал, который завершает процесс (т.е. SIGKILL). Я использовал SIGUSR1 и установил обработчик сигнала, который завершает (0) родительский процесс, поэтому вызывающий получает сообщение об успешном завершении. Только оговорка: вы получаете успех, даже если exec не работает. Тем не менее, это проблема, с которой невозможно работать, потому что после успешного exec вы больше не можете сигнализировать родителям. И поскольку вы не знаете, когда exec завершится с ошибкой (если он не сработает), вы вернетесь в состояние гонки.