Tuesday, December 3, 2013

sleep timers and process scheduling in Xinu

Having successfully switched my main system clock to a handler hooked to the RTC via IRQ8, I was able to provide basic system timing functionality at only 8% CPU usage by the virtual machine in which I was running Xinu. Having reduced this CPU wastage from 100% to 15%, and now again to 8%, I felt quite satisfied. As far as I can tell, there are no other regular interrupts being handled by Xinu, and I suspect that the remaining 8% CPU usage is actually the work performed by the virtual machine to emulate the PIT, RTC, and other hardware timers and resources.
However, my work was not yet quite complete. Without an interrupt-based millisecond timer, my modified version of Xinu would be unable to properly service the millisecond sleep()s offered by the sleepms() API provided in sleep.c.

Studying the process creation, scheduling, preemption, wakeup, and context-switching design of Xinu, I found that Xinu's current scheduling system is basically an absolute-priority system with only no situations under which a process may ordinarily make way for another, lower-priority process. For a process to be chosen to run, it must be at the front of the ready list. The readylist is ordered strictly by process priority, and that order is defined during process insertion. When processes are resume()d, the new process is inserted by ready() into the ready list according to its priority, as previously defined by either create() or chprio(). During a resched(), the previously running process is reinserted into the ready list according to its priority, and the first process in the readylist is removed and given the CPU. Surprisingly, even the chprio() method does not remove and re-insert the process into the ready list, meaning that the process' priority will only functionally change once it has had the opportunity to become the highest priority (i.e. ended up at the front of the readylist) process available to run and then been reinserted into the ready list.

Semaphores and signals (using send()), along with resume() and unsleep() are all methods of causing a resched() and possibly a context switch. However, since all of these can only happen by the will of a an already running process, since no process with a lower priority than the currently running process would ever be chosen by the scheduler, and since no higher priority process can be added to the readylist without one of those four functions being called by the currently running process, there is no need to run a periodic preemption timer, as the currently-running process is always the highest priority process that is available to run anyway.

This means that there in the current Xinu architecture, all that would be needed to complete my low-power Xinu build is a millisecond timer that is active while there are sleeping processes, and deactivated as soon as the sleeping queue is empty again. This was easily accomplished by embedding a call in sleep() to a function that unmasks IRQ0, and embedding a call in wakeup() that masks IRQ0 interrupts if there are no processes in the sleeping queue. Thus, as soon as a process requests to sleep, the timer is activated and regular wakeups commence.

The resulting code in clkinit.c and clkint.S is basically as follows:


void clkinit(void) {
    set_evec(IRQBASE + 8, (uint32)rtcint); /*** set handler for IRQ8 (RTC) */

    sleepq = newqueue();    /* allocate a queue to hold the delta   */
    preempt = QUANTUM;      /* initial time quantum         */
    slnonempty = FALSE;
    clktime = 0;            /* start counting seconds       */

    intv = 15; /*** this should give us a clock rate of 2 Hz */

    /*** first, write the desired rate into the bottom 4 bits of register A */
    outb(RTC_CMD, RTC_REG_A | RTC_NMI);
    outb(RTC_CMOS, (char)intv);

    /*** then, enable IRQ8 to start the clock */
    /*** by writing 0x40 to enable bit 6 of register B */
    outb(RTC_CMD, RTC_REG_B | RTC_NMI); /*** set the register index to 'B' */
    char reg_B_val = inb(RTC_CMOS);
    outb(RTC_CMD, RTC_REG_B | RTC_NMI); /*** CMOS reads reset the register index to 'D' */
                              /*** so we need to reset it to 'B' */
    outb(RTC_CMOS, reg_B_val | 0x40); /*** this enables the firing of interrupts */

    rtc_nmi_enable(); /*** not sure this is necessary, but may as well */

    /*** now set up Intel 8254-2 (PIT) */
    /*  set to: timer 0, 16-bit counter, rate generator mode,
        counter is binary */

    outb(CLKCNTL, SET_CLOCK0_AS_BINARY_TIMER);

    intv = 1193182/CLKTICKS_PER_SEC;

    /* must write LSB first, then MSB */
    outb(CLOCK0, (char)intv);
    outb(CLOCK0, intv>>8);
    return;
}



clkint:
    pushal
    cli
    movb    $EOI,%al
    outb    %al,$OCW1_2 
    cmpl    $0,slnonempty   # if no sleeping processes,
    je      clpreem     #   skip to preemption check
    movl    sltop,%eax  # decrement key of first
    decl    (%eax)      #   sleeping process
    jg      clpreem     # must use jg for signed int
    call    wakeup      # if zero, call wakeup
clpreem:
    decl    preempt     # decrement preemption counter
    jg      clret       # must use jg for signed int
    call    resched     # if preemption, call resched
clret:                  # return from interrupt
    sti
    popal
    iret

rtcint: /*** this clock interrupt happens @ 2Hz */
    pushal
    cli
    mov     $EOI,%al
    outb    %al,$OCW1_2
    mov     $EOI,%al
    outb    %al,$OCW2_2 /*** must restore both PICs since this is a second-PIC interrupt */

    /*** to keep the interrupts enabled, we must read RTC register C */
    movb    $RTC_REG_C,%al
    outb    %al,$RTC_CMD
    inb     $RTC_CMOS   /*** currently, we're uninterested in the actual value */

    subw    $1,count2
    ja      cl1     /*** if we're in between half-second ticks, just return */
    incl    clktime
    movw    $2,count2   /*** reset down-counter */
rtcret:
    sti
    popal

    iret


Even better, this low-power system is adaptable to preemptive schedulers with some basic added logic that would intercept all calls to chprio() and the other routines that might possibly affect which process should be currently chosen to run, and temporarily enabling the periodic millisecond wakeups in order to service these situations where preemption is desirable. Best of all, the system would still be able to return to its low-power configuration any time all processes were in a waiting queue for interrupt-signaled I/O, for semaphores, or other inter-process signalling.

No comments:

Post a Comment