Here’s an ELI5 (Explain Like I’m 5) version of what the scheduler() function in xv6 is doing:

What is a scheduler?

Imagine a classroom with a bunch of students (processes) but only one teacher (CPU). The teacher can only help one student at a time, so she needs a way to pick which student to help next. That’s what the scheduler does — it chooses which process gets to use the CPU next.

Code Walkthrough

Here’s the code we’re talking about:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();  // get the current CPU
  c->proc = 0;

  for(;;){
    sti(); // allow interrupts

    acquire(&ptable.lock);

    int found = 0;

    for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
      if(p->state != RUNNABLE)
        continue;

      found = 1;

      c->proc = p;
      switchuvm(p);
      p->state = RUNNING;

      swtch(&c->scheduler, p->context);
      switchkvm();

      c->proc = 0;
    }

    if(!found){
      release(&ptable.lock);
      sti();
      asm volatile("hlt");
      continue;
    }

    release(&ptable.lock);
  }
}

What’s happening step-by-step?

1
2
void 
scheduler(void)

This is the main loop for choosing and running processes. It runs forever.

The two-line style is a nod to older Unix/C practices, emphasizing readability on narrow terminals and clean vertical formatting (the names of functions are always at the start of lines). It’s not required by the language, just a style that’s still respected in minimalist or historically-inspired codebases like xv6.

In most modern C you would see:

1
void scheduler(void)

Which is more compact and familiar to today’s programmers.


Setup

1
2
3
struct proc *p;
struct cpu *c = mycpu();  // which CPU is running this scheduler
c->proc = 0;              // no process is running yet
  • We get the current CPU (c) that’s doing the scheduling.
  • We say: “You’re not running any process yet.”

Loop forever

1
for(;;) {

This is an infinite loop: we’ll keep checking for runnable processes forever.


Allow interruptions

1
sti();

This turns on interrupts, letting the CPU respond to things like keyboard input, timers, etc.


Lock the process table

1
acquire(&ptable.lock);

We’re about to look at the list of processes, so we lock it so no one else changes it while we’re looking.


Search for a process to run

1
2
3
4
int found = 0;
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
    if(p->state != RUNNABLE)
        continue;

We go through each process in the system. If it’s not ready to run, we skip it.


🏃‍♂️ Run it!

If it is runnable:

1
2
3
4
5
6
7
8
found = 1;
c->proc = p;         // tell the CPU we're running this process
switchuvm(p);        // switch to that process’s memory
p->state = RUNNING;  // mark it as running

swtch(&c->scheduler, p->context);  // switch to the process
switchkvm();        // switch back to kernel memory when done
c->proc = 0;        // we’re done running this process

We:

  • Say “this CPU is now running process p”
  • Set up its memory
  • Switch the CPU to actually run it
  • When it’s done (e.g. goes to sleep or gives up control), we come back
  • Mark the CPU as not running anything now

💤 Nothing to do? Go to sleep.

1
2
3
4
5
6
if(!found){
  release(&ptable.lock);
  sti();
  asm volatile("hlt");
  continue;
}

If we didn’t find any process to run, we:

  • Unlock the process table
  • Allow interrupts
  • Use the hlt instruction to put the CPU to sleep until something wakes it (like a timer or input)

This saves CPU power instead of spinning in a loop doing nothing.


Unlock and repeat

1
release(&ptable.lock);

If we ran something or not, we unlock the process table and go back to the top of the loop.


Summary:

This function:

  • Loops forever
  • Looks through all processes to find one ready to run
  • Switches to that process
  • Sleeps the CPU if nothing is ready
  • Repeats