/* * Select abstraction functions for the CUPS scheduler. * * Copyright 2007-2016 by Apple Inc. * Copyright 2006-2007 by Easy Software Products. * * Licensed under Apache License v2.0. See the file "LICENSE" for more information. */ /* * Include necessary headers... */ #include "cupsd.h" #ifdef HAVE_EPOLL # include # include #elif defined(HAVE_KQUEUE) # include # include #elif defined(HAVE_POLL) # include #else # include #endif /* HAVE_EPOLL */ /* * Design Notes for Poll/Select API in CUPSD * ----------------------------------------- * * SUPPORTED APIS * * OS select poll epoll kqueue /dev/poll * -------------- ------ ------ ------ ------ --------- * AIX YES YES NO NO NO * FreeBSD YES YES NO YES NO * HP-UX YES YES NO NO NO * Linux YES YES YES NO NO * macOS YES YES NO YES NO * NetBSD YES YES NO YES NO * OpenBSD YES YES NO YES NO * Solaris YES YES NO NO YES * Tru64 YES YES NO NO NO * Windows YES NO NO NO NO * * * HIGH-LEVEL API * * typedef void (*cupsd_selfunc_t)(void *data); * * void cupsdStartSelect(void); * void cupsdStopSelect(void); * void cupsdAddSelect(int fd, cupsd_selfunc_t read_cb, * cupsd_selfunc_t write_cb, void *data); * void cupsdRemoveSelect(int fd); * int cupsdDoSelect(int timeout); * * * IMPLEMENTATION STRATEGY * * 0. Common Stuff * a. CUPS array of file descriptor to callback functions * and data + temporary array of removed fd's. * b. cupsdStartSelect() creates the arrays * c. cupsdStopSelect() destroys the arrays and all elements. * d. cupsdAddSelect() adds to the array and allocates a * new callback element. * e. cupsdRemoveSelect() removes from the active array and * adds to the inactive array. * f. _cupsd_fd_t provides a reference-counted structure for * tracking file descriptors that are monitored. * g. cupsdDoSelect() frees all inactive FDs. * * 1. select() O(n) * a. Input/Output fd_set variables, copied to working * copies and then used with select(). * b. Loop through CUPS array, using FD_ISSET and calling * the read/write callbacks as needed. * c. cupsdRemoveSelect() clears fd_set bit from main and * working sets. * d. cupsdStopSelect() frees all of the memory used by the * CUPS array and fd_set's. * * 2. poll() - O(n log n) * a. Regular array of pollfd, sorted the same as the CUPS * array. * b. Loop through pollfd array, call the corresponding * read/write callbacks as needed. * c. cupsdAddSelect() adds first to CUPS array and flags the * pollfd array as invalid. * d. cupsdDoSelect() rebuilds pollfd array as needed, calls * poll(), then loops through the pollfd array looking up * as needed. * e. cupsdRemoveSelect() flags the pollfd array as invalid. * f. cupsdStopSelect() frees all of the memory used by the * CUPS array and pollfd array. * * 3. epoll() - O(n) * a. cupsdStartSelect() creates epoll file descriptor using * epoll_create() with the maximum fd count, and * allocates an events buffer for the maximum fd count. * b. cupsdAdd/RemoveSelect() uses epoll_ctl() to add * (EPOLL_CTL_ADD) or remove (EPOLL_CTL_DEL) a single * event using the level-triggered semantics. The event * user data field is a pointer to the new callback array * element. * c. cupsdDoSelect() uses epoll_wait() with the global event * buffer allocated in cupsdStartSelect() and then loops * through the events, using the user data field to find * the callback record. * d. cupsdStopSelect() closes the epoll file descriptor and * frees all of the memory used by the event buffer. * * 4. kqueue() - O(n) * b. cupsdStartSelect() creates kqueue file descriptor * using kqueue() function and allocates a global event * buffer. * c. cupsdAdd/RemoveSelect() uses EV_SET and kevent() to * register the changes. The event user data field is a * pointer to the new callback array element. * d. cupsdDoSelect() uses kevent() to poll for events and * loops through the events, using the user data field to * find the callback record. * e. cupsdStopSelect() closes the kqueue() file descriptor * and frees all of the memory used by the event buffer. * * 5. /dev/poll - O(n log n) - NOT YET IMPLEMENTED * a. cupsdStartSelect() opens /dev/poll and allocates an * array of pollfd structs; on failure to open /dev/poll, * revert to poll() system call. * b. cupsdAddSelect() writes a single pollfd struct to * /dev/poll with the new file descriptor and the * POLLIN/POLLOUT flags. * c. cupsdRemoveSelect() writes a single pollfd struct to * /dev/poll with the file descriptor and the POLLREMOVE * flag. * d. cupsdDoSelect() uses the DP_POLL ioctl to retrieve * events from /dev/poll and then loops through the * returned pollfd array, looking up the file descriptors * as needed. * e. cupsdStopSelect() closes /dev/poll and frees the * pollfd array. * * PERFORMANCE * * In tests using the "make test" target with option 0 (keep cupsd * running) and the "testspeed" program with "-c 50 -r 1000", epoll() * performed 5.5% slower than select(), followed by kqueue() at 16% * slower than select() and poll() at 18% slower than select(). Similar * results were seen with twice the number of client connections. * * The epoll() and kqueue() performance is likely limited by the * number of system calls used to add/modify/remove file * descriptors dynamically. Further optimizations may be possible * in the area of limiting use of cupsdAddSelect() and * cupsdRemoveSelect(), however extreme care will be needed to avoid * excess CPU usage and deadlock conditions. * * We may be able to improve the poll() implementation simply by * keeping the pollfd array sync'd with the _cupsd_fd_t array, as that * will eliminate the rebuilding of the array whenever there is a * change and eliminate the fd array lookups in the inner loop of * cupsdDoSelect(). * * Since /dev/poll will never be able to use a shadow array, it may * not make sense to implement support for it. ioctl() overhead will * impact performance as well, so my guess would be that, for CUPS, * /dev/poll will yield a net performance loss. */ /* * Local structures... */ typedef struct _cupsd_fd_s { int fd, /* File descriptor */ use; /* Use count */ cupsd_selfunc_t read_cb, /* Read callback */ write_cb; /* Write callback */ void *data; /* Data pointer for callbacks */ } _cupsd_fd_t; /* * Local globals... */ static cups_array_t *cupsd_fds = NULL; #if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) static cups_array_t *cupsd_inactive_fds = NULL; static int cupsd_in_select = 0; #endif /* HAVE_EPOLL || HAVE_KQUEUE */ #ifdef HAVE_KQUEUE static int cupsd_kqueue_fd = -1, cupsd_kqueue_changes = 0; static struct kevent *cupsd_kqueue_events = NULL; #elif defined(HAVE_POLL) static int cupsd_alloc_pollfds = 0, cupsd_update_pollfds = 0; static struct pollfd *cupsd_pollfds = NULL; # ifdef HAVE_EPOLL static int cupsd_epoll_fd = -1; static struct epoll_event *cupsd_epoll_events = NULL; # endif /* HAVE_EPOLL */ #else /* select() */ static fd_set cupsd_global_input, cupsd_global_output, cupsd_current_input, cupsd_current_output; #endif /* HAVE_KQUEUE */ /* * Local functions... */ static int compare_fds(_cupsd_fd_t *a, _cupsd_fd_t *b); static _cupsd_fd_t *find_fd(int fd); #define release_fd(f) { \ (f)->use --; \ if (!(f)->use) free((f));\ } #define retain_fd(f) (f)->use++ /* * 'cupsdAddSelect()' - Add a file descriptor to the list. */ int /* O - 1 on success, 0 on error */ cupsdAddSelect(int fd, /* I - File descriptor */ cupsd_selfunc_t read_cb, /* I - Read callback */ cupsd_selfunc_t write_cb,/* I - Write callback */ void *data) /* I - Data to pass to callback */ { _cupsd_fd_t *fdptr; /* File descriptor record */ #ifdef HAVE_EPOLL int added; /* 1 if added, 0 if modified */ #endif /* HAVE_EPOLL */ /* * Range check input... */ cupsdLogMessage(CUPSD_LOG_DEBUG2, "cupsdAddSelect(fd=%d, read_cb=%p, write_cb=%p, data=%p)", fd, read_cb, write_cb, data); if (fd < 0) return (0); /* * See if this FD has already been added... */ if ((fdptr = find_fd(fd)) == NULL) { /* * No, add a new entry... */ if ((fdptr = calloc(1, sizeof(_cupsd_fd_t))) == NULL) return (0); fdptr->fd = fd; fdptr->use = 1; if (!cupsArrayAdd(cupsd_fds, fdptr)) { cupsdLogMessage(CUPSD_LOG_EMERG, "Unable to add fd %d to array!", fd); free(fdptr); return (0); } #ifdef HAVE_EPOLL added = 1; } else added = 0; #else } #endif /* HAVE_EPOLL */ #ifdef HAVE_KQUEUE { struct kevent event; /* Event data */ struct timespec timeout; /* Timeout value */ timeout.tv_sec = 0; timeout.tv_nsec = 0; if (fdptr->read_cb != read_cb) { if (read_cb) EV_SET(&event, fd, EVFILT_READ, EV_ADD, 0, 0, fdptr); else EV_SET(&event, fd, EVFILT_READ, EV_DELETE, 0, 0, fdptr); if (kevent(cupsd_kqueue_fd, &event, 1, NULL, 0, &timeout)) { cupsdLogMessage(CUPSD_LOG_EMERG, "kevent() returned %s", strerror(errno)); return (0); } } if (fdptr->write_cb != write_cb) { if (write_cb) EV_SET(&event, fd, EVFILT_WRITE, EV_ADD, 0, 0, fdptr); else EV_SET(&event, fd, EVFILT_WRITE, EV_DELETE, 0, 0, fdptr); if (kevent(cupsd_kqueue_fd, &event, 1, NULL, 0, &timeout)) { cupsdLogMessage(CUPSD_LOG_EMERG, "kevent() returned %s", strerror(errno)); return (0); } } } #elif defined(HAVE_POLL) # ifdef HAVE_EPOLL if (cupsd_epoll_fd >= 0) { struct epoll_event event; /* Event data */ event.events = 0; if (read_cb) event.events |= EPOLLIN; if (write_cb) event.events |= EPOLLOUT; event.data.ptr = fdptr; if (epoll_ctl(cupsd_epoll_fd, added ? EPOLL_CTL_ADD : EPOLL_CTL_MOD, fd, &event)) { close(cupsd_epoll_fd); cupsd_epoll_fd = -1; cupsd_update_pollfds = 1; } } else # endif /* HAVE_EPOLL */ cupsd_update_pollfds = 1; #else /* select() */ /* * Add or remove the file descriptor in the input and output sets * for select()... */ if (read_cb) FD_SET(fd, &cupsd_global_input); else { FD_CLR(fd, &cupsd_global_input); FD_CLR(fd, &cupsd_current_input); } if (write_cb) FD_SET(fd, &cupsd_global_output); else { FD_CLR(fd, &cupsd_global_output); FD_CLR(fd, &cupsd_current_output); } #endif /* HAVE_KQUEUE */ /* * Save the (new) read and write callbacks... */ fdptr->read_cb = read_cb; fdptr->write_cb = write_cb; fdptr->data = data; return (1); } /* * 'cupsdDoSelect()' - Do a select-like operation. */ int /* O - Number of files or -1 on error */ cupsdDoSelect(long timeout) /* I - Timeout in seconds */ { int nfds; /* Number of file descriptors */ _cupsd_fd_t *fdptr; /* Current file descriptor */ #ifdef HAVE_KQUEUE int i; /* Looping var */ struct kevent *event; /* Current event */ struct timespec ktimeout; /* kevent() timeout */ cupsd_in_select = 1; if (timeout >= 0 && timeout < 86400) { ktimeout.tv_sec = timeout; ktimeout.tv_nsec = 0; nfds = kevent(cupsd_kqueue_fd, NULL, 0, cupsd_kqueue_events, MaxFDs, &ktimeout); } else nfds = kevent(cupsd_kqueue_fd, NULL, 0, cupsd_kqueue_events, MaxFDs, NULL); cupsd_kqueue_changes = 0; for (i = nfds, event = cupsd_kqueue_events; i > 0; i --, event ++) { fdptr = (_cupsd_fd_t *)event->udata; if (cupsArrayFind(cupsd_inactive_fds, fdptr)) continue; retain_fd(fdptr); if (fdptr->read_cb && event->filter == EVFILT_READ) (*(fdptr->read_cb))(fdptr->data); if (fdptr->use > 1 && fdptr->write_cb && event->filter == EVFILT_WRITE && !cupsArrayFind(cupsd_inactive_fds, fdptr)) (*(fdptr->write_cb))(fdptr->data); release_fd(fdptr); } #elif defined(HAVE_POLL) struct pollfd *pfd; /* Current pollfd structure */ int count; /* Number of file descriptors */ # ifdef HAVE_EPOLL cupsd_in_select = 1; if (cupsd_epoll_fd >= 0) { int i; /* Looping var */ struct epoll_event *event; /* Current event */ if (timeout >= 0 && timeout < 86400) nfds = epoll_wait(cupsd_epoll_fd, cupsd_epoll_events, MaxFDs, timeout * 1000); else nfds = epoll_wait(cupsd_epoll_fd, cupsd_epoll_events, MaxFDs, -1); if (nfds < 0 && errno != EINTR) { close(cupsd_epoll_fd); cupsd_epoll_fd = -1; } else { for (i = nfds, event = cupsd_epoll_events; i > 0; i --, event ++) { fdptr = (_cupsd_fd_t *)event->data.ptr; if (cupsArrayFind(cupsd_inactive_fds, fdptr)) continue; retain_fd(fdptr); if (fdptr->read_cb && (event->events & (EPOLLIN | EPOLLERR | EPOLLHUP))) (*(fdptr->read_cb))(fdptr->data); if (fdptr->use > 1 && fdptr->write_cb && (event->events & (EPOLLOUT | EPOLLERR | EPOLLHUP)) && !cupsArrayFind(cupsd_inactive_fds, fdptr)) (*(fdptr->write_cb))(fdptr->data); release_fd(fdptr); } goto release_inactive; } } # endif /* HAVE_EPOLL */ count = cupsArrayCount(cupsd_fds); if (cupsd_update_pollfds) { /* * Update the cupsd_pollfds array to match the current FD array... */ cupsd_update_pollfds = 0; /* * (Re)allocate memory as needed... */ if (count > cupsd_alloc_pollfds) { int allocfds = count + 16; if (cupsd_pollfds) pfd = realloc(cupsd_pollfds, (size_t)allocfds * sizeof(struct pollfd)); else pfd = malloc((size_t)allocfds * sizeof(struct pollfd)); if (!pfd) { cupsdLogMessage(CUPSD_LOG_EMERG, "Unable to allocate %d bytes for polling.", (int)((size_t)allocfds * sizeof(struct pollfd))); return (-1); } cupsd_pollfds = pfd; cupsd_alloc_pollfds = allocfds; } /* * Rebuild the array... */ for (fdptr = (_cupsd_fd_t *)cupsArrayFirst(cupsd_fds), pfd = cupsd_pollfds; fdptr; fdptr = (_cupsd_fd_t *)cupsArrayNext(cupsd_fds), pfd ++) { pfd->fd = fdptr->fd; pfd->events = 0; if (fdptr->read_cb) pfd->events |= POLLIN; if (fdptr->write_cb) pfd->events |= POLLOUT; } } if (timeout >= 0 && timeout < 86400) nfds = poll(cupsd_pollfds, (nfds_t)count, timeout * 1000); else nfds = poll(cupsd_pollfds, (nfds_t)count, -1); if (nfds > 0) { /* * Do callbacks for each file descriptor... */ for (pfd = cupsd_pollfds; count > 0; pfd ++, count --) { if (!pfd->revents) continue; if ((fdptr = find_fd(pfd->fd)) == NULL) continue; retain_fd(fdptr); if (fdptr->read_cb && (pfd->revents & (POLLIN | POLLERR | POLLHUP))) (*(fdptr->read_cb))(fdptr->data); if (fdptr->use > 1 && fdptr->write_cb && (pfd->revents & (POLLOUT | POLLERR | POLLHUP))) (*(fdptr->write_cb))(fdptr->data); release_fd(fdptr); } } #else /* select() */ struct timeval stimeout; /* Timeout for select() */ int maxfd; /* Maximum file descriptor */ /* * Figure out the highest file descriptor number... */ if ((fdptr = (_cupsd_fd_t *)cupsArrayLast(cupsd_fds)) == NULL) maxfd = 1; else maxfd = fdptr->fd + 1; /* * Do the select()... */ cupsd_current_input = cupsd_global_input; cupsd_current_output = cupsd_global_output; if (timeout >= 0 && timeout < 86400) { stimeout.tv_sec = timeout; stimeout.tv_usec = 0; nfds = select(maxfd, &cupsd_current_input, &cupsd_current_output, NULL, &stimeout); } else nfds = select(maxfd, &cupsd_current_input, &cupsd_current_output, NULL, NULL); if (nfds > 0) { /* * Do callbacks for each file descriptor... */ for (fdptr = (_cupsd_fd_t *)cupsArrayFirst(cupsd_fds); fdptr; fdptr = (_cupsd_fd_t *)cupsArrayNext(cupsd_fds)) { retain_fd(fdptr); if (fdptr->read_cb && FD_ISSET(fdptr->fd, &cupsd_current_input)) (*(fdptr->read_cb))(fdptr->data); if (fdptr->use > 1 && fdptr->write_cb && FD_ISSET(fdptr->fd, &cupsd_current_output)) (*(fdptr->write_cb))(fdptr->data); release_fd(fdptr); } } #endif /* HAVE_KQUEUE */ #if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) /* * Release all inactive file descriptors... */ # ifndef HAVE_KQUEUE release_inactive: # endif /* !HAVE_KQUEUE */ cupsd_in_select = 0; for (fdptr = (_cupsd_fd_t *)cupsArrayFirst(cupsd_inactive_fds); fdptr; fdptr = (_cupsd_fd_t *)cupsArrayNext(cupsd_inactive_fds)) { cupsArrayRemove(cupsd_inactive_fds, fdptr); release_fd(fdptr); } #endif /* HAVE_EPOLL || HAVE_KQUEUE */ /* * Return the number of file descriptors handled... */ return (nfds); } #ifdef CUPSD_IS_SELECTING /* * 'cupsdIsSelecting()' - Determine whether we are monitoring a file * descriptor. */ int /* O - 1 if selecting, 0 otherwise */ cupsdIsSelecting(int fd) /* I - File descriptor */ { return (find_fd(fd) != NULL); } #endif /* CUPSD_IS_SELECTING */ /* * 'cupsdRemoveSelect()' - Remove a file descriptor from the list. */ void cupsdRemoveSelect(int fd) /* I - File descriptor */ { _cupsd_fd_t *fdptr; /* File descriptor record */ #ifdef HAVE_EPOLL struct epoll_event event; /* Event data */ #elif defined(HAVE_KQUEUE) struct kevent event; /* Event data */ struct timespec timeout; /* Timeout value */ #elif defined(HAVE_POLL) /* No variables for poll() */ #endif /* HAVE_EPOLL */ /* * Range check input... */ cupsdLogMessage(CUPSD_LOG_DEBUG2, "cupsdRemoveSelect(fd=%d)", fd); if (fd < 0) return; /* * Find the file descriptor... */ if ((fdptr = find_fd(fd)) == NULL) return; #ifdef HAVE_EPOLL if (epoll_ctl(cupsd_epoll_fd, EPOLL_CTL_DEL, fd, &event)) { close(cupsd_epoll_fd); cupsd_epoll_fd = -1; cupsd_update_pollfds = 1; } #elif defined(HAVE_KQUEUE) timeout.tv_sec = 0; timeout.tv_nsec = 0; if (fdptr->read_cb) { EV_SET(&event, fd, EVFILT_READ, EV_DELETE, 0, 0, fdptr); if (kevent(cupsd_kqueue_fd, &event, 1, NULL, 0, &timeout)) { cupsdLogMessage(CUPSD_LOG_EMERG, "kevent() returned %s", strerror(errno)); goto cleanup; } } if (fdptr->write_cb) { EV_SET(&event, fd, EVFILT_WRITE, EV_DELETE, 0, 0, fdptr); if (kevent(cupsd_kqueue_fd, &event, 1, NULL, 0, &timeout)) { cupsdLogMessage(CUPSD_LOG_EMERG, "kevent() returned %s", strerror(errno)); goto cleanup; } } #elif defined(HAVE_POLL) /* * Update the pollfds array... */ cupsd_update_pollfds = 1; #else /* select() */ FD_CLR(fd, &cupsd_global_input); FD_CLR(fd, &cupsd_global_output); FD_CLR(fd, &cupsd_current_input); FD_CLR(fd, &cupsd_current_output); #endif /* HAVE_EPOLL */ #ifdef HAVE_KQUEUE cleanup: #endif /* HAVE_KQUEUE */ /* * Remove the file descriptor from the active array and add to the * inactive array (or release, if we don't need the inactive array...) */ cupsArrayRemove(cupsd_fds, fdptr); #if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) if (cupsd_in_select) cupsArrayAdd(cupsd_inactive_fds, fdptr); else #endif /* HAVE_EPOLL || HAVE_KQUEUE */ release_fd(fdptr); } /* * 'cupsdStartSelect()' - Initialize the file polling engine. */ void cupsdStartSelect(void) { cupsdLogMessage(CUPSD_LOG_DEBUG, "cupsdStartSelect()"); cupsd_fds = cupsArrayNew((cups_array_func_t)compare_fds, NULL); #if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) cupsd_inactive_fds = cupsArrayNew((cups_array_func_t)compare_fds, NULL); #endif /* HAVE_EPOLL || HAVE_KQUEUE */ #ifdef HAVE_EPOLL cupsd_epoll_fd = epoll_create(MaxFDs); cupsd_epoll_events = calloc((size_t)MaxFDs, sizeof(struct epoll_event)); cupsd_update_pollfds = 0; #elif defined(HAVE_KQUEUE) cupsd_kqueue_fd = kqueue(); cupsd_kqueue_changes = 0; cupsd_kqueue_events = calloc((size_t)MaxFDs, sizeof(struct kevent)); #elif defined(HAVE_POLL) cupsd_update_pollfds = 0; #else /* select() */ FD_ZERO(&cupsd_global_input); FD_ZERO(&cupsd_global_output); #endif /* HAVE_EPOLL */ } /* * 'cupsdStopSelect()' - Shutdown the file polling engine. */ void cupsdStopSelect(void) { _cupsd_fd_t *fdptr; /* Current file descriptor */ cupsdLogMessage(CUPSD_LOG_DEBUG, "cupsdStopSelect()"); for (fdptr = (_cupsd_fd_t *)cupsArrayFirst(cupsd_fds); fdptr; fdptr = (_cupsd_fd_t *)cupsArrayNext(cupsd_fds)) free(fdptr); cupsArrayDelete(cupsd_fds); cupsd_fds = NULL; #if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) cupsArrayDelete(cupsd_inactive_fds); cupsd_inactive_fds = NULL; #endif /* HAVE_EPOLL || HAVE_KQUEUE */ #ifdef HAVE_KQUEUE if (cupsd_kqueue_events) { free(cupsd_kqueue_events); cupsd_kqueue_events = NULL; } if (cupsd_kqueue_fd >= 0) { close(cupsd_kqueue_fd); cupsd_kqueue_fd = -1; } cupsd_kqueue_changes = 0; #elif defined(HAVE_POLL) # ifdef HAVE_EPOLL if (cupsd_epoll_events) { free(cupsd_epoll_events); cupsd_epoll_events = NULL; } if (cupsd_epoll_fd >= 0) { close(cupsd_epoll_fd); cupsd_epoll_fd = -1; } # endif /* HAVE_EPOLL */ if (cupsd_pollfds) { free(cupsd_pollfds); cupsd_pollfds = NULL; cupsd_alloc_pollfds = 0; } cupsd_update_pollfds = 0; #else /* select() */ FD_ZERO(&cupsd_global_input); FD_ZERO(&cupsd_global_output); #endif /* HAVE_EPOLL */ } /* * 'compare_fds()' - Compare file descriptors. */ static int /* O - Result of comparison */ compare_fds(_cupsd_fd_t *a, /* I - First file descriptor */ _cupsd_fd_t *b) /* I - Second file descriptor */ { return (a->fd - b->fd); } /* * 'find_fd()' - Find an existing file descriptor record. */ static _cupsd_fd_t * /* O - FD record pointer or NULL */ find_fd(int fd) /* I - File descriptor */ { _cupsd_fd_t *fdptr, /* Matching record (if any) */ key; /* Search key */ cupsArraySave(cupsd_fds); key.fd = fd; fdptr = (_cupsd_fd_t *)cupsArrayFind(cupsd_fds, &key); cupsArrayRestore(cupsd_fds); return (fdptr); }