Вопрос или проблема
У меня есть хост с Debian 12 (bookworm), на котором я запускаю три виртуальные машины, используя QEMU / KVM. Чтобы упростить управление виртуальными машинами, каждая из них имеет сокет монитора QEMU. Эти сокеты — /vm/1.sock
, /vm/2.sock
и /vm/3.sock
. Среди прочего, я могу использовать такой сокет для корректного завершения работы соответствующей виртуальной машины; команда будет выглядеть примерно так:
printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/1.sock
Пока что всё хорошо. Это работает, как ожидалось, для каждой из трёх виртуальных машин / сокетов.
Теперь у меня есть скрипт, который должен завершить работу всех этих виртуальных машин, при этом не должно происходить задержек, если одна из команд на завершение работы зависает или занимает много времени. Это означает, что мне нужно выполнять командную строку, показанную выше, в фоновом режиме.
Соответствующая часть в оригинальной версии этого скрипта выглядит так:
{ printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/1.sock; } &
{ printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/2.sock; } &
{ printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/3.sock; } &
Помимо того, что это производит странный вывод (поскольку команды выполняются асинхронно и их выводы переплетаются), это не работает так, как задумано. Это завершает работу одной из виртуальных машин, но не двух других. Во время моих тестов всегда завершалась работа ВМ #2, но я полагаю, что это чистая случайность.
[ Замечание: ВМ #2 требуется всего 3 секунды для завершения работы, в то время как ВМ #1 и ВМ #3 требуют около 10 секунд; это может быть причиной того, что завершается работа всегда ВМ #2. Но давайте оставим это пока что; я всё равно не могу объяснить это и все еще считаю, что это случайность, что завершает работу только ВМ #2. ]
Затем я изменил указанный выше фрагмент следующим образом:
( printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/1.sock ) &
( printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/2.sock ) &
( printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/3.sock ) &
Конечно, эта версия также производит странный вывод, но в остальном работает; она надежно завершает работу всех трёх виртуальных машин.
Хотя мне радостно иметь рабочее решение, я хотел бы понять суть вопроса. После того, как я снова просмотрел соответствующие части руководства по bash, я полагаю, что обе версии должны завершать работу всех виртуальных машин, но это не так. Почему вторая версия работает, а первая — нет?
Конечно, я прочитал несколько подобных вопросов на этом сайте и в других местах, которые касаются выполнения команд или пайпов в фоновом режиме. Из этого исследования у меня сложилось впечатление, что обе версии должны работать. Некоторые ответы также предлагали переместить &
внутрь фигурных скобок, вот так:
(printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/1.sock &)
Но я еще не пробовал это, потому что сначала хотел бы понять разницу между первой и второй версией, показанными выше.
Ответ или решение
Понимание пайпинга в сокетах UNIX в фоновом режиме
В данной статье мы рассмотрим причину, по которой одна версия вашего скрипта работает, а другая — нет, когда дело касается выполнения команд в фоновом режиме с использованием сокетов UNIX для управления виртуальными машинами, запущенными через QEMU/KVM на Debian 12.
Контекст задачи
Вы имеете три виртуальные машины, каждая из которых связана с QEMU-мониторами через сокеты, расположенные по путям /vm/1.sock
, /vm/2.sock
и /vm/3.sock
. Вы используете socat
для отправки команды system_powerdown
в каждый из этих сокетов для корректного завершения работы виртуальных машин.
Ваши исходные варианты были следующими:
Первый вариант:
{ printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/1.sock; } &
{ printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/2.sock; } &
{ printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/3.sock; } &
Второй вариант:
( printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/1.sock ) &
( printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/2.sock ) &
( printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/3.sock ) &
Причины различий в поведении
Использование фигурных скобок
Первый вариант использует фигурные скобки { ... }
, которые создают блок команд и выполняют их в текущем контексте, а не в новом. Это означает, что любой выходной поток и состояние процессов внутри этого блока остаются в основном потоке выполнения. Поскольку команды выполняются асинхронно, вы не можете гарантировать их индивидуальное завершение — результат выполнения может быть непредсказуемым, особенно если один из процессов занимает больше времени, чем другие, как в вашем случае со временем завершения работы виртуальных машин.
Вывод всех процессов будет смешан и может привести к тому, что некоторые процессы просто не успеют завершиться корректно, если они не владеют необходимыми ресурсами, такими как доступ к сокету.
Использование круглых скобок
Во втором варианте вы используете круглые скобки ( ... )
, что создает новый подшельф с отдельным контекстом. Каждый процесс, запущенный в круглых скобках, получает собственный выходной поток, и они могут выполняться одновременно без влияния друг на друга. Это позволяет каждому socat
корректно взаимодействовать со своими сокетами без риска влияния процессов друг на друга.
Каждый из этих процессов будет независимым, что позволяет вам надежно завершить все три виртуальные машины, несмотря на различия во времени отклика. Это и объясняет, почему этот вариант работает, тогда как первый — нет.
Рекомендации
-
Использование подшельфов: Всегда, когда вы выполняете асинхронные команды, которые должны работать независимо, используйте круглые скобки для создания подшельфов.
-
Логирование вывода: Чтобы упростить отладку и наблюдение, вы можете перенаправить вывод каждого из соусов в файл:
( printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/1.sock ) >> vm1.log 2>&1 & ( printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/2.sock ) >> vm2.log 2>&1 & ( printf "%s\n" 'system_powerdown' | socat - unix-connect:/vm/3.sock ) >> vm3.log 2>&1 &
-
Ожидание завершения: Если вы хотите, чтобы основной скрипт ждал завершения всех фоновых процессов, вы можете использовать команду
wait
, чтобы гарантировать корректную синхронизацию.
Заключение
Понимание различий в выполнении команд в фоновом режиме в Unix-подобных системах имеет огромное значение для построения надежных автоматизированных сценариев. Правильное использование подшельфов и управление потоками вывода помогут избежать проблем, связанных с неожиданным поведением при работе с асинхронными задачами. Надеюсь, это объяснение помогло вам лучше понять, как добиться желаемого результата.