Observing Process Lifetimes Without Polling

This technote describes how to track the lifetime of a process on the system. There are a variety of different ways to do this, and the best approach depends on your specific circumstances. This technote describes the common approaches and the situations in which they are appropriate.

You should read this technote if you are developing Mac OS X software that uses one or more cooperating processes. Specifically, the technote covers all levels of the Mac OS X software stack (from BSD to Cocoa).





Introduction

After programming Mac OS X for a while, you will inevitably come across situations where it's necessary to create a suite of cooperating processes. For example:

  • If you're writing an application, you may want to factor some code out into a separate helper process. Perhaps you want to put some unreliable code in a separate process so that, if it crashes, it doesn't crash your main application. Or perhaps you want to access certain APIs that aren't thread safe without locking up your application's user interface.

  • You may be writing a suite of cooperating applications. Perhaps you're writing a word processor and want to call on the services of a separate equation editor.

  • If you're writing a daemon, you may need to interface with a variety of agent programs that have access to per-user state.

Once you have multiple processes you inevitably run into the issue of process lifetime: that is, one process needs to know whether another process is running. This technote explains various techniques that you can use to be notified when a process launches or terminates. It is split into two main sections. Observing Processes That You Started shows how to monitor a process that you launched, while Observing Arbitrary Processes shows how to monitor a process that you didn't launch. Finally, On Process Serial Numbers contains some important information about the process serial number based APIs discussed in this technote.

But first, let's start with a discussion of an alternative approach that offers a number of key advantages.

IMPORTANT: All of the techniques discussed here notify you when things change. It is possible to get the same information by polling the process list, but polling is generally a bad idea (it consumes CPU time, reduces battery life, increase the working set of your process, and increases your latency in responding to an event).

Back to Top

The Service-Oriented Alternative

One of the most common reasons for monitoring the lifecycle of a process is that the process provides some service to you. For example, consider a movie transcoding application that sublaunches a worker process to do the actual transcoding. The main application needs to monitor the state of the worker process so that it can relaunch it if it quits unexpectedly.

You can avoid this requirement by rethinking your approach. Rather than explicitly managing the state of your helper process, reimagine it as a service that your application calls upon. You can then use launchd to manage that service; it will take care of all the nitty-gritty details of launching and terminating the process that provides that service.

A full discussion of this service-oriented approach is outside the scope of this technote. For more information you should read up on launchd. A good place to start is the launchd page on Mac OS Forge. I specifically recommend that you watch the Google TechTalk that's referenced by that page.

Back to Top

Observing Processes That You Started

There are many different techniques for monitoring the lifetime of a process that you started. Each technique has a number of pros and cons. Read the following sections to determine which is most appropriate for your circumstances.

NSTask

NSTask makes it easy to launch a helper process and wait for it to terminate. You can wait synchronously (using -[NSTask waitUntilExit]) or install a notification callback for the NSTaskDidTerminateNotification notification. Listing 1 shows the synchronous approach and Listing 2 show the asynchronous one.

Listing 1: Using NSTask synchronously

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

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

Listing 2: Using NSTask asynchronously

- (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;
}

Back to Top

Application Died Events

If you launch an application using a process serial number based API, you can learn about its termination by registering for the kAEApplicationDied Apple event.

IMPORTANT: This event is only delivered for applications that you launched.

Listing 3 shows how to register for and handle an application died event.

Listing 3: Using application died events

- (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;
}

IMPORTANT: Application died events are based on serial numbers, a fact which has a number of important consequences. See On Process Serial Numbers for details.

Back to Top

The UNIX Way

Mac OS X's BSD subsystem has two fundamental APIs for starting new processes:

  • fork and exec — This technique has its origins in the first UNIX systems. The fork routine creates a new process that is an exact clone of the current process, and the exec routine (which is actually a family of routines, all based on the execve routine) causes the current process to start running a new executable.

  • posix_spawn — This API acts like a combination of fork and exec. It was introduced in Mac OS X 10.5.

In both cases the resulting process is a child of the current process. There are two traditional UNIX ways to learn about the death of a child process:

  • synchronously, using one of the family of wait routines (typically waitpid)

  • asynchronously, by way of the SIGCHLD signal

Waiting synchronously is appropriate in many situations. For example, if the parent process can make no progress until the child is done, it's reasonable to wait synchronously. Listing 4 shows an example of how to fork, then exec, then wait.

Listing 4: Fork, exec, wait

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) );
    }
}

On the other hand there are circumstances where waiting synchronously is a really bad idea. For example, if you're running on the main thread of an application and the child process might operate for an extended period of time, you don't want to lock up your application's user interface waiting for the child to quit. In cases like this, you can wait asynchronously by listening for the SIGCHLD signal.

IMPORTANT: If you use the SIGCHLD technique you must still reap the child by calling a wait routine. Failure to do so will result in a zombie process.

Listening for a signal can be tricky because of the wacky execution environment associated with signal handlers. Specifically, if you install a signal handler (using signal or sigaction), you must be very careful about what you do in that handler. Very few functions are safe to call from a signal handler. For example, it is not safe to allocate memory using malloc!

The functions that are safe from a signal handler (the async-signal safe functions) are listed on the sigaction man page.

In most cases you must take extra steps to redirect incoming signals to a more sensible environment. There are two standard techniques for doing this:

  • sockets — In this technique you create a UNIX domain socket pair and add one end to your runloop using CFSocket. When a signal arrives, the signal handler writes a dummy message to the socket. This wakes up the runloop and allows you to process the signal in a safe environment.

    To see this technique in action, look at the InstallSignalToSocket routine in Sample Code 'CFLocalServer'.

  • kqueues — The kqueue mechanism allows you to listen for a signal without installing any signal handlers. So you can create a kqueue, instruct it to listen for SIGCHLD signals, and then wrap it up in a CFFileDescriptor and add it to your runloop. When a signal arrives, the callback routine associated with the CFFileDescriptor runs and you can process the signal in a safe environment.

    To see this technique in action, look at the InstallHandleSIGTERMFromRunLoop routine in Sample Code 'PreLoginAgents'.

IMPORTANT: The kqueue technique requires Mac OS X 10.5 or later because it uses CFFileDescriptor.

Back to Top

UNIX Alternatives

There are numerous pitfalls associated with handing the SIGCHLD signal. The previous section described the deepest one, but there are others. Using SIGCHLD is particularly tricky when you're writing library code, because the disposition of SIGCHLD is controlled by the main program itself and your library code can't require that it be set one way or another.

There are various techniques to avoid all of this messing around with SIGCHLD. One such technique is to create a UNIX domain socket pair, and organize for the child to have the only descriptor that references one end, and for the parent to have a descriptor for the other end. When the child terminates, the system closes the child's descriptor and that causes the other end of the socket to indicate an end of file (that is, it becomes readable but, when you read from it, the read routine returns 0). When the parent detects this end of file condition it can reap the child.

Listing 5 shows an example of this technique.

Listing 5: Using a socket to detect child termination

- (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!
}

Back to Top

Observing Arbitrary Processes

There are fewer options available if you want monitor the status of a process that you did not launch. However, the facilities that are available should be enough to meet most needs. Again, the right choice of API depends on your circumstances. Read the following sections to understand which API is appropriate and when.

NSWorkspace

NSWorkspace provides a very easy way for you to learn about applications being launched and quit. To register for these notifications you must:

  1. get NSWorkspace's custom notification center by calling -[NSWorkspace notificationCenter]

  2. add observers for the NSWorkspaceDidLaunchApplicationNotification and NSWorkspaceDidTerminateApplicationNotification events

When you get a notification, the user info dictionary contains information about the affected process. The keys for that dictionary are listed in NSWorkspace.h, starting with "NSApplicationPath".

Listing 6 shows an example of how to use NSWorkspace to learn application launch and termination.

Listing 6: Using NSWorkspace to learn about application launch and termination

- (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!
}

IMPORTANT: NSWorkspace is a process serial number based API, a fact which has a number of important consequences. See On Process Serial Numbers for details.

Back to Top

Carbon Event Manager

Carbon Event Manager sends a number of events related to process management. Specifically, the kEventAppLaunched event is sent when an application is launched and the kEventAppTerminated when an application terminates. You register for these events as you would any other Carbon event. Listing 7 shows an example of this.

When your event handler is called the kEventParamProcessID parameter will contain the ProcesSerialNumber of the affected process. You can call Process Manager to get more information about the process.

IMPORTANT: By the time your application has received the kEventAppTerminated event, the terminating application has probably quit. Thus you can't get information about that application using GetProcessInformation. If you need information about terminating applications, you will have to cache it in advance.

Listing 7: Using Carbon events to learn about application launch and termination

- (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;
}

IMPORTANT: These Carbon events are based on process serial numbers, a fact which has a number of important consequences. See On Process Serial Numbers for details.

Back to Top

kqueues

Both NSWorkspace and Carbon events only work within a single GUI login context. If you're writing a program that does not run within a GUI login context (a daemon perhaps), or you need to monitor a process in a different context from the one in which you're running, you will need to consider alternatives.

One such alternative is the kqueue NOTE_EXIT event. You can use this to detect when a process quits, regardless of what context it's running in. Unlike NSWorkspace and Carbon events, you must specify exactly which process to monitor; there is no way to be notified when any process terminates.

Listing 8 is a simplistic example of how you can use kqueues to watch for the termination of a specific process.

Listing 8: Using kqueues to monitor a specific process

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!
}

Back to Top

On Process Serial Numbers

Mac OS X has a number of high-level APIs for process management that work in terms of process serial numbers (of type ProcessSerialNumber). These include Launch Services, Process Manager, and NSWorkspace. These APIs all share three important features:

  • They work in the context of a single GUI login session. For example, if you use NSWorkspace to observe applications being launched and terminated, you will only be notified about applications running in the same GUI login session.

  • They only see processes that connect to the window server. For example, if you use NSTask to run a BSD command line tool like find, an NSWorkspace-based observer will not be notified of the tool's launch or termination.

  • They are, in general, not usable by processes that run outside of a GUI login context (for example, daemons).

See Technical Note TN2083, 'Daemons and Agents' for more information about execution contexts and their effect on high-level APIs.

Back to Top

Further Reading

Document Revision History

Date Notes
2008-09-10 A major rewrite to eliminate use of deprecated APIs and to update the technote to reference the latest techniques.
2008-09-10 A major rewrite to eliminate use of deprecated APIs and to update the technote to reference the latest techniques.
2008-09-08 A major rewrite to eliminate use of deprecated APIs and to update the technote to reference the latest techniques.
2002-07-01 Shows a variety of methods to observe process lifetimes without polling.

Posted: 2008-09-10


Did this document help you?
Yes: Tell us what works for you.
It’s good, but: Report typos, inaccuracies, and so forth.
It wasn’t helpful: Tell us what would have helped.