Техническое примечание TN2050

Наблюдение времен жизни процесса без опроса

Этот technote описывает, как отследить время жизни процесса в системе. Существует множество различных способов сделать это, и лучший подход зависит от Ваших определенных обстоятельств. Этот technote описывает общие подходы и ситуации, в которых они являются надлежащими.

Необходимо считать этот technote при разработке программного обеспечения Mac OS X, использующего один или несколько процессов сотрудничества. В частности technote покрывает все уровни штабеля программного обеспечения Mac OS X (от BSD до Какао).

Введение
Альтернатива для обслуживания широкого круга запросов
Наблюдение процессов, которые Вы запустили
Наблюдение произвольных процессов
На порядковых номерах процесса
Дополнительные материалы для чтения
История версии документа

Введение

После программирования Mac OS X некоторое время, Вы неизбежно столкнетесь с ситуациями, где необходимо создать комплект сотрудничающих процессов. Например:

Как только у Вас есть многократные процессы, Вы неизбежно сталкиваетесь с проблемой времени жизни процесса: т.е. один процесс должен знать, работает ли другой процесс. Этот technote объясняет различные методы, которые можно использовать, чтобы быть уведомленными, когда процесс запускается или завершается. Это разделяется на два основных раздела. Наблюдение Процессов, Которые Вы Запустили, показывает, как контролировать процесс, который Вы запустили при Замечании, что Произвольные Процессы показывают, как контролировать процесс, который Вы не запускали. Наконец, На Порядковых номерах Процесса содержит некоторую важную информацию о порядковом номере процесса, базируемом APIs, обсужденный в этом technote.

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

Альтернатива для обслуживания широкого круга запросов

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

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

Полное обсуждение этого подхода для обслуживания широкого круга запросов выходит за рамки этого technote. Для получения дополнительной информации необходимо читать на launchd. Хорошее место для запуска является launchd страницей на Штамповочном прессе Mac OS. Я в частности рекомендую наблюдать Google TechTalk, на это ссылается та страница.

Наблюдение процессов, которые Вы запустили

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

NSTask

NSTask упрощает запускать процесс помощника и ожидать его для завершения. Можно ожидать синхронно (использование -[NSTask waitUntilExit]) или установите обратный вызов уведомления для NSTaskDidTerminateNotification уведомление. Перечисление 1 показывает, что синхронный подход и Перечисление 2 показывают асинхронное.

Перечисление 1  Используя NSTask синхронно

- (IBAction)testNSTaskSync:(id)sender
{
    NSTask *    syncTask;

    syncTask = [NSTask 
        launchedTaskWithLaunchPath:@"/bin/sleep" 
        arguments:[NSArray arrayWithObject:@"1"]
    ];
    [syncTask waitUntilExit];
}

Перечисление 2  Используя NSTask асинхронно

- (IBAction)testNSTaskAsync:(id)sender
{
    task = [[NSTask alloc] init]; 
    [task setLaunchPath:@"/bin/sleep"];
    [task setArguments:[NSArray arrayWithObject:@"1"]];

    [[NSNotificationCenter defaultCenter] 
        addObserver:self 
        selector:@selector(taskExited:) 
        name:NSTaskDidTerminateNotification 
        object:task
    ];

    [task launch];

    // Execution continues in -taskExited:, below.
}

- (void)taskExited:(NSNotification *)note
{
    // You've been notified!

    [[NSNotificationCenter defaultCenter] 
        removeObserver:self 
        name:NSTaskDidTerminateNotification 
        object:task
    ];
    [task release];
    task = nil;
}

Приложение умерло события

Если Вы запускаетесь, приложение с помощью порядкового номера процесса базировало API, можно узнать о его завершении путем регистрации для kAEApplicationDied Событие Apple.

Перечисление 3 показывает, как зарегистрироваться для и обработать умершее событие приложения.

Перечисление 3  Используя приложение умерло события

- (IBAction)testApplicationDied:(id)sender
{
    NSURL *     url;
    static BOOL sHaveInstalledAppDiedHandler;

    if ( ! sHaveInstalledAppDiedHandler ) {
        (void) AEInstallEventHandler(
            kCoreEventClass, 
            kAEApplicationDied, 
            (AEEventHandlerUPP) AppDiedHandler, 
            (SRefCon) self, 
            false
        );
        sHaveInstalledAppDiedHandler = YES;
    }

    url = [NSURL fileURLWithPath:@"/Applications/TextEdit.app"];
    (void) LSOpenCFURLRef( (CFURLRef) url, NULL);

    // Execution continues in AppDiedHandler, below.
}

static OSErr AppDiedHandler(
    const AppleEvent *  theAppleEvent, 
    AppleEvent *        reply, 
    SRefCon             handlerRefcon
)
{
    SInt32              errFromEvent;
    ProcessSerialNumber psn;
    DescType            junkType;
    Size                junkSize;

    (void) AEGetParamPtr(
        theAppleEvent, 
        keyErrorNumber, 
        typeSInt32, 
        &junkType, 
        &errFromEvent, 
        sizeof(errFromEvent), 
        &junkSize
    );
    (void) AEGetParamPtr(
        theAppleEvent, 
        keyProcessSerialNumber, 
        typeProcessSerialNumber, 
        &junkType, 
        &psn, 
        sizeof(psn), 
        &junkSize
    );

    // You've been notified!

    NSLog(
        @"died %lu.%lu %d", 
        (unsigned long) psn.highLongOfPSN, 
        (unsigned long) psn.lowLongOfPSN, 
        (int) errFromEvent
    );

    return noErr;
}

UNIX путь

Подсистема BSD Mac OS X имеет два фундаментальных APIs для запуска новых процессов:

  • ветвление и руководитель — Этот метод возникают в первых системах UNIX. fork подпрограмма создает новый процесс, который является точным клоном текущего процесса и исполнительной подпрограммой (который является фактически семьей подпрограмм, все на основе execve подпрограммы) заставляет текущий процесс начинать выполнять новую исполнимую программу.

  • posix_spawn — Этот API действует как комбинация fork и exec. Это было представлено в Mac OS X 10.5.

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

Ожидание синхронно является надлежащим во многих ситуациях. Например, если родительский процесс не может сделать успехи, пока дочерний элемент не сделан, разумно ожидать синхронно. Перечисление 4 показывает пример того, как разветвиться, затем руководитель, затем ожидайте.

  Ветвление перечисления 4, руководитель, ожидает

extern char **environ;

- (IBAction)testWaitPID:(id)sender
{
    pid_t       pid;
    char *      args[3] = { "/bin/sleep", "1", NULL };
    pid_t       waitResult;
    int         status;

    // I used fork/exec rather than posix_spawn because I would like this 
    // code to be compatible with 10.4.x.

    pid = fork();
    switch (pid) {
        case 0:
            // child
            (void) execve(args[0], args, environ);
            _exit(EXIT_FAILURE);
            break;
        case -1:
            // error
            break;
        default:
            // parent
            break;
    }
    if (pid >= 0) {
        do {
            waitResult = waitpid(pid, &status, 0);
        } while ( (waitResult == -1) && (errno == EINTR) );
    }
}

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

Прислушивание к сигналу может быть хитрым из-за дурацкой среды выполнения, связанной с сигнальными обработчиками. В частности при установке сигнального обработчика (использующий сигнал или sigaction), необходимо быть очень осторожны относительно того, что Вы делаете в том обработчике. Очень немного функций безопасно вызвать от сигнального обработчика. Например, не безопасно выделить использование памяти malloc!

Функции, которые безопасны от сигнального обработчика (асинхронно-сигнальные безопасные функции) перечислены на sigaction странице справочника.

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

  • сокеты — В этом методе Вы создаете пару сокета домена UNIX и добавляете один конец Вашему runloop, использующему CFSocket. Когда сигнал поступает, сигнальный обработчик пишет фиктивное сообщение в сокет. Это будит runloop и позволяет Вам обрабатывать сигнал в безопасной среде.

    Для наблюдения этого метода в действии посмотрите на InstallSignalToSocket подпрограмма в Примере кода 'CFLocalServer'.

  • kqueues — kqueue механизм позволяет Вам прислушиваться к сигналу, не устанавливая сигнальных обработчиков. Таким образом, можно создать kqueue, дайте ему команду прислушиваться SIGCHLD сигналы, и затем оборачивают его в CFFileDescriptor и добавляют его к Вашему runloop. Когда сигнал поступает, подпрограмма обратного вызова, связанная с выполнениями CFFileDescriptor, и можно обработать сигнал в безопасной среде.

    Для наблюдения этого метода в действии посмотрите на InstallHandleSIGTERMFromRunLoop подпрограмма в Примере кода 'PreLoginAgents'.

Альтернативы UNIX

Существуют многочисленные ловушки, связанные с вручением SIGCHLD сигнал. Предыдущий раздел описал самый глубокий, но существуют другие. Используя SIGCHLD когда Вы пишете код библиотеки, потому что расположение, особенно хитро SIGCHLD управляется самой основной программой, и Ваш код библиотеки не может потребовать, чтобы это было установлено так или иначе.

Существуют различные методы для предотвращения всего этого бездельничающего с SIGCHLD. Один такой метод должен создать пару сокета домена UNIX и организовать для дочернего элемента для имения единственного дескриптора, ссылающегося на один конец, и для родителя для имения дескриптора для другого конца. Когда дочерний элемент завершает, система закрывает дескриптор дочернего элемента, и это заставляет другой конец сокета указывать конец файла (т.е. это становится читаемым, но, когда Вы читаете из него, read подпрограмма возвращается 0). Когда родитель обнаруживает этот конец условия файла, это может пожинать дочерний элемент.

Перечисление 5 показывает пример этого метода.

Перечисление 5  Используя сокет для обнаружения дочернего завершения

- (IBAction)testSocketPair:(id)sender
{
    int                 fds[2];
    int                 remoteSocket;
    int                 localSocket;
    CFSocketContext     context = { 0, self, NULL, NULL, NULL };
    CFRunLoopSourceRef  rls;
    char *              args[3] = { "/bin/sleep", "1", NULL } ;

    // Create a socket pair and wrap the local end up in a CFSocket.

    (void) socketpair(AF_UNIX, SOCK_STREAM, 0, fds);

    remoteSocket = fds[0];
    localSocket  = fds[1];
    socket = CFSocketCreateWithNative(
        NULL, 
        localSocket, 
        kCFSocketDataCallBack, 
        SocketClosedSocketCallBack, 
        &context
    );
    CFSocketSetSocketFlags(
        socket, 
        kCFSocketAutomaticallyReenableReadCallBack | kCFSocketCloseOnInvalidate
    );

    // Add the CFSocket to our runloop.

    rls = CFSocketCreateRunLoopSource(NULL, socket, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
    CFRelease(rls);

    // fork and exec the child process.

    childPID = fork();
    switch (childPID) {
        case 0:
            // child
            (void) execve(args[0], args, environ);
            _exit(EXIT_FAILURE);
            break;
        case -1:
            // error
            break;
        default:
            // parent
            break;
    }

    // Close our reference to the remote socket. The only reference remaining 
    // is the one in the child. When that dies, the socket will become readable.

    (void) close(remoteSocket);

    // Execution continues in SocketClosedSocketCallBack, below.
}

static void SocketClosedSocketCallBack(
    CFSocketRef             s, 
    CFSocketCallBackType    type, 
    CFDataRef               address, 
    const void *            data, 
    void *                  info
)
{
    int             waitResult;
    int             status;

    // Reap the child.

    do {
        waitResult = waitpid( ((AppDelegate *) info)->childPID, &status, 0);
    } while ( (waitResult == -1) && (errno == EINTR) );

    // You've been notified!
}

Наблюдение произвольных процессов

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

NSWorkspace

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

  1. получите пользовательский центр уведомления NSWORKSPACE путем вызова -[NSWorkspace notificationCenter]

  2. добавьте наблюдателей для NSWorkspaceDidLaunchApplicationNotification и NSWorkspaceDidTerminateApplicationNotification события

Когда Вы получаете уведомление, пользовательский информационный словарь содержит информацию о затронутом процессе. Ключи для того словаря перечислены в NSWorkspace.h, начиная с «NSApplicationPath».

Перечисление 6 показывает пример того, как использовать NSWorkspace для изучения запуска приложения и завершения.

Перечисление 6  Используя NSWorkspace для приобретения знаний о запуске приложения и завершении

- (IBAction)testNSWorkspace:(id)sender
{
    NSNotificationCenter *  center;

    NSLog(@"-[AppDelegate testNSWorkspace:]");

    // Get the custom notification center.

    center = [[NSWorkspace sharedWorkspace] notificationCenter];

    // Install the notifications.

    [center addObserver:self 
        selector:@selector(appLaunched:) 
        name:NSWorkspaceDidLaunchApplicationNotification 
        object:nil
    ];
    [center addObserver:self 
        selector:@selector(appTerminated:) 
        name:NSWorkspaceDidTerminateApplicationNotification 
        object:nil
    ];

    // Execution continues in -appLaunched: and -appTerminated:, below.
}

- (void)appLaunched:(NSNotification *)note
{
    NSLog(@"launched %@\n", [[note userInfo] objectForKey:@"NSApplicationName"]);

    // You've been notified!
}

- (void)appTerminated:(NSNotification *)note
{
    NSLog(@"terminated %@\n", [[note userInfo] objectForKey:@"NSApplicationName"]);

    // You've been notified!
}

Менеджер событий углерода

Менеджер событий углерода отправляет много событий, связанных с управлением процессами. В частности, kEventAppLaunched когда приложение запускается и, событие отправляется kEventAppTerminated когда завершается приложение. Вы регистрируетесь для этих событий, поскольку Вы были бы любое другое событие Carbon. Перечисление 7 показывает пример этого.

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

  События Listing 7 Using Carbon для приобретения знаний о запуске приложения и завершении

- (IBAction)testCarbonEvents:(id)sender
{
    static EventHandlerRef sCarbonEventsRef = NULL;
    static const EventTypeSpec kEvents[] = {
        { kEventClassApplication, kEventAppLaunched },
        { kEventClassApplication, kEventAppTerminated }
    };

    if (sCarbonEventsRef == NULL) {
        (void) InstallEventHandler(
            GetApplicationEventTarget(),
            (EventHandlerUPP) CarbonEventHandler,
            GetEventTypeCount(kEvents),
            kEvents,
            self,
            &sCarbonEventsRef
        );
    }

    // Execution continues in CarbonEventHandler, below.
}

static OSStatus CarbonEventHandler(
    EventHandlerCallRef inHandlerCallRef, 
    EventRef            inEvent, 
    void *              inUserData
)
{
    ProcessSerialNumber psn;

    (void) GetEventParameter(
        inEvent, 
        kEventParamProcessID, 
        typeProcessSerialNumber, 
        NULL, 
        sizeof(psn), 
        NULL, 
        &psn
    );
    switch ( GetEventKind(inEvent) ) {
        case kEventAppLaunched:
            NSLog(
                @"launched %u.%u", 
                (unsigned int) psn.highLongOfPSN, 
                (unsigned int) psn.lowLongOfPSN
            );
            // You've been notified!
            break;
        case kEventAppTerminated:
            NSLog(
                @"terminated %u.%u", 
                (unsigned int) psn.highLongOfPSN, 
                (unsigned int) psn.lowLongOfPSN
            );
            // You've been notified!
            break;
        default:
            assert(false);
    }
    return noErr;
}

kqueues

Оба события NSWorkspace и Carbon только работают в единственном контексте входа в систему GUI. Если Вы запишете программу, не работающую в контексте входа в систему GUI (демон, возможно), или необходимо контролировать процесс в различном контексте от того, в котором Вы работаете, то необходимо будет рассмотреть альтернативы.

Одна такая альтернатива является kqueue NOTE_EXIT событие. Можно использовать это для обнаружения, когда процесс выходит, независимо от того, в каком контексте он работает. В отличие от событий NSWorkspace и Carbon, необходимо указать точно который процесс контролировать; нет никакого пути, который будет уведомлен, когда завершается любой процесс.

Перечисление 8 является упрощенным примером того, как можно использовать kqueues для наблюдения за завершением определенного процесса.

Перечисление 8  Используя kqueues для контроля определенного процесса

static pid_t gTargetPID = -1;
    // We assume that some other code sets up gTargetPID.

- (IBAction)testNoteExit:(id)sender
{
    FILE *                  f;
    int                     kq;
    struct kevent           changes;
    CFFileDescriptorContext context = { 0, self, NULL, NULL, NULL };
    CFRunLoopSourceRef      rls;

    // Create the kqueue and set it up to watch for SIGCHLD. Use the 
    // new-in-10.5 EV_RECEIPT flag to ensure that we get what we expect.

    kq = kqueue();

    EV_SET(&changes, gTargetPID, EVFILT_PROC, EV_ADD | EV_RECEIPT, NOTE_EXIT, 0, NULL);
    (void) kevent(kq, &changes, 1, &changes, 1, NULL);

    // Wrap the kqueue in a CFFileDescriptor (new in Mac OS X 10.5!). Then 
    // create a run-loop source from the CFFileDescriptor and add that to the 
    // runloop.

    noteExitKQueueRef = CFFileDescriptorCreate(NULL, kq, true, NoteExitKQueueCallback, &context);
    rls = CFFileDescriptorCreateRunLoopSource(NULL, noteExitKQueueRef, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
    CFRelease(rls);

    CFFileDescriptorEnableCallBacks(noteExitKQueueRef, kCFFileDescriptorReadCallBack);

    // Execution continues in NoteExitKQueueCallback, below.
}

static void NoteExitKQueueCallback(
    CFFileDescriptorRef f, 
    CFOptionFlags       callBackTypes, 
    void *              info
)
{
    struct kevent   event;

    (void) kevent( CFFileDescriptorGetNativeDescriptor(f), NULL, 0, &event, 1, NULL);

    NSLog(@"terminated %d", (int) (pid_t) event.ident);

    // You've been notified!
}

На порядковых номерах процесса

Mac OS X имеет много высокоуровневых APIs для управления процессами, работающий с точки зрения порядковых номеров процесса (типа ProcessSerialNumber). Они включают Launch Services, Диспетчер процессов и NSWorkspace. Этот APIs вся доля три важных функции:

Посмотрите Техническое примечание TN2083, 'Демоны и Агенты' для получения дополнительной информации о контекстах выполнения и их эффекте на высокоуровневый APIs.

Дополнительные материалы для чтения



История версии документа


ДатаПримечания
10.09.2008

Основная перезапись, чтобы устранить использование осуждаемого APIs и обновить technote для ссылки на последние методы.

 

Основная перезапись, чтобы устранить использование осуждаемого APIs и обновить technote для ссылки на последние методы.

08.09.2008

Основная перезапись, чтобы устранить использование осуждаемого APIs и обновить technote для ссылки на последние методы.

01.07.2002

Новый документ, показывающий множество методов для наблюдения времен жизни процесса без опроса.