First revision: March 29, 1988 Protocol Scheduler The CAP protocol libraries are designed so that protocols run based on two types of events. The first type is incoming packets/input ready and the second is timeouts. These are "scheduled" or handled by giving up the CPU for a specified amount of time during which these events are awaited and processed. Thus, these events run non-preemptively. Because of this, there's a lot less worry about critical sections, etc. (Warning: this means running protocol from signal handlers is a bad idea since the signal handler may have been invoked inside the protocol scheduler). Both input ready and timeout routines should follow these rules: o don't block for any significant amounts of time (>.25 seconds is a good rule of thumb) o don't call abSleep or doTimeout SCHEDULING o int abSleep(giveup time, until_first) giveup time - time in tick (1/4 second units) until_first - TRUE/FALSE abSleep runs protocol events. If until_first is set to TRUE, then abSleep will return after the first (set) of protocol events occur. In this case the giveup time specifies the maximum time abSleep would keep control. Warning: more than one protocol event may occur even if until_first is set. The basic algorithm is: while giveup time is still valid sleep until input ready or Timeout if input ready then get all input else if Timeout then run all timeouts if event occurred and until_first set, then return end There should probably be a version of abSleep that accepts absolute/relative times via struct timeval to allow better control, but there isn't a need yet... INTERNAL CLOCK: An internal clock is used to redefine relative times as absolute (system TOD based) times to minimize the effects of scheduling, etc. (e.g. we can't trust relative times in a timeshared environment). o getschedulerclock(struct timeval **tv) getschedulerclock returns the current scheduler reference clock setting. (Pass pointer to timeval pointer and it will return pointer to the reference clock.) Even though you can update the reference clock through this pointer, you shouldn't unless you know what you are doing. o updateschedulerclock() updates the scheduler reference clock settting (safely). EVENT: INPUT Input protocol event handling below the user level is generally done on the basis of incoming packets. At the user level, it is possible to specify non-protocol file descriptors for input protocol event handling. This is useful when you don't want to block on input because protocol must be scheduled--it's silly to make the user level program have to another set of selects. o int init_fdlistening(); Initialize the file descriptor listening code. This is generally done in abInit. Calling init_fdlistening twice is okay -- the second call is ignored. Returns: number of file descriptors that it can handle. The rest of the calls return -1 for error and 0 for success. o int fdlistener(int fd, int (*listener)(), caddr_t addrarg, int intarg) Install the listener "listener" that is to be called with the arguments "addrarg" and "intarg" when input is ready on the file descriptor "fd". Note: fdlistenread is used to call the listener. If "listener" is NULL, input ready waits are still done on the file descriptor, but the no listener will be called. The addrarg and intarg distinction is made in case a pointer is not necessarily an int on a particular machine. The reason for having both is that it is useful to be able to pass an array/buffer with a size to "read" routines. The read listener should not do more than a single read call unless it is know that sufficient data is there to satisfy it or else things will block. Extreme care should be taken when using stdio since the read system call is not under user control. o int fdunlisten(int fd) Remove any listeners on file descriptor "fd". Returns error if no listener was installed. o int fdlistenread(int fd) Calls the listener for file descriptor "fd" with the argument saved by fdlistener. Included at the user level to allow a forced "read" call. o int fdlistensuspend(int fd) Suspend input ready check on file descriptor "fd". Returns error if not installed by fdlistener. Note: this can be called from inside a "fd" listener and it will take effect immediately. o int fdlistenresume(int fd) Resume input ready check on file descriptor "fd". Returns error if not installed by fdlistener. Note: this can be called from inside a "fd" listener and it will take effect immediately. fdlistensuspend and fdlistenresume are useful to when the listener doesn't actually read the data, but instead just marks that input is ready. For example: waitforinput(fd, waitvar, dummy) int fd; int *waitvar; int dummy { (void)fdlistensuspend(fd); *waitvar = 1; } /* calling sequence */ fdlistener(fileno(stdin), waitforinput, &waitvar, 0); do { /* okay to do even if not suspended */ fdlistenresume(fileno(stdin)); waitvar = 0; do { abSleep(4, TRUE); /* run protocol events */ } until (waitvar); read input on stdin } while (not end of file on stdin) EVENT: TIMEOUTS Timeouts are used to schedule a procedure to "run" at a later point. Timeouts are always stored internally in absolute time format (see the notes on the internal clock above). Due to the nature of the environment, timeouts can only occur near the times specified. Things are designed so that timeouts should never occur before the time specified. o void Timeout(void (*fun)(), caddr_t arg, int timeout) o void AppleTimeout(void (*fun)(), caddr_t arg, int timeout, boolean doupdate) o void relTimeout(void (*fun)(), caddr_t arg, struct timeval *tv, boolean doupdate)) o void absTimeout(void (*fun)(), caddr_t arg, struct timeval *tv) Timeout schedules "fun" to be called with argument "arg" at "timeout" ticks in the future. The call will never occur before "timeout" ticks. It will only occur near timeout ticks if abSleep is called around that time. Thus, if timeouts are critical, then it is important to call "abSleep" often. AppleTimeout is actually the basis for "Timeout". It takes as an additional argument "doupdate" that, if FALSE, notifies the timeout code that the internal clock should be considered "accurate" and need not be updated. One may make this assumption if AppleTimeout or relTimeout (see below) was called just before with doupdate TRUE with little or no code intervening. Note: Timeout just calls AppleTimeout with doupdate TRUE. relTimeout replaces the Apple tick time with "struct timeval" set to the interval (relative time). This allows us to schedule to down to microseconds (be warned, the resolution of your interval timer is unlikely to be that accurate! It is even more unlikely you can get your scheduler to guarantee that you will be scheduled at microsecond accuracy. Don't count on better than about 1/10 second or so). See AppleTimeout above for information on the doupdate flag. absTimeout is like relTimeout except it takes an absolute time and thus removes the need for the "doupdate" flag. Note: two or more timeouts scheduled for the same time will occur in an indeterminate order and within the same "abSleep" call. o int remTimeout(void (*fun)(), caddr_t arg) remTimeout removes a timeout to call function "fun" with argument "arg". remTimeout will only remove the first instance of the pair that it finds. remTimeout returns true if it removed the pair, false otherwise. So to guarantee that you have removed all instances, you can (a) make sure you only have one outstanding at a time or (b) use the following code fragment: while (remTimeout(fun,arg)) /* NULL STATEMENT */; o boolean minTimeout(struct timeval *timeval) minTimeout will return in "timeval" the time that the "next" timeout is scheduled to occur. Note: it may be in the past if the scheduler has not run. It returns FALSE if there are no timeouts enqueued. o int doTimeout() doTimeout is included to allow forcing of timeouts without running the "input" events. If doTimeout is used, it should probably be used in conjunction with "minTimout" to minimize the work.