Надежно убить процесс сна после сигнала USR1 - PullRequest
5 голосов
/ 14 апреля 2020

Я пишу сценарий оболочки, который периодически выполняет задачу и получает сигнал USR1 от другого процесса.

Структура скрипта похожа на этот ответ :

#!/bin/bash

trap 'echo "doing some work"' SIGUSR1

while :
do
    sleep 10 && echo "doing some work" &
    wait $!
done

Однако, у этого скрипта есть проблема, что процесс сна продолжается в фоновом режиме и только умирает на его тайм-аут. (обратите внимание, что при получении USR1 во время ожидания $ !, процесс ожидания задерживается на свое обычное время ожидания, но эхо periodi c действительно отменяется.) Например, вы можете увидеть количество процессов ожидания на вашем компьютере, используя pkill -0 -c sleep .

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

#!/bin/bash

pid=
trap '[[ $pid ]] && kill $pid; echo "doing some work"' SIGUSR1

while :
do
    sleep 10 && echo "doing some work" &
    pid=$!
    wait $pid
    pid=
done

Однако этот сценарий имеет состояние гонки, если мы спамим Сигнал USR1 быстро, например, с:

pkill -USR1 trap-test.sh; pkill -USR1 trap-test.sh

, затем он попытается уничтожить уже убитый PID и распечатать ошибку. Не говоря уже о том, что мне не нравится этот код.

Есть ли лучший способ надежного уничтожения разветвленного процесса при прерывании? Или альтернативная структура для достижения той же функциональности?

Ответы [ 2 ]

5 голосов
/ 14 апреля 2020

Ни один из ваших сценариев не завершает sleep, и вы делаете его более запутанным, отправляя USR1 с помощью pkill. Поскольку фоновое задание является форком переднего плана, они имеют одно и то же имя (trap-test.sh); так что pkill соответствует и сигнализирует обоим. Это в неопределенном порядке убивает фоновый процесс (оставляя sleep живым, объяснено ниже) и запускает ловушку на переднем плане, отсюда и условие гонки.

Кроме того, в приведенных вами примерах фоновое задание всегда просто sleep x, но в вашем скрипте это sleep 10 && echo 'doing some work'; который требует, чтобы раздвоенный подоболочек ожидал завершения sleep и выполнял условно echo. Сравните эти два:

$ sleep 10 &
[1] 9401
$ pstree 9401
sleep
$
$ sleep 10 && echo foo &
[2] 9410
$ pstree 9410
bash───sleep

Итак, давайте начнем с нуля и воспроизведем основную проблему в терминале.

$ set +m
$ sleep 100 && echo 'doing some work' &
[1] 9923
$ pstree -pg $$
bash(9871,9871)─┬─bash(9923,9871)───sleep(9924,9871)
                └─pstree(9927,9871)
$ kill $!
$ pgrep sleep
9924
$ pkill -e sleep
sleep killed (pid 9924)

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

Уничтожение фонового задания не убило sleep, мне нужно было завершить его вручную. Это произошло из-за того, что сигнал, отправленный процессу, не передается автоматически дочерним элементам его цели; то есть sleep вообще не получил сигнал TERM.

Чтобы уничтожить sleep, а также подоболочку, мне нужно поместить фоновое задание в в отдельную группу процессов - который требует включения элементов управления заданиями, в противном случае все задания помещаются в группу процессов основной оболочки, как показано в выводе pstree выше, и отправляют ему сигнал TERM, как показано ниже.

$ set -m
$ sleep 100 && echo 'doing some work' &
[1] 10058
$ pstree -pg $$
bash(9871,9871)─┬─bash(10058,</code><b>10058</b><code>)───sleep(10059,</code><b>10058</b><code>)
                └─pstree(10067,10067)
$ kill -- </code><b>-$!</b><code>
$
[1]+  Terminated              sleep 100 && echo 'doing some work'
$ pgrep sleep
$

С некоторой доработкой и адаптацией этой концепции ваш сценарий будет выглядеть так:

#!/bin/bash -
set -m

usr1_handler() {
  kill -- -$!
  echo 'doing some work'
}

do_something() {
  trap '' USR1
  sleep 10 && echo 'doing some work'
}

trap usr1_handler USR1 EXIT

echo "my PID is $$"

while true; do
  do_something &
  wait
done

Это выведет my PID is xxx (где xxx - PID процесса переднего плана) и начнет цикл. Отправка сигнала USR1 на xxx (т. Е. kill -USR1 xxx) вызовет прерывание и вызовет завершение фонового процесса и его дочерних элементов. Таким образом, wait вернется и l oop продолжится.

Если вы используете pkill, вместо этого он все равно будет работать, так как фоновый процесс игнорирует USR1.

Для получения дополнительной информации см .:

0 голосов
/ 15 апреля 2020

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

TrapQuit вызывается на SIGUSR1 или других полученных сигналах выхода (включая CTRL + C). Вы можете добавить любую обработку, необходимую в TrapQuit, или вызвать ее при обычном завершении работы скрипта с кодом выхода.

# Kill process and children bash 3.2+ implementation

# BusyBox compatible version
function IsInteger {
    local value="${1}"

    #if [[ $value =~ ^[0-9]+$ ]]; then
    expr "$value" : "^[0-9]\+$" > /dev/null 2>&1
    if [  $? -eq 0 ]; then
        echo 1
    else
        echo 0
    fi
}

# Portable child (and grandchild) kill function tested under Linux, BSD, MacOS X, MSYS and cygwin
function KillChilds {
    local pid="${1}" # Parent pid to kill childs
    local self="${2:-false}" # Should parent be killed too ?

    # Paranoid checks, we can safely assume that $pid should not be 0 nor 1
    if [ $(IsInteger "$pid") -eq 0 ] || [ "$pid" == "" ] || [ "$pid" == "0" ] || [ "$pid" == "1" ]; then
        echo "CRITICAL: Bogus pid given [$pid]."
        return 1
    fi

    if kill -0 "$pid" > /dev/null 2>&1; then
        # Warning: pgrep is not native on cygwin, must be installed via procps package
        if children="$(pgrep -P "$pid")"; then
            if [[ "$pid" == *"$children"* ]]; then
                echo "CRITICAL: Bogus pgrep implementation."
                children="${children/$pid/}"
            fi
            for child in $children; do
                KillChilds "$child" true
            done
        fi
    fi

    # Try to kill nicely, if not, wait 15 seconds to let Trap actions happen before killing
    if [ "$self" == true ]; then
        # We need to check for pid again because it may have disappeared after recursive function call
        if kill -0 "$pid" > /dev/null 2>&1; then
            kill -s TERM "$pid"
            if [ $? != 0 ]; then
                sleep 15
                kill -9 "$pid"
                if [ $? != 0 ]; then
                    return 1
                fi
            else
                return 0
            fi
        else
            return 0
        fi
    else
        return 0
    fi
}

function TrapQuit {
    local exitcode="${1:-0}"

    KillChilds $SCRIPT_PID > /dev/null 2>&1
    exit $exitcode
}

# Launch TrapQuit on USR1 / other signals

trap TrapQuit USR1 QUIT INT EXIT
...