From owner-p4-projects Sun Sep 8 18:43:16 2002 Delivered-To: p4-projects@freebsd.org Received: by hub.freebsd.org (Postfix, from userid 32767) id C929137B401; Sun, 8 Sep 2002 18:42:18 -0700 (PDT) Delivered-To: perforce@freebsd.org Received: from mx1.FreeBSD.org (mx1.FreeBSD.org [216.136.204.125]) by hub.freebsd.org (Postfix) with ESMTP id 4FC7C37B400 for ; Sun, 8 Sep 2002 18:42:18 -0700 (PDT) Received: from freefall.freebsd.org (freefall.FreeBSD.org [216.136.204.21]) by mx1.FreeBSD.org (Postfix) with ESMTP id ABD5E43E72 for ; Sun, 8 Sep 2002 18:42:17 -0700 (PDT) (envelope-from mini@freebsd.org) Received: from freefall.freebsd.org (perforce@localhost [127.0.0.1]) by freefall.freebsd.org (8.12.4/8.12.4) with ESMTP id g891gHJU059269 for ; Sun, 8 Sep 2002 18:42:17 -0700 (PDT) (envelope-from mini@freebsd.org) Received: (from perforce@localhost) by freefall.freebsd.org (8.12.4/8.12.4/Submit) id g891gH8o059266 for perforce@freebsd.org; Sun, 8 Sep 2002 18:42:17 -0700 (PDT) Date: Sun, 8 Sep 2002 18:42:17 -0700 (PDT) Message-Id: <200209090142.g891gH8o059266@freefall.freebsd.org> X-Authentication-Warning: freefall.freebsd.org: perforce set sender to mini@freebsd.org using -f From: Jonathan Mini Subject: PERFORCE change 17248 for review To: Perforce Change Reviews Sender: owner-p4-projects@FreeBSD.ORG Precedence: bulk List-ID: List-Archive: (Web Archive) List-Help: (List Instructions) List-Subscribe: List-Unsubscribe: X-Loop: FreeBSD.ORG http://people.freebsd.org/~peter/p4db/chv.cgi?CH=17248 Change 17248 by mini@mini_stylus on 2002/09/08 18:41:19 Switch over to KSE-style scheduling primitives, but don't actually use KSE yet. Affected files ... .. //depot/projects/kse/lib/libc_r/arch/i386/_thread_enter_uts.S#1 add .. //depot/projects/kse/lib/libc_r/arch/i386/_thread_switch.S#1 add .. //depot/projects/kse/lib/libc_r/sys/Makefile.inc#2 edit .. //depot/projects/kse/lib/libc_r/uthread/Makefile.inc#6 edit .. //depot/projects/kse/lib/libc_r/uthread/pthread_private.h#9 edit .. //depot/projects/kse/lib/libc_r/uthread/uthread_create.c#6 edit .. //depot/projects/kse/lib/libc_r/uthread/uthread_init.c#7 edit .. //depot/projects/kse/lib/libc_r/uthread/uthread_kern.c#8 edit .. //depot/projects/kse/lib/libc_r/uthread/uthread_priority_queue.c#3 edit Differences ... ==== //depot/projects/kse/lib/libc_r/sys/Makefile.inc#2 (text+ko) ==== @@ -2,5 +2,5 @@ .PATH: ${.CURDIR}/sys ${.CURDIR}/arch/${MACHINE_ARCH} -SRCS+= uthread_error.c _atomic_lock.S +SRCS+= uthread_error.c _atomic_lock.S _thread_enter_uts.S _thread_switch.S ==== //depot/projects/kse/lib/libc_r/uthread/Makefile.inc#6 (text+ko) ==== @@ -63,6 +63,7 @@ uthread_pause.c \ uthread_poll.c \ uthread_priority_queue.c \ + uthread_printf.c \ uthread_pselect.c \ uthread_read.c \ uthread_readv.c \ ==== //depot/projects/kse/lib/libc_r/uthread/pthread_private.h#9 (text+ko) ==== @@ -68,18 +68,8 @@ /* Output debug messages like this: */ -#define stdout_debug(args...) do { \ - char buf[128]; \ - snprintf(buf, sizeof(buf), ##args); \ - __sys_write(1, buf, strlen(buf)); \ -} while (0) -#define stderr_debug(args...) do { \ - char buf[128]; \ - snprintf(buf, sizeof(buf), ##args); \ - __sys_write(2, buf, strlen(buf)); \ -} while (0) - - +#define stdout_debug(args...) _thread_printf( args ) +#define stderr_debug(args...) _thread_printf( args ) /* * Priority queue manipulation macros (using pqe link): @@ -704,13 +694,6 @@ ; #endif -SCLASS int _thread_kern_in_sched -#ifdef GLOBAL_PTHREAD_PRIVATE -= 0; -#else -; -#endif - /* Time of day at last scheduling timer signal: */ SCLASS struct timeval volatile _sched_tod #ifdef GLOBAL_PTHREAD_PRIVATE @@ -818,7 +801,7 @@ /* * Declare the kernel scheduler jump buffer and stack: */ -SCLASS ucontext_t _thread_kern_sched_ctx; +SCLASS struct kse_mailbox _thread_kern_kse_mailbox; SCLASS void * _thread_kern_sched_stack #ifdef GLOBAL_PTHREAD_PRIVATE @@ -891,15 +874,18 @@ void _thread_dump_info(void); void _thread_init(void); void _thread_kern_sched(void); -void _thread_kern_scheduler(void); +void _thread_kern_scheduler(struct kse_mailbox *); void _thread_kern_sched_state(enum pthread_state, char *fname, int lineno); void _thread_kern_sched_state_unlock(enum pthread_state state, spinlock_t *lock, char *fname, int lineno); void _thread_kern_set_timeout(const struct timespec *); void _thread_kern_sig_defer(void); void _thread_kern_sig_undefer(void); +void _thread_printf(const char *, ...); void _thread_start(void); void _thread_seterrno(pthread_t, int); +int _thread_enter_uts(struct thread_mailbox *tm, struct kse_mailbox *km); +int _thread_switch(struct thread_mailbox *, struct thread_mailbox **); pthread_addr_t _thread_gc(pthread_addr_t); void _thread_enter_cancellation_point(void); void _thread_leave_cancellation_point(void); ==== //depot/projects/kse/lib/libc_r/uthread/uthread_create.c#6 (text+ko) ==== @@ -232,9 +232,6 @@ { struct pthread *curthread = _get_curthread(); - /* We just left the scheduler via swapcontext: */ - _thread_kern_in_sched = 0; - /* Run the current thread's start routine with argument: */ pthread_exit(curthread->start_routine(curthread->arg)); ==== //depot/projects/kse/lib/libc_r/uthread/uthread_init.c#7 (text+ko) ==== @@ -267,15 +267,12 @@ _thread_initial->attr.stacksize_attr = PTHREAD_STACK_INITIAL; /* Setup the context for the scheduler: */ - getcontext(&_thread_kern_sched_ctx); - _thread_kern_sched_ctx.uc_stack.ss_sp = + _thread_kern_kse_mailbox.km_stack.ss_sp = _thread_kern_sched_stack; - _thread_kern_sched_ctx.uc_stack.ss_size = sched_stack_size; - makecontext(&_thread_kern_sched_ctx, _thread_kern_scheduler, 1); + _thread_kern_kse_mailbox.km_stack.ss_size = sched_stack_size; + _thread_kern_kse_mailbox.km_func = + (void *)_thread_kern_scheduler; - /* Block all signals to the scheduler's context. */ - sigfillset(&_thread_kern_sched_ctx.uc_sigmask); - /* * Write a magic value to the thread structure * to help identify valid ones: @@ -288,8 +285,10 @@ /* Setup the context for initial thread. */ getcontext(&_thread_initial->mailbox.tm_context); - _thread_kern_sched_ctx.uc_stack.ss_sp = _thread_initial->stack; - _thread_kern_sched_ctx.uc_stack.ss_size = PTHREAD_STACK_INITIAL; + _thread_initial->mailbox.tm_context.uc_stack.ss_sp = + _thread_initial->stack; + _thread_initial->mailbox.tm_context.uc_stack.ss_size = + PTHREAD_STACK_INITIAL; /* Default the priority of the initial thread: */ _thread_initial->base_priority = PTHREAD_DEFAULT_PRIORITY; ==== //depot/projects/kse/lib/libc_r/uthread/uthread_kern.c#8 (text+ko) ==== @@ -1,4 +1,5 @@ /* + * Copyright (c) 2002 Jonathan Mini * Copyright (c) 1995-1998 John Birrell * All rights reserved. * @@ -57,6 +58,7 @@ #define DBG_MSG(x...) #endif + /* Static function prototype definitions: */ static void thread_kern_idle(void); @@ -73,27 +75,137 @@ void _thread_kern_sched(void) { + struct timespec ts; + struct timeval tv; struct pthread *curthread = _get_curthread(); + unsigned int current_tick; + + /* Get the current time of day. */ + GET_CURRENT_TOD(tv); + TIMEVAL_TO_TIMESPEC(&tv, &ts); + current_tick = _sched_ticks; /* - * Flag the pthread kernel as executing scheduler code - * to avoid a scheduler signal from interrupting this - * execution and calling the scheduler again. + * Enter a critical section. + */ + _thread_kern_kse_mailbox.km_curthread = NULL; + + /* + * If this thread is becoming inactive, make note of the + * time. + */ + if (curthread->state != PS_RUNNING) { + /* + * Save the current time as the time that the + * thread became inactive: + */ + curthread->last_inactive = (long)current_tick; + if (curthread->last_inactive < + curthread->last_active) { + /* Account for a rollover: */ + curthread->last_inactive =+ + UINT_MAX + 1; + } + } + + /* + * Place this thread into the appropriate queue(s). */ - _thread_kern_in_sched = 1; + switch (curthread->state) { + case PS_DEAD: + case PS_STATE_MAX: /* XXX: silences -Wall */ + case PS_SUSPENDED: + /* Dead or suspended threads are not placed in any queue. */ + break; + case PS_RUNNING: + /* + * Save the current time as the time that the + * thread became inactive: + */ + current_tick = _sched_ticks; + curthread->last_inactive = (long)current_tick; + if (curthread->last_inactive < + curthread->last_active) { + /* Account for a rollover: */ + curthread->last_inactive =+ UINT_MAX + 1; + } + + if ((curthread->slice_usec != -1) && + (curthread->attr.sched_policy != SCHED_FIFO)) { + /* + * Accumulate the number of microseconds for + * which the current thread has run: + */ + curthread->slice_usec += + (curthread->last_inactive - + curthread->last_active) * + (long)_clock_res_usec; + /* Check for time quantum exceeded: */ + if (curthread->slice_usec > TIMESLICE_USEC) + curthread->slice_usec = -1; + } + + if (curthread->slice_usec == -1) { + /* + * The thread exceeded its time + * quantum or it yielded the CPU; + * place it at the tail of the + * queue for its priority. + */ + PTHREAD_PRIOQ_INSERT_TAIL(curthread); + } else { + /* + * The thread hasn't exceeded its + * interval. Place it at the head + * of the queue for its priority. + */ + PTHREAD_PRIOQ_INSERT_HEAD(curthread); + } + break; + case PS_SPINBLOCK: + /* Increment spinblock count. */ + _spinblock_count++; + /*FALLTHROUGH*/ + case PS_DEADLOCK: + case PS_JOIN: + case PS_MUTEX_WAIT: + case PS_WAIT_WAIT: + /* No timeouts for these states. */ + curthread->wakeup_time.tv_sec = -1; + curthread->wakeup_time.tv_nsec = -1; + + /* Restart the time slice. */ + curthread->slice_usec = -1; + + /* Insert into the waiting queue. */ + PTHREAD_WAITQ_INSERT(curthread); + break; + + case PS_COND_WAIT: + case PS_SLEEP_WAIT: + /* These states can timeout. */ + /* Restart the time slice. */ + curthread->slice_usec = -1; + + /* Insert into the waiting queue. */ + PTHREAD_WAITQ_INSERT(curthread); + break; + } /* Switch into the scheduler's context. */ - swapcontext(&curthread->mailbox.tm_context, &_thread_kern_sched_ctx); - DBG_MSG("Returned from swapcontext, thread %p\n", curthread); + DBG_MSG("Calling _thread_enter_uts()\n"); + _thread_enter_uts(&curthread->mailbox, &_thread_kern_kse_mailbox); + DBG_MSG("Returned from _thread_enter_uts, thread %p\n", curthread); /* - * This point is reached when swapcontext() is called + * This point is reached when _thread_switch() is called * to restore the state of a thread. * - * This is the normal way out of the scheduler. + * This is the normal way out of the scheduler (for synchronous + * switches). */ - _thread_kern_in_sched = 0; + /* XXXKSE: Do this inside _thread_kern_scheduler() */ if (curthread->sig_defer_count == 0) { if (((curthread->cancelflags & PTHREAD_AT_CANCEL_POINT) == 0) && @@ -114,128 +226,50 @@ /* Run the installed switch hook: */ thread_run_switch_hook(_last_user_thread, curthread); } + DBG_MSG("Fuck me\n"); } void -_thread_kern_scheduler(void) +_thread_kern_scheduler(struct kse_mailbox *km) { struct timespec ts; struct timeval tv; - struct pthread *curthread = _get_curthread(); - pthread_t pthread, pthread_h; + pthread_t td, pthread, pthread_h; unsigned int current_tick; - int add_to_prioq; + struct thread_mailbox *tm, *p; - /* - * Enter a scheduling loop that finds the next thread that is - * ready to run. This loop completes when there are no more threads - * in the global list. It is interrupted each time a thread is - * scheduled, but will continue when we return. - */ - while (!(TAILQ_EMPTY(&_thread_list))) { + DBG_MSG("entering\n"); + while (!TAILQ_EMPTY(&_thread_list)) { - /* If the currently running thread is a user thread, save it: */ - if ((curthread->flags & PTHREAD_FLAGS_PRIVATE) == 0) - _last_user_thread = curthread; - - /* Get the current time of day: */ + /* Get the current time of day. */ GET_CURRENT_TOD(tv); TIMEVAL_TO_TIMESPEC(&tv, &ts); current_tick = _sched_ticks; - add_to_prioq = 0; - if (curthread != &_thread_kern_thread) { - /* - * This thread no longer needs to yield the CPU. - */ - if (curthread->state != PS_RUNNING) { - /* - * Save the current time as the time that the - * thread became inactive: - */ - curthread->last_inactive = (long)current_tick; - if (curthread->last_inactive < - curthread->last_active) { - /* Account for a rollover: */ - curthread->last_inactive =+ - UINT_MAX + 1; - } - } - - /* - * Place the currently running thread into the - * appropriate queue(s). - */ - switch (curthread->state) { - case PS_DEAD: - case PS_STATE_MAX: /* to silence -Wall */ - case PS_SUSPENDED: - /* - * Dead and suspended threads are not placed - * in any queue: - */ - break; - - case PS_RUNNING: - /* - * Runnable threads can't be placed in the - * priority queue until after waiting threads - * are polled (to preserve round-robin - * scheduling). - */ - add_to_prioq = 1; - break; - - /* - * States which do not depend on file descriptor I/O - * operations or timeouts: - */ - case PS_DEADLOCK: - case PS_JOIN: - case PS_MUTEX_WAIT: - case PS_WAIT_WAIT: - /* No timeouts for these states: */ - curthread->wakeup_time.tv_sec = -1; - curthread->wakeup_time.tv_nsec = -1; - - /* Restart the time slice: */ - curthread->slice_usec = -1; - - /* Insert into the waiting queue: */ - PTHREAD_WAITQ_INSERT(curthread); - break; - - /* States which can timeout: */ - case PS_COND_WAIT: - case PS_SLEEP_WAIT: - /* Restart the time slice: */ - curthread->slice_usec = -1; - - /* Insert into the waiting queue: */ - PTHREAD_WAITQ_INSERT(curthread); - break; - - /* States that require periodic work: */ - case PS_SPINBLOCK: - /* No timeouts for this state: */ - curthread->wakeup_time.tv_sec = -1; - curthread->wakeup_time.tv_nsec = -1; - - /* Increment spinblock count: */ - _spinblock_count++; - - /* FALLTHROUGH */ - } + /* + * Pick up threads that had blocked in the kernel and + * have now completed their trap (syscall, vm fault, etc). + * These threads were PS_RUNNING (and still are), but they + * need to be added to the run queue so that they can be + * scheduled again. + */ + DBG_MSG("Picking up km_completed\n"); + p = km->km_completed; + km->km_completed = NULL; /* XXX: Atomic xchg here. */ + while ((tm = p) != NULL) { + p = tm->tm_next; + tm->tm_next = NULL; + PTHREAD_PRIOQ_INSERT_TAIL((pthread_t)tm->tm_udata); } - last_tick = current_tick; + /* Deliver posted signals. */ + /* XXX: Not yet. */ + DBG_MSG("Picking up signals\n"); - /* - * Wake up threads that have timedout. This has to be - * done after polling in case a thread does a poll or - * select with zero time. - */ + /* Wake up threads that have timed out. */ + DBG_MSG("setactive\n"); PTHREAD_WAITQ_SETACTIVE(); + DBG_MSG("Picking up timeouts (%x)\n", TAILQ_FIRST(&_waitingq)); while (((pthread = TAILQ_FIRST(&_waitingq)) != NULL) && (pthread->wakeup_time.tv_sec != -1) && (((pthread->wakeup_time.tv_sec == 0) && @@ -243,6 +277,7 @@ (pthread->wakeup_time.tv_sec < ts.tv_sec) || ((pthread->wakeup_time.tv_sec == ts.tv_sec) && (pthread->wakeup_time.tv_nsec <= ts.tv_nsec)))) { + DBG_MSG("\t...\n"); /* * Remove this thread from the waiting queue * (and work queue if necessary) and place it @@ -251,6 +286,7 @@ PTHREAD_WAITQ_CLEARACTIVE(); if (pthread->flags & PTHREAD_FLAGS_IN_WORKQ) PTHREAD_WORKQ_REMOVE(pthread); + DBG_MSG("\twaking thread\n"); PTHREAD_NEW_STATE(pthread, PS_RUNNING); PTHREAD_WAITQ_SETACTIVE(); /* @@ -258,119 +294,39 @@ */ pthread->timeout = 1; } + DBG_MSG("clearactive\n"); PTHREAD_WAITQ_CLEARACTIVE(); /* - * Check to see if the current thread needs to be added - * to the priority queue: - */ - if (add_to_prioq != 0) { - /* - * Save the current time as the time that the - * thread became inactive: - */ - current_tick = _sched_ticks; - curthread->last_inactive = (long)current_tick; - if (curthread->last_inactive < - curthread->last_active) { - /* Account for a rollover: */ - curthread->last_inactive =+ UINT_MAX + 1; - } - - if ((curthread->slice_usec != -1) && - (curthread->attr.sched_policy != SCHED_FIFO)) { - /* - * Accumulate the number of microseconds for - * which the current thread has run: - */ - curthread->slice_usec += - (curthread->last_inactive - - curthread->last_active) * - (long)_clock_res_usec; - /* Check for time quantum exceeded: */ - if (curthread->slice_usec > TIMESLICE_USEC) - curthread->slice_usec = -1; - } - - if (curthread->slice_usec == -1) { - /* - * The thread exceeded its time - * quantum or it yielded the CPU; - * place it at the tail of the - * queue for its priority. - */ - PTHREAD_PRIOQ_INSERT_TAIL(curthread); - } else { - /* - * The thread hasn't exceeded its - * interval. Place it at the head - * of the queue for its priority. - */ - PTHREAD_PRIOQ_INSERT_HEAD(curthread); - } - } - - /* * Get the highest priority thread in the ready queue. */ + DBG_MSG("Selecting thread\n"); pthread_h = PTHREAD_PRIOQ_FIRST(); /* Check if there are no threads ready to run: */ - if (pthread_h == NULL) { - /* - * Lock the pthread kernel by changing the pointer to - * the running thread to point to the global kernel - * thread structure: - */ - _set_curthread(&_thread_kern_thread); - curthread = &_thread_kern_thread; - - DBG_MSG("No runnable threads, using kernel thread %p\n", - curthread); - - /* - * There are no threads ready to run, so wait until - * something happens that changes this condition: - */ - thread_kern_idle(); - - /* - * This process' usage will likely be very small - * while waiting in a poll. Since the scheduling - * clock is based on the profiling timer, it is - * unlikely that the profiling timer will fire - * and update the time of day. To account for this, - * get the time of day after polling with a timeout. - */ - gettimeofday((struct timeval *) &_sched_tod, NULL); - - /* Check once more for a runnable thread: */ - pthread_h = PTHREAD_PRIOQ_FIRST(); - } - - if (pthread_h != NULL) { + if (pthread_h) { + DBG_MSG("Scheduling thread\n"); /* Remove the thread from the ready queue: */ PTHREAD_PRIOQ_REMOVE(pthread_h); /* Make the selected thread the current thread: */ _set_curthread(pthread_h); - curthread = pthread_h; /* * Save the current time as the time that the thread * became active: */ current_tick = _sched_ticks; - curthread->last_active = (long) current_tick; + pthread_h->last_active = (long) current_tick; /* * Check if this thread is running for the first time * or running again after using its full time slice * allocation: */ - if (curthread->slice_usec == -1) { + if (pthread_h->slice_usec == -1) { /* Reset the accumulated time slice period: */ - curthread->slice_usec = 0; + pthread_h->slice_usec = 0; } /* @@ -378,19 +334,47 @@ * installed switch hooks. */ if ((_sched_switch_hook != NULL) && - (_last_user_thread != curthread)) { + (_last_user_thread != pthread_h)) { thread_run_switch_hook(_last_user_thread, - curthread); + pthread_h); } /* * Continue the thread at its current frame: */ - swapcontext(&_thread_kern_sched_ctx, - &curthread->mailbox.tm_context); + _last_user_thread = td; + DBG_MSG("switch in\n"); + _thread_switch(&pthread_h->mailbox, + &_thread_kern_kse_mailbox.km_curthread); + DBG_MSG("switch out\n"); + } else { + /* + * There is nothing for us to do. Either + * yield, or idle until something wakes up. + */ + DBG_MSG("No runnable threads, idling.\n"); + + /* + * kse_yield() only returns if we are the + * only thread in this process. If so, then + * we drop into an idle loop. + */ + kse_yield(); + thread_kern_idle(); + + /* + * This thread's usage will likely be very small + * while waiting in a poll. Since the scheduling + * clock is based on the profiling timer, it is + * unlikely that the profiling timer will fire + * and update the time of day. To account for this, + * get the time of day after polling with a timeout. + */ + gettimeofday((struct timeval *) &_sched_tod, NULL); } + DBG_MSG("looping\n"); } - - /* There are no more threads, so exit this process: */ + /* There are no threads; exit. */ + DBG_MSG("No threads, exiting.\n"); exit(0); } @@ -401,10 +385,10 @@ /* * Flag the pthread kernel as executing scheduler code - * to avoid a scheduler signal from interrupting this - * execution and calling the scheduler again. + * to avoid an upcall from interrupting this execution + * and calling the scheduler again. */ - _thread_kern_in_sched = 1; + _thread_kern_kse_mailbox.km_curthread = NULL; /* Change the state of the current thread: */ curthread->state = state; @@ -423,10 +407,10 @@ /* * Flag the pthread kernel as executing scheduler code - * to avoid a scheduler signal from interrupting this - * execution and calling the scheduler again. + * to avoid an upcall from interrupting this execution + * and calling the scheduler again. */ - _thread_kern_in_sched = 1; + _thread_kern_kse_mailbox.km_curthread = NULL; /* Change the state of the current thread: */ curthread->state = state; @@ -439,6 +423,13 @@ _thread_kern_sched(); } +/* + * XXX - What we need to do here is schedule ourselves an idle thread, + * which does the poll()/nanosleep()/whatever, and then will cause an + * upcall when it expires. This thread never gets inserted into the + * run_queue (in fact, there's no need for it to be a thread at all). + * timeout period has arrived. + */ static void thread_kern_idle() { @@ -460,6 +451,8 @@ /* * Either there are no threads in the waiting queue, * or there are no threads that can timeout. + * + * XXX: kse_yield() here, maybe? */ PANIC("Would idle forever"); } ==== //depot/projects/kse/lib/libc_r/uthread/uthread_priority_queue.c#3 (text+ko) ==== @@ -68,9 +68,9 @@ if (((thrd)->flags & _PQ_IN_SCHEDQ) != 0) \ PANIC(msg); \ } while (0) -#define _PQ_ASSERT_PROTECTED(msg) \ - PTHREAD_ASSERT((_thread_kern_in_sched != 0) || \ - ((_get_curthread())->sig_defer_count > 0), \ +#define _PQ_ASSERT_PROTECTED(msg) \ + PTHREAD_ASSERT((_thread_kern_kse_mailbox.km_curthread == NULL) || \ + ((_get_curthread())->sig_defer_count > 0), \ msg); #else To Unsubscribe: send mail to majordomo@FreeBSD.org with "unsubscribe p4-projects" in the body of the message