If you look at the code for releasing a lock, you will see that it does not explicitly enable interrupts. Instead it uses the function popcli
.
void release ( struct spinlock* lk )
{
...
popcli(); // enable interrupts
}
The function popcli
does not always enable interrupts. It is used in tandem with pushcli
to track nesting level. "Pushcli/popcli are like cli/sti except that they are matched: it takes two popcli to undo two pushcli". 1
void popcli ( void )
{
// If interrupts are enabled, panic...
if ( readeflags() & FL_IF )
{
panic( "popcli: interruptible" );
}
// Track depth of cli nesting
mycpu()->ncli -= 1;
// Popped more than were pushed...
if ( mycpu()->ncli < 0 )
{
panic( "popcli" );
}
// Reached outermost, so restore interrupt state
if ( mycpu()->ncli == 0 && mycpu()->intena )
{
sti(); // enable interrupts
}
}
Whereas popcli
sometimes enables interrupts, pushcli
always disables interrupts.
void pushcli ( void )
{
int eflags;
eflags = readeflags();
// Disable interrupts
cli();
// Save interrupt state at start of outermost
if ( mycpu()->ncli == 0 )
{
mycpu()->intena = eflags & FL_IF;
}
// Track depth of cli nesting
mycpu()->ncli += 1;
}
By explicitly calling sti
, the scheduler overrides whatever the current push/popcli state is. I think this provides the brief window desired to allow an IO interrupt to occur. I.e. the period of time between the call to sti
and the call to cli
(via acquire
-> pushcli
-> cli
).
void scheduler ( void )
{
...
for ( ;; )
{
// Enable interrupts on this processor.
sti();
// Acquire process table lock
acquire( &ptable.lock );
// Loop over process table looking for process to run.
for ( p = ptable.proc; p < &ptable.proc[ NPROC ]; p += 1 )
{
...
}
// Release process table lock
release( &ptable.lock );
}
}