launchd: сон в обработчике управляемого сигнала GCD

Я сталкиваюсь со странной ситуацией в управляемом демоне launchd, когда пытаюсь заснуть в обработчике SIGTERM, который управляется с помощью Grand Central Dispatch, как описано здесь.
Все работает нормально, и я получаю обработчик сигнала SIGTERM до получения SIGKILL, когда я не сплю в обработчике SIGTERM. Но как только я сплю - даже на очень короткое время, как usleep(1); - Я вообще не получаю обработчик SIGTERM, но вместо этого мой демон мгновенно запускается SIGKILL.

Кстати, я использую EnableTransactions в моем файле plist и надлежащем vproc_transaction_begin(3)/vproc_transaction_end(3) код, как описано здесь.

Не спать в обработчике SIGTERM - это не вариант для меня, потому что мне нужно опросить информацию о моих "клиентских процессах", чтобы узнать, сохранено ли это для завершения работы демона или нет.

Мне кажется, что существует какой-то флаг компилятора, отвечающий за непосредственный прием SIGKILL (а не ожидаемого SIGTERM), как только я немного сплю в обработчике сигналов, потому что, когда я сплю, я не вижу никаких выходов моего обработчика SIGTERM совсем. Я, однако, ожидал увидеть отладочные распечатки вплоть до вызова сна, но это не так.

Вот мой файл plist:

  <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
            <key>Label</key>
            <string>org.example.myTestD</string>
            <key>ProgramArguments</key>
            <array>
                    <string>/usr/sbin/myTestD</string>
            </array>

            <key>RunAtLoad</key>
            <true/>

            <key>KeepAlive</key>
            <true/>

            <key>ExitTimeOut</key>
            <integer>0</integer>

            <key>EnableTransactions</key>
            <true/>
    </dict>
    </plist>

А вот и мой обработчик SIGTERM. Обратите внимание, что я вижу какие-либо выходные данные, как только добавляю usleep(1); линия.

static void mySignalHandler(int sigraised)
{
    int fd = open("/tmp/myTestD.log", O_WRONLY | O_CREAT | O_APPEND, 0777);
    if (fd <= 0) return;

    dprintf(fd, "%s(): signal received = %d, sleeping some time ...\n", __func__, sigraised);
    usleep(1);
    dprintf(fd, "%s(): ... done\n", __func__);

    dprintf(fd, "%s(): EXITING\n", __func__);
    close(fd);

    // transactionHandle is global variable assigned in daemon main
    if (transactionHandle) vproc_transaction_end(NULL, transactionHandle);

    exit(0);
}

Большое спасибо за любые подсказки / ответы!

Крис

1 ответ

Я думаю, суть вашей проблемы в том, что у вас есть это в списке:

        <key>ExitTimeOut</key>
        <integer>0</integer>

Страница руководства для launchd.plist говорит:

ExitTimeOut <целое число>

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

Немного поэкспериментировав, кажется, что этот текст не точный. Опытным путем я заметил, что если это значение установлено в 0, Я понимаю поведение, которое вы описываете (где процесс KILLсразу после получения TERM, независимо от каких-либо невыполненных объявленных транзакций.) Если я изменю это значение на какое-то произвольное большее число, например, скажем, 60, я наблюдаю TERM вызывается обработчик и имеет возможность сделать очистку перед выходом.

Не совсем понятно, используете ли вы классическую обработку сигналов или GCD, так как ссылка, которую вы разместили, описывает и то и другое, но если вы используете классическую обработку сигналов UNIX, то я должен также упомянуть, что вы вызывали функции в своем обработчике сигналов, которые отсутствуют в списке функций, которые можно вызывать в обработчиках сигналов (dprintf а также usleep нет в списке.) Но более вероятно, что вы используете GCD.

Еще одна вещь, которая приходит в голову мне, что если вы использовали vproc_transaction_begin/end чтобы заключить в скобки все рабочие элементы, которые вы ожидаете, в вашем обработчике, тогда вы получите это поведение "бесплатно", вообще не нуждаясь в обработчике сигнала. Вполне возможно, что есть некоторая централизованная работа по очистке, которую нужно выполнять независимо от обычных рабочих элементов, но если это просто ожидание завершения других асинхронных задач, это может быть еще проще.

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

#import <Foundation/Foundation.h>

#import <vproc.h>

static void SignalHandler(int sigraised);
static void FakeWork();
static void Log(NSString* str);

int64_t outstandingTransactions;
dispatch_source_t fakeWorkGeneratorTimer;

int main(int argc, const char * argv[])
{
    @autoreleasepool
    {
        // Set up GCD handler for SIGTERM
        dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGTERM, 0, dispatch_get_global_queue(0, 0));
        dispatch_source_set_event_handler(source, ^{
            SignalHandler(SIGTERM);
        });
        dispatch_resume(source);

        // Tell the standard signal handling mechanism to ignore SIGTERM
        struct sigaction action = { 0 };
        action.sa_handler = SIG_IGN;
        sigaction(SIGTERM, &action, NULL);

        // Set up a 10Hz timer to generate "fake work" events
        fakeWorkGeneratorTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
        dispatch_source_set_timer(fakeWorkGeneratorTimer, DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC, 0.05 * NSEC_PER_SEC);
        dispatch_source_set_event_handler(fakeWorkGeneratorTimer, ^{
            // Dont add an event *every* time...
            if (arc4random_uniform(10) >= 5) dispatch_async(dispatch_get_global_queue(0, 0), ^{ FakeWork(); });
        });
        dispatch_resume(fakeWorkGeneratorTimer);

        // Start the run loop
        while (1)
        {
            // The runloop also listens for SIGTERM and will return from here, so I'm just sending it right back in.
            [[NSRunLoop currentRunLoop] run];
        }
    }

    return 0;
}

static void SignalHandler(int sigraised)
{
    // Open a transaction so that we dont get killed before getting to the end of this handler
    vproc_transaction_t transaction = vproc_transaction_begin(NULL);

    // Turn off the fake work generator
    dispatch_suspend(fakeWorkGeneratorTimer);

    Log([NSString stringWithFormat: @"%s(): signal received = %d\n", __func__, sigraised]);

    int64_t transCount = outstandingTransactions;
    while (transCount > 0)
    {
        Log([NSString stringWithFormat: @"%s(): %lld transactions outstanding. Waiting...\n", __func__, transCount]);
        usleep(USEC_PER_SEC / 4);
        transCount = outstandingTransactions;
    }

    Log([NSString stringWithFormat: @"%s(): EXITING\n", __func__]);

    // Close the transaction
    vproc_transaction_end(NULL, transaction);

    exit(0);
}

static void FakeWork()
{
    static int64_t workUnitNumber;

    const NSTimeInterval minWorkDuration = 1.0 / 100.0; // 10ms
    const NSTimeInterval maxWorkDuration = 4.0; // 4s

    OSAtomicIncrement64Barrier(&outstandingTransactions);
    int64_t serialNum = OSAtomicIncrement64Barrier(&workUnitNumber);
    vproc_transaction_t transaction = vproc_transaction_begin(NULL);

    Log([NSString stringWithFormat: @"Starting work unit: %@", @(serialNum)]);

    // Set up a callback some random time later.
    int64_t taskDuration = arc4random_uniform(NSEC_PER_SEC * (maxWorkDuration - minWorkDuration)) + (minWorkDuration * NSEC_PER_SEC);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, taskDuration), dispatch_get_global_queue(0, 0), ^{
        Log([NSString stringWithFormat: @"Finishing work unit: %@", @(serialNum)]);
        vproc_transaction_end(NULL, transaction);
        OSAtomicDecrement64Barrier(&outstandingTransactions);
    });
}

static void Log(NSString* str)
{
    static NSObject* lockObj = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        lockObj = [NSObject new];
    });

    @synchronized(lockObj)
    {
        int fd = open("/tmp/myTestD.log", O_WRONLY | O_CREAT | O_APPEND, 0777);
        if (fd <= 0) return;
        dprintf(fd, "%s\n", str.UTF8String);
        close(fd);
    }
}

И плист

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Label</key>
        <string>DaemonDeathTest</string>
        <key>ProgramArguments</key>
        <array>
            <string>/tmp/bin/DaemonDeathTest</string>
        </array>

        <key>RunAtLoad</key>
        <true/>

        <key>KeepAlive</key>
        <true/>

        <key>ExitTimeOut</key>
        <integer>60</integer>

        <key>EnableTransactions</key>
        <true/>
    </dict>
</plist>
Другие вопросы по тегам