Mac OS X Internals: A Systems Approach

9.16. Notifications

In simple terms, a notification is a message sent by one entity to another to inform the latter of an event's occurrence. The following are some general aspects of notifications and notification mechanisms in the context of Mac OS X.

  • Notifications can be intraprocess, interprocess, or even between the kernel and a user process. A single notification can also be broadcast to multiple parties. Mac OS X provides several user-level and kernel-level notification mechanisms.

  • There are several ways notification messages can be transmitted, for example, using Mach IPC, signals, shared memory, and so on. A single notification API may offer multiple delivery mechanisms.

  • In a typical interprocess notification mechanism, one party programmatically registers a request with a (usually) centralized notification broker. The registration includes details of the types of events the caller is interested in knowing about. Mac OS X frameworks refer to such an interested party as an observer.

  • A notification may be generated because an event of interest occurred, or it may be programmatically posted (fabricated) by a poster. Depending on program logic, the observer and poster may be the same process.

Let us look at some important notification mechanisms in Mac OS X. Some of these are general-purpose notification mechanisms, allowing programs to exchange arbitrary information, whereas others are special-purpose mechanisms that support only specific types of notifications.

9.16.1. Foundation Notifications

The Foundation framework provides the NSNotificationCenter and NSDistributedNotificationCenter classes for intraprocess and interprocess notifications, respectively. Both these classes use the abstraction of a notification brokerthe notification center. The default notification center for either class can be accessed through the defaultCenter class method.

Each process has a default process-local NSNotificationCenter object that is automatically created. The class provides instance methods for adding observers, removing observers, and posting notifications. A single notification is represented as an NSNotification object, which consists of a name, an object, and an optional dictionary that can contain arbitrary associated data.

The NSDistributedNotificationCenter class is similar in concept and provides similar methods. Since its scope is system-wide, it requires a different brokerthe distributed notification daemon (/usr/sbin/distnoted) provides the relevant services. distnoted is automatically started during system bootstrap.

Let us look at an example of using distributed notifications in an Objective-C program. The example consists of two programs: an observer and a poster. The poster takes a name-value pair of strings and calls the postNotificationName:object: selector. The latter creates an NSNotification object, associates the given name and value with it, and posts it to the distributed notification center. Figure 955 shows the source for the poster.

Figure 955. A program for posting distributed notifications (NSNotification)

// NSNotificationPoster.m #import <AppKit/AppKit.h> #define PROGNAME "NSNotificationPoster" int main(int argc, char **argv) { if (argc != 3) { fprintf(stderr, "usage: %s <some name> <some value>\n", PROGNAME); exit(1); } NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSString *someName = [NSString stringWithCString:argv[1] encoding:NSASCIIStringEncoding]; NSString *someValue = [NSString stringWithCString:argv[2] encoding:NSASCIIStringEncoding]; NSNotificationCenter *dnc = [NSDistributedNotificationCenter defaultCenter]; [dnc postNotificationName:someName object:someValue]; [pool release]; exit(0); }

The observer communicates with the distributed notification center to register its interest in all distributed notifications, after which it simply runs in a loop, printing the name and value of each notification as it arrives. Figure 956 shows the source for the observer.

Figure 956. A program for observing distributed notifications (NSNotification)

// NSNotificationObserver.m #import <AppKit/AppKit.h> @interface DummyNotificationHandler : NSObject { NSNotificationCenter *dnc; } - (void)defaultNotificationHandler:(NSNotification *)notification; @end @implementation DummyNotificationHandler - (id)init { [super init]; dnc = [NSDistributedNotificationCenter defaultCenter]; [dnc addObserver:self selector:@selector(defaultNotificationHandler:) name:nil object:nil]; return self; } - (void)dealloc { [dnc removeObserver:self name:nil object:nil]; [super dealloc]; } - (void)defaultNotificationHandler:(NSNotification *)notification { NSLog(@"name=%@ value=%@", [notification name], [notification object]); } @end int main(int argc, char **argv) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSRunLoop *runloop = [NSRunLoop currentRunLoop]; [[DummyNotificationHandler alloc] init]; [runloop run]; [pool release]; exit(0); }

Let us test the programs by first running the observer and then posting a few notifications through the poster.

$ gcc -Wall -o observer NSNotificationObserver.m -framework Foundation $ gcc -Wall -o poster NSNotificationPoster.m -framework Foundation $ ./observer # another shell prompt $ ./poster system mach 2005-09-17 20:39:10.093 observer[4284] name=system value=mach

Note that since the observer program specified the notification name and identifying object as nil while adding the DummyNotificationHandler class instance as an observer, it will receive all other system-wide notifications that distnoted broadcasts.

9.16.2. The notify(3) API

Mac OS X provides a stateless, system-wide notification system whose services are available to user programs through the notify(3) API. The mechanism is implemented as a client-server system. The notification server (/usr/sbin/notifyd) provides a system-wide notification center. It is one of the daemons started during a normal bootstrap of the system. The client API, which is implemented as part of the system library, uses Mach IPC to communicate with the server.

A notify(3) notification is associated with a null-terminated, UTF-8-encoded string name in a namespace shared system-wide by all clients of the system. Although a notification name can be arbitrary, Apple recommends using reverse DNS naming conventions. The names prefixed by com.apple. are reserved for Apple's use, whereas the names prefixed by self. should be used by a program for process-local notifications.

A client can post a notification for a given name by calling the notify_post() function, which takes a single argument: the notification's name.

A client can monitor a given name for notifications. Moreover, the client can specify the mechanism through which the system should deliver the notification to the client. Supported delivery mechanisms are as follows: sending a specified signal, writing to a file descriptor, sending a message to a Mach port, and updating a shared memory location. Clients can register for these mechanisms by calling notify_register_signal(), notify_register_file_descriptor(), notify_register_mach_port(), and notify_register_check(), respectively. Each registration function provides the caller with a token and, if necessary, a mechanism-specific object such as a Mach port or a file descriptor, which the client will use to receive the notification. The token can be used with the notify_check() call to check whether any notifications have been posted for the associated name. It is also used with the notify_cancel() call to cancel the notification and free any associated resources.

Let us look at an example of posting and receiving notifications using the notify(3) API. Figure 957 shows a common header file in which we define names for our notifications. We use a common prefix, followed by one of descriptor, mach_port, or signal, indicating the delivery mechanism we will specify when registering for each name.

Figure 957. Common header file for defining notification names

// notify_common.h #ifndef _NOTIFY_COMMON_H_ #define _NOTIFY_COMMON_H_ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <notify.h> #define PREFIX "com.osxbook.notification." #define NOTIFICATION_BY_FILE_DESCRIPTOR PREFIX "descriptor" #define NOTIFICATION_BY_MACH_PORT PREFIX "mach_port" #define NOTIFICATION_BY_SIGNAL PREFIX "signal" #define NOTIFICATION_CANCEL PREFIX "cancel" #endif

Figure 958 shows the program we will use to post notifications. Note that posting is independent of the delivery mechanismyou always use notify_post() to explicitly post a notification, regardless of how it is delivered.

Figure 958. Program for posting notify(3) notifications

// notify_producer.c #include "notify_common.h" #define PROGNAME "notify_producer" int usage(void) { fprintf(stderr, "usage: %s -c|-f|-p|-s\n", PROGNAME); return 1; } int main(int argc, char **argv) { int ch, options = 0; char *name; if (argc != 2) return usage(); while ((ch = getopt(argc, argv, "cfps")) != -1) { switch (ch) { case 'c': name = NOTIFICATION_CANCEL; break; case 'f': name = NOTIFICATION_BY_FILE_DESCRIPTOR; break; case 'p': name = NOTIFICATION_BY_MACH_PORT; break; case 's': name = NOTIFICATION_BY_SIGNAL; break; default: return usage(); break; } options++; } if (options == 1) return (int)notify_post(name); else return usage(); }

Let us now write the consumer program for receiving notifications produced by the program in Figure 958. We will register for four specific notifications, one to be delivered through a signal, another to be delivered through a file descriptor, and two to be delivered through Mach messages. One of the latter will be used as a cancellation triggerit will cause the program to cancel all registrations and exit. Figure 959 shows the consumer program.

Figure 959. Receiving notifications through multiple mechanisms

// notify_consumer.c #include "notify_common.h" #include <pthread.h> #include <mach/mach.h> #include <signal.h> void sighandler_USR1(int s); void cancel_all_notifications(void); static int token_fd = -1, token_mach_port = -1, token_signal = -1; static int token_mach_port_cancel = -1; void * consumer_file_descriptor(void *arg) { int status; int fd, check; status = notify_register_file_descriptor(NOTIFICATION_BY_FILE_DESCRIPTOR, &fd, 0, &token_fd); if (status != NOTIFY_STATUS_OK) { perror("notify_register_file_descriptor"); return (void *)status; } while (1) { if ((status = read(fd, &check, sizeof(check))) < 0) return (void *)status; // perhaps the notification was canceled if (check == token_fd) printf("file descriptor: received notification\n"); else printf("file descriptor: spurious notification?\n"); } return (void *)0; } void * consumer_mach_port(void *arg) { int status; kern_return_t kr; mach_msg_header_t msg; mach_port_t notify_port; status = notify_register_mach_port(NOTIFICATION_BY_MACH_PORT, &notify_port, 0, &token_mach_port); if (status != NOTIFY_STATUS_OK) { perror("notify_register_mach_port"); return (void *)status; } // to support cancellation of all notifications and exiting, we register // a second notification here, but reuse the Mach port allocated above status = notify_register_mach_port(NOTIFICATION_CANCEL, &notify_port, NOTIFY_REUSE, &token_mach_port_cancel); if (status != NOTIFY_STATUS_OK) { perror("notify_register_mach_port"); mach_port_deallocate(mach_task_self(), notify_port); return (void *)status; } while (1) { kr = mach_msg(&msg, // message buffer MACH_RCV_MSG, // option 0, // send size MACH_MSG_SIZE_MAX, // receive limit notify_port, // receive name MACH_MSG_TIMEOUT_NONE, // timeout MACH_PORT_NULL); // cancel/receive notification if (kr != MACH_MSG_SUCCESS) mach_error("mach_msg(MACH_RCV_MSG)", kr); if (msg.msgh_id == token_mach_port) printf("Mach port: received notification\n"); else if (msg.msgh_id == token_mach_port_cancel) { cancel_all_notifications(); printf("canceling all notifications and exiting\n"); exit(0); } else printf("Mach port: spurious notification?\n"); } return (void *)0; } void sighandler_USR1(int s) { int status, check; status = notify_check(token_signal, &check); if ((status == NOTIFY_STATUS_OK) && (check != 0)) printf("signal: received notification\n"); else printf("signal: spurious signal?\n"); } void * consumer_signal(void *arg) { int status, check; // set up signal handler signal(SIGUSR1, sighandler_USR1); status = notify_register_signal(NOTIFICATION_BY_SIGNAL, SIGUSR1, &token_signal); if (status != NOTIFY_STATUS_OK) { perror("notify_register_signal"); return (void *)status; } // since notify_check() always sets check to 'true' when it is called for // the first time, we make a dummy call here (void)notify_check(token_signal, &check); while (1) { // just sleep for a day sleep(86400); } return (void *)0; } void cancel_all_notifications(void) { if (token_fd != -1) notify_cancel(token_fd); if (token_mach_port != -1) notify_cancel(token_mach_port); if (token_signal != -1) notify_cancel(token_signal); } int main(int argc, char **argv) { int ret; pthread_t pthread_fd, pthread_mach_port; if ((ret = pthread_create(&pthread_fd, (const pthread_attr_t *)0, consumer_file_descriptor, (void *)0))) goto out; if ((ret = pthread_create(&pthread_mach_port, (const pthread_attr_t *)0, consumer_mach_port, (void *)0))) goto out; if (consumer_signal((void *)0) != (void *)0) goto out; out: cancel_all_notifications(); return 0; } $ gcc -Wall -o notify_consumer notify_consumer.c $ gcc -Wall -o notify_producer notify_producer.c $ ./notification_consumer # another shell prompt $ ./notify_producer -f file descriptor: received notification $ ./notify_producer -p Mach port: received notification $ ./notify_producer -s signal: received notification $ killall -USR1 notify_consumer signal: spurious signal? $ ./notify_producer -c canceling all notifications and exiting $

Once you have a token after registering for a notification, you can also use the token to monitor a file pathname by calling notify_monitor_file(), whose arguments include a token and a pathname. Thereafter, in addition to notifications explicitly posted through notify_post(), the system will deliver a notification each time the pathname is modified. Note that the pathname does not have to exist when you call notify_monitor_file()if it doesn't exist, the first notification will correspond to the file's creation. We can add the code shown in Figure 960 (the highlighted portion) to the consumer_mach_port() function in Figure 959 to make the program exit whenever a given pathsay, /tmp/notify.cancelis modified.

Figure 960. Monitoring a file through notify(3)

void * consumer_mach_port(void *arg) { ... status = notify_register_mach_port(NOTIFICATION_CANCEL, &notify_port, NOTIFY_REUSE, &token_mach_port_cancel); if (status != NOTIFY_STATUS_OK) { perror("notify_register_mach_port"); mach_port_deallocate(mach_task_self(), notify_port); return (void *)status; } status = notify_monitor_file(token_mach_port_cancel, "/tmp/notify.cancel"); if (status != NOTIFY_STATUS_OK) { perror("notify_monitor_file"); mach_port_deallocate(mach_task_self(), notify_port); return (void *)status; } while (1) { ... } ... }

9.16.3. Kernel Event Notification Mechanism (kqueue(2))

Mac OS X provides a FreeBSD-derived mechanism called kqueue for kernel event notification. The mechanism gets its name from the kqueue data structure (struct kqueue [bsd/sys/eventvar.h]), which represents a kernel queue of events.

A program uses this mechanism through the kqueue() and kevent() system calls. kqueue() creates a new kernel event queue and returns a file descriptor. Specific operations performed by kqueue() in the kernel include the following:

  • Create a new open file structure (struct fileproc [bsd/sys/file_internal.h]) and allocate a file descriptor, which the calling process uses to refer to the open file.

  • Allocate and initialize a kqueue data structure (struct kqueue [bsd/sys/eventvar.h]).

  • Set the file structure's f_flag field to (FREAD | FWRITE) to specify that the file is open for both reading and writing.

  • Set the file structure's f_type (descriptor type) field to DTYPE_KQUEUE to specify that the descriptor references a kqueue.

  • Set the file structure's f_ops field (file operations table) to point to the kqueueops global structure variable [bsd/kern/kern_event.c].

  • Set the file structure's f_data field (private data) to point to the newly allocated kqueue structure.

kevent() is used both for registering events with a kernel queue (given the corresponding descriptor) and for retrieving any pending events. An event is represented by a kevent structure [bsd/sys/event.h].

struct kevent { uintptr_t ident; // identifier for this event short filter; // filter for event u_short flags; // action flags for kqueue u_int fflags; // filter flag value intptr_t data; // filter data value void *udata; // opaque user data identifier };

Kernel events are generated by various parts of the kernel calling kqueue functions to add kernel notes (struct knote [bsd/sys/event.h]). The proc structure's p_klist field is a list of attached kernel notes.

A caller can populate a kevent structure and invoke kevent() to request to be notified when that event occurs. The kevent structure's filter field specifies the kernel filter to be used to process the event. The ident field is interpreted by the kernel based on the filter. For example, the filter can be EVFILT_PROC, which means the caller is interested in process-related events, such as the process exiting or forking. In this case, the ident field specifies a process ID. Table 99 shows the system-defined filters and corresponding identifier types.

Table 99. Kqueue Filters

Filter

Identifier

Examples of Events

EVFILT_FS

File system being mounted or unmounted, NFS server not responding, free space falling below the minimum threshold on an HFS Plus file system

EVFILT_PROC

A process ID

Process performing a fork, exec, or exit operation

EVFILT_READ

A file descriptor

Data available to read

EVFILT_SIGNAL

A signal number

Specified signal delivered to the process

EVFILT_TIMER

An interval

Timer expired

EVFILT_VNODE

A file descriptor

Vnode operations such as deletion, rename, content change, attribute change, link count change, and so on

EVFILT_WRITE

A file descriptor

Possibility of data to be written

The flags field specifies one or more actions to perform, such as adding the specified event to the kqueue (EV_ADD) or removing the event from the kqueue (EV_DELETE). The fflags field is used to specify one or more filter-specific events that should be monitored. For example, if the exit and fork operations are to be monitored for a process using the EVFILT_PROC filter, the fflags field should contain the bitwise OR of NOTE_EXIT and NOTE_FORK.

The data field contains filter-specific data, if any. For example, for the EVFILT_SIGNAL filter, data will contain the number of times the signal has occurred since the last call to kevent().

The udata field optionally contains user-defined data that is not interpreted by the kernel.

The EV_SET macro can be used to populate a kevent structure.

Figure 961 shows a program that uses the EVFILT_VNODE filter to watch for events on a given file.

Figure 961. Using the kqueue() and kevent() system calls to watch for file events

// kq_fwatch.c #include <stdio.h> #include <stdlib.h> #include <sys/fcntl.h> #include <sys/event.h> #include <unistd.h> #define PROGNAME "kq_fwatch" typedef struct { u_int event; const char *description; } VnEventDescriptions_t; VnEventDescriptions_t VnEventDescriptions[] = { { NOTE_ATTRIB, "attributes changed" }, { NOTE_DELETE, "deleted" }, { NOTE_EXTEND, "extended" }, { NOTE_LINK, "link count changed" }, { NOTE_RENAME, "renamed" }, { NOTE_REVOKE, "access revoked or file system unmounted" }, { NOTE_WRITE, "written" }, }; #define N_EVENTS (sizeof(VnEventDescriptions)/sizeof(VnEventDescriptions_t)) int process_events(struct kevent *kl) { int i, ret = 0; for (i = 0; i < N_EVENTS; i++) if (VnEventDescriptions[i].event & kl->fflags) printf("%s\n", VnEventDescriptions[i].description); if (kl->fflags & NOTE_DELETE) // stop when the file is gone ret = -1; return ret; } int main(int argc, char **argv) { int fd, ret = -1, kqfd = -1; struct kevent changelist; if (argc != 2) { fprintf(stderr, "usage: %s <file to watch>\n", PROGNAME); exit(1); } // create a new kernel event queue (not inherited across fork()) if ((kqfd = kqueue()) < 0) { perror("kqueue"); exit(1); } if ((fd = open(argv[1], O_RDONLY)) < 0) { perror("open"); exit(1); } #define NOTE_ALL NOTE_ATTRIB |\ NOTE_DELETE |\ NOTE_EXTEND |\ NOTE_LINK |\ NOTE_RENAME |\ NOTE_REVOKE |\ NOTE_WRITE EV_SET(&changelist, fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, NOTE_ALL, 0, NULL); // the following kevent() call is for registering events ret = kevent(kqfd, // kqueue file descriptor &changelist, // array of kevent structures 1, // number of entries in the changelist array NULL, // array of kevent structures (for receiving) 0, // number of entries in the above array NULL); // timeout if (ret < 0) { perror("kqueue"); goto out; } do { // the following kevent() call is for receiving events // we recycle the changelist from the previous call if ((ret = kevent(kqfd, NULL, 0, &changelist, 1, NULL)) == -1) { perror("kevent"); goto out; } // kevent() returns the number of events placed in the receive list if (ret != 0) ret = process_events(&changelist); } while (!ret); out: if (kqfd >= 0) close(kqfd); exit(ret); } $ gcc -Wall -o kq_fwatch kq_fwatch.c $ touch /tmp/file.txt $ ./kq_fwatch /tmp/file.txt # another shell prompt $ touch /tmp/file.txt attributes changed $ echo hello > /tmp/file.txt attributes changed written $ sync /tmp/file.txt attributes changed $ ln /tmp/file.txt /tmp/file2.txt attributes changed link count changed $ rm /tmp/file2.txt deleted

The Finder uses the kqueue mechanism to learn about changes made to a directory that is being displayed in a Finder window, with the Desktop being a special case of a Finder window. This allows the Finder to update the directory's view.

9.16.4. Core Foundation Notifications

Core Foundation notifications are discussed in Section 9.17.1. We mention them here for completeness.

9.16.5. Fsevents

Mac OS X 10.4 introduced an in-kernel notification mechanism called fsevents that can inform user-space subscribers of volume-level file system changes as they occur. The Spotlight search system uses this mechanism. We will discuss fsevents in Chapter 11.

9.16.6. Kauth

Kauth (kernel authorization) is a kernel subsystem introduced in Mac OS X 10.4 that provides a kernel programming interface (KPI) using which loadable kernel code can participate in authorization decisions in the kernel. It can also be used as a notification mechanism. We will discuss kauth in Chapter 11.

Категории