嘉宏 Alvin
又歆 Yuhsin
佳晉 JohnnyThe lab has five tasks. Each task is on its own branch of the lab repository, with its own spec. In every task you write a scheduling policy in kernel/sched.c and connect it to the timer in kernel/trap.c. Do the tasks in order: Task 5 uses the pickers that you write in Tasks 1–4.
| Task | Branch | Scheduler | Points | Public tests |
|---|---|---|---|---|
| Task 1 | q0 | Round robin | 10 | 5 |
| Task 2 | q1 | Shortest estimated time left, no preemption | 10 | 5 |
| Task 3 | q2 | Priority with aging, preemptive | 10 | 7 |
| Task 4 | q3 | First come, first served | 10 | 4 |
| Task 5 | q4 | Multilevel feedback queue with four queues | 60 | 20 |
Deadline: 2026/11/10 23:59 (UTC+8). Upload your zip to the eLearn assignment by then (Part 10).
Every command in this lab runs in the course container. It has the RISC-V compiler, QEMU, gdb and the grading script. The TAs grade in the same container.
git clone https://github.com/GaGa55555LaLa/os-xv6-lab-student.git
cd os-xv6-lab-studentdocker build -t xv6-lab .devcontainerdocker run -it --rm --name xv6 \
-u "$(id -u):$(id -g)" \
-v "$PWD":/xv6 -w /xv6 \
xv6-lab bash
On Windows, in PowerShell:
docker run -it --rm --name xv6 `
-v "${PWD}:/xv6" -w /xv6 `
xv6-lab bash./lab shell builds the image if needed and starts the container. ./lab grade and ./lab make qemu run one command in it. These work on Linux, macOS, and in Git Bash or WSL on Windows.exit. Your files stay in the repository folder on your computer../grade prints env: $'python3\r': No such file or directory, commit your changes first, then run:
git config core.autocrlf false
git rm -r -q --cached .
git reset -q --hardmake qemu # build and boot xv6 on one CPU; quit with Ctrl-a, then x
$ schedcheck # in xv6: run every small test (mlfqcheck in Task 5)
$ schedcheck late # run one test
$ schedtest 42 # a larger workload with seed 42, drawn as a Gantt chart
./grade # in the container shell: build, run every test, compare
./grade late wake # only some tests
git checkout q0 # switch to Task 1
git commit -am "q0: round robin" # commit your work on this branch
git show q0:kernel/sched.c # read a file of another branch without switching
git show origin/q1:kernel/sched.c # the same, for a branch you have not checked out yet
The prompt in the container shows the current branch, for example /xv6 (q0) $. Commit your work before you switch branches. The TAs grade the last commit on each branch; uncommitted changes are not graded.
user/ (you may add your own tests there), mkfs/, Makefile, kernel/param.h, kernel/syscall.h, kernel/schedlog.h, grade, test-xv6.py, .devcontainer/, lab and .gitattributes.schedlog() or logat() call. These calls record the events that the tests compare (Part 8).xv6 keeps every process in a fixed array, proc[NPROC], in kernel/proc.c. Each entry is a struct proc (kernel/proc.h). The field p->state holds the state of the process. An entry whose state is UNUSED is free. allocproc() finds a free entry and gives it the next pid, so a process created later has a larger pid.
The xv6 states have different names from the ones in the lecture:
| xv6 state | Lecture name | Meaning |
|---|---|---|
USED | New | allocproc() has reserved the entry, but the process cannot run yet. |
RUNNABLE | Ready | The process can run and waits for the CPU. |
RUNNING | Running | The process is on the CPU now. |
SLEEPING | Waiting | The process waits for an event, such as the end of a pause(). |
ZOMBIE | Terminated | The process has exited. Its parent has not collected it with wait() yet. |
Each transition is caused by a few kernel functions and records an event in the test log (Part 8). These six paths are the Trace Code paths that the lab quiz covers.
The specs talk about a "ready queue", but xv6 has no queue data structure. The ready queue is the set of entries in proc[] whose state is RUNNABLE. In q4, queue Qk is the set of RUNNABLE entries whose level is k.
The order of a queue is not stored either. It comes from fields of struct proc, for example enter_tick (the tick at which the process joined the ready queue) and pid. A picker such as pick_rr() returns the process at the head of a queue. It calls pick_by() in kernel/sched.c, which loops over proc[] and keeps the best entry so far. Part 8 shows a queue changing tick by tick.
The lab runs xv6 on one CPU: the Makefile fixes CPUS to 1. So at most one process is RUNNING at a time.
A trap stops the instructions that the CPU is running and jumps to a handler in the kernel. A system call, an exception (such as a page fault) and a device interrupt are all traps. The timer is a device that raises interrupts at times the kernel chooses.
Every timer interrupt reaches clockintr(), which adds 1 to the global counter ticks. One tick, the time between two timer interrupts, is the unit of time in this lab. Every count in the specs, such as a time slice, an allotment or a waiting time, is a number of ticks.
The register stvec holds the address of the trap handler. xv6 changes it when the CPU moves between user mode and kernel mode. So a timer interrupt reaches clockintr() by one of two paths, depending on what the CPU was doing. A test job burns CPU time by calling the system call cputicks() in a loop, so the interrupt can find it in user mode or in the kernel.
The lab connects the timer to the scheduler with two functions in kernel/sched.c. You add the calls to them in kernel/trap.c (TODO (trap)).
sched_tick() does the accounting for the tick that has just ended. It charges the tick to the running process, adds 1 to the waiting time of every RUNNABLE process, and wakes up every pause() that is due. It is called in clockintr(), right after ticks++.sched_timer() does the preemption check. It calls should_preempt(). If should_preempt() returns 0, the running process continues. Otherwise the process gives up the CPU. It is called in usertrap() and kerneltrap(), after devintr() has returned 2 (a timer interrupt).Since clockintr() runs inside devintr(), the accounting always happens before the preemption check. Until sched_tick() is connected, ticks still grows but no pause() ever wakes up, so every test hangs. When the interrupt finds the CPU in the scheduler loop, myproc() returns 0: there is no process to check.
For the lab quiz. The Trace Code section of q4-spec.md asks how long one tick is, which line in clockintr() stops the interrupt from staying pending, and what happens when you remove that line. Find the answers in kernel/trap.c and by running the experiment. The full list of quiz topics is in Part 10.
Kernel code often reads and changes data that other kernel code also uses, such as the fields of a process. Two pieces of code must not change the same data at the same time, or one of them may see it half updated. On a computer with several CPUs, two CPUs can run kernel code at once. Even on one CPU, a timer interrupt can stop kernel code in the middle of an update and run other kernel code. A lock makes such code take turns.
xv6 uses spin locks (struct spinlock, kernel/spinlock.c). acquire(&lk) waits until no other CPU holds lk, and then holds it. release(&lk) gives it back. The code between the two calls is a critical section: while one CPU is in it, no other CPU can enter a critical section of the same lock. For example, kfork() in kernel/proc.c makes a new process runnable like this:
acquire(&np->lock);
np->state = RUNNABLE;
np->enter_tick = ticks;
release(&np->lock);
Code that reads these fields also holds np->lock. So it sees either the old values or the new ones, never a RUNNABLE process with an old enter_tick.
| Lock | How many | Protects |
|---|---|---|
p->lock | one per process | the state of the process and its scheduling fields, such as state, chan, level, enter_tick, slice_used and wait_ticks |
tickslock | one | the global counter ticks |
The comments in struct proc (kernel/proc.h) say which lock protects each field.
acquire() also turns interrupts off on the CPU, and they stay off until the CPU has released every lock it holds. So code that holds a spin lock cannot be interrupted by the timer. Without this, the timer handler could try to acquire a lock that the interrupted code already holds, and wait for it forever. For example, clockintr() acquires tickslock, which sys_pause() also holds while it reads ticks.
This lab runs on one CPU, but the locks still matter: the kernel checks how they are used and panics when a rule is broken. Part 5 shows the checks in sched(). Three rules follow:
acquire() of the same lock panics with acquire. The pickers and top_level() acquire the lock of every process in turn. So do not call them while you hold any p->lock.release() panics with release.A process that must wait for an event sleeps: it sets its state to SLEEPING and gives up the CPU. A process gives up the CPU by calling sched(); Part 5 shows what happens inside it. When the event happens, other kernel code calls wakeup(chan). The channel chan is an address that names the event.
Sleeping takes two steps:
sleep_prepare(chan) stores chan in p->chan.sleep() sets the state to SLEEPING and calls sched(), but only if p->chan is still set.wakeup(chan) clears p->chan in every process that waits on chan, and calls sched_wakeup() for each one that is already SLEEPING. The two steps prevent a lost wakeup: if the event happens between the two steps, p->chan is already cleared, so sleep() returns at once instead of sleeping forever.
pause()pause(n) (sys_pause() in kernel/sysproc.c) sets p->wake_tick to the current tick plus n and sleeps on the channel &p->wake_tick. At every tick, sched_tick() wakes up each process whose wake_tick has come. So a process in pause(n) sleeps for n ticks and wakes up once. The test programs use pause() to stand for I/O.
sleep() calls sched_sleep(p) just before sched(). For the scheduler, this is the end of a CPU burst: the time a process runs until it blocks.sched_wakeup(p) sets the state to RUNNABLE, records the tick at which the process joins the ready queue, and resets its waiting time.schedstart(), which sleeps until the start tick of the job. When that sleep ends, sched_wakeup() records a new event instead of a wake event.kexit() closes the files of the process, gives its children to init, wakes up its parent, sets its state to ZOMBIE, and calls sched(). It never returns. The parent's wait() later finds the ZOMBIE child and frees its entry in proc[].
Each process has a kernel thread: a kernel stack (p->kstack) and a set of saved registers (p->context). When a process makes a system call or is interrupted, the kernel code runs on the kernel stack of that process. The CPU also has a scheduler thread, which runs scheduler() on its own stack. Its saved registers are in cpu->context.
swtch(old, new) in kernel/swtch.S saves the current registers in old and loads the registers in new, including the stack pointer sp and the return address ra. So the ret at the end of swtch returns into the other thread, at the point where that thread last called swtch.
xv6 never switches directly from one process to another. A process calls sched(), which switches to the scheduler thread. The scheduler thread selects the next process and switches to it. In the example below, process A calls pause() and goes to sleep, and process B, which slept earlier and has been woken up, runs next.
The loop in scheduler() (kernel/proc.c) is short:
for (;;) {
intr_on(); // a pending interrupt is taken here,
intr_off(); // then interrupts are off again
if ((p = sched_pick()) != 0) { // returns p locked and RUNNABLE
p->state = RUNNING;
p->slice_used = 0;
p->wait_ticks = 0;
schedlog(p, SEV_RUN);
c->proc = p;
swtch(&c->context, &p->context);
c->proc = 0; // p has left the CPU
release(&p->lock);
} else {
asm volatile("wfi"); // nothing to run: wait until an interrupt is pending
}
}
Simplified from kernel/proc.c: the variable found, the line mycpu()->intena = 0; and the comments are left out.
A process that has never run has no earlier call to sched(). allocproc() sets its context.ra to forkret, so the first swtch() to it starts forkret(), which releases p->lock and returns to user space.
sched() checkssched() checks four conditions before it calls swtch(), and panics if one of them fails. These panics are the most common crashes in this lab.
| Panic message | Meaning |
|---|---|
sched p->lock | The caller does not hold p->lock. |
sched locks | The caller holds another lock in addition to p->lock. |
sched RUNNING | p->state is still RUNNING. The caller must set the new state first. |
sched interruptible | Interrupts are on. |
Why is p->lock held during the switch? p->state and p->context must change together. If the lock were released before swtch(), the process would be RUNNABLE while its registers are only half saved, and another CPU could start to run it. So the lock is acquired in one thread and released in another, as the stepper shows.
The scheduling policy is in kernel/sched.c. The rest of the kernel calls it through a few functions, the hooks. The TODO comments in kernel/sched.c and kernel/trap.c on each branch mark exactly what to change. In every task you also add the calls to sched_tick() and sched_timer() in kernel/trap.c (TODO (trap)).
| Hook | Called by | When | Locks held on entry | You change it in |
|---|---|---|---|---|
sched_pick() | scheduler() | The CPU needs a process to run. | none; returns the process with its lock held | Task 5 |
sched_sleep(p) | sleep() | p blocks: its CPU burst ends. | p->lock | Task 5 |
sched_wakeup(p) | wakeup(), sched_tick(), kkill() | p goes from SLEEPING to RUNNABLE. | p->lock | Task 5 |
sched_tick() | clockintr() (TODO trap) | Every tick, after ticks++. | none; locks each process in turn | Task 5 (also charge(), which it calls) |
sched_timer() | usertrap(), kerneltrap() (TODO trap) | A timer interrupt while a process runs. | none | every task (TODO (yield)) |
should_preempt(p) | sched_timer() | Same as above. | none | every task |
pick_rr(), pick_shortest(), pick_priority(), pick_fifo() | sched_pick(), should_preempt() | Find the head of a queue. | none; return the process unlocked | one picker in each of Tasks 1–4; all four are copied into Task 5 |
A picker returns its result without the lock. Before it uses the result, the caller must lock the process and check that its state is still RUNNABLE, as sched_pick() does.
The lab adds these fields to struct proc. All of them are protected by p->lock.
| Field | Meaning | Changed by the provided code |
|---|---|---|
level | The queue of the process (q4; always 0 in q0–q3). | allocproc(), schedstart() |
priority | 0 (highest) to 9 (lowest). It never changes. | allocproc(), schedstart() |
burst_est | Estimated length of the current CPU burst, in ticks. | allocproc(), schedstart() |
cur_burst | Ticks run in the current CPU burst. | +1 in sched_tick(); 0 in sched_sleep() |
slice_used | Ticks run since the process was last picked. | +1 in sched_tick(); 0 in scheduler() |
allot_used | Ticks run in the current queue during this burst (q4). | allocproc(), schedstart() |
wait_ticks | Ticks spent RUNNABLE since the process last ran. | +1 in sched_tick(); 0 in scheduler() and sched_wakeup() |
enter_tick | The tick at which the process joined the ready queue. | userinit(), kfork(), sched_wakeup(), schedstart() |
rticks | Total ticks run. The system call cputicks() returns it. | +1 in sched_tick() |
demoted | The process moved down a queue during the last tick (q4). | allocproc() |
Before you write code for a task, answer its Think first questions. Open Where to look if you are stuck.
Processes take turns. Each one runs for at most a short, fixed time, then goes to the end of the ready queue, and the next one gets the CPU.
RR_SLICE ticks (2, in kernel/param.h).| Case | Result |
|---|---|
| p has not used up its time slice | keep running |
p has used up its time slice, and another process is RUNNABLE | give up the CPU, go to the end of the ready queue |
p has used up its time slice, and no other process is RUNNABLE | keep running, and start a new time slice |
should_preempt() must do it.pause()) gives up the CPU by itself. You do not handle that.TODO (trap) reports, the time slice of the running process grows, and so does the waiting time of every RUNNABLE process. A pause() that is due wakes up.TODO (trap) in kernel/trap.c: tell the scheduler about every tick, and on a timer interrupt let sched_timer() check whether the running process gives up the CPU. Do the accounting first, then the check. A timer interrupt in the kernel may come when no process is running. Do this first. Until it is done, pause() never wakes up and every test hangs.TODO (q0) in kernel/sched.c: pick_rr() chooses the next process to run, and should_preempt() decides whether the running process must give up the CPU.TODO (yield) in kernel/sched.c: when sched_timer() gives up the CPU, put the process back at the end of the ready queue and switch to the scheduler.TODO (trap) and TODO (yield) are the same in every task. You can copy them to the later tasks.
| 165 | void |
| 166 | clockintr() |
| 167 | { |
| 168 | if (cpuid() == 0) { |
| 169 | acquire(&tickslock); |
| 170 | ticks++; |
| 171 | wakeup(&ticks); |
| 172 | release(&tickslock); |
| 173 | // TODO (trap): tell the scheduler that a tick has passed. |
| 174 | } |
| 81 | if (killed(p)) |
| 82 | kexit(-1); |
| 83 | |
| 84 | // TODO (trap): on a timer interrupt (which_dev == 2), let the |
| 85 | // scheduler decide whether to give up the CPU. |
| 86 | |
| 87 | prepare_return(); |
| 148 | if ((which_dev = devintr()) == 0) { |
| 149 | // interrupt or trap from an unknown source |
| 150 | printk("scause=0x%lx sepc=0x%lx stval=0x%lx\n", scause, r_sepc(), |
| 151 | r_stval()); |
| 152 | panic("kerneltrap"); |
| 153 | } |
| 154 | |
| 155 | // TODO (trap): on a timer interrupt (which_dev == 2), let the |
| 156 | // scheduler decide whether to give up the CPU. There may be no |
| 157 | // current process: the CPU can be in the scheduler loop. |
| 158 | |
| 159 | // the scheduler may have caused some traps to occur, |
| 160 | // so restore trap registers for use by kernelvec.S's sepc instruction. |
| 161 | w_sepc(sepc); |
| 108 | // In ready-queue order; the running process gives up the CPU when its |
| 109 | // time slice is used up (see should_preempt()). |
| 110 | struct proc * |
| 111 | pick_rr(int level) |
| 112 | { |
| 113 | // TODO (q0): the RUNNABLE process in queue level that runs next. |
| 114 | return 0; |
| 115 | } |
| 201 | #define PREEMPT_REQUEUE 2 // back to the end of the ready queue |
| 202 | |
| 203 | // Should the running process p give up the CPU at this timer interrupt? |
| 204 | static int |
| 205 | should_preempt(struct proc *p) |
| 206 | { |
| 207 | // TODO (q0): must p give up the CPU now? Return 0 to keep |
| 208 | // running, or PREEMPT_REQUEUE. |
| 209 | return 0; |
| 210 | } |
| 211 | |
| 212 | // Called from usertrap() and kerneltrap() on a timer interrupt. |
| 213 | void |
| 214 | sched_timer(void) |
| 215 | { |
| 216 | struct proc *p = myproc(); |
| 217 | int r = should_preempt(p); |
| 218 | |
| 219 | if (r == 0) |
| 220 | return; |
| 221 | acquire(&p->lock); |
| 222 | // TODO (yield): p gives up the CPU. Put it back in the ready queue, |
| 223 | // at the end of the queue. |
| 224 | schedlog(p, r == PREEMPT_REQUEUE ? SEV_REQUEUE : SEV_PREEMPT); |
| 225 | // TODO (yield): switch to the scheduler. |
| 226 | release(&p->lock); |
| 227 | } |
kerneltrap(), what does myproc() return when the interrupt came while the CPU was in the scheduler loop? What would sched_timer() do with that value?
The first line of sched_timer() above, and the tab "In the scheduler loop" in Part 2.
slice_used show for the tick that has just ended?
charge() in kernel/sched.c, and "Two hooks" in Part 2.
struct proc. Which ones? Write before(a, b) on paper. Check the case where a and b joined in the same tick: does before(b, a) give the opposite answer?
should_preempt() find out whether another process is RUNNABLE, using only code that already exists?
Read what pick_by() returns and which states it accepts. What is p's state while should_preempt() runs?
should_preempt() read and write, and which lock protects them? May you still hold that lock when you call pick_rr()?
The loop in scheduler() in Part 5.
TODO (yield): what must be true about p before sched() may switch away from it? And what makes p count as joining the end of the ready queue?
| Test | What it checks |
|---|---|
turns | jobs in the ready queue take turns, RR_SLICE ticks each |
alone | a job alone keeps running after its time slice |
late | a job that arrives later joins the end of the queue (Part 8 walks through it) |
wake | a job that wakes up joins the end of the queue |
slice | a job alone starts a new time slice when its slice is used up |
Run the process that is expected to finish its CPU burst soonest. The running process keeps the CPU until it blocks or exits.
burst_est argument of schedstart()). In this task the estimate never changes.RUNNABLE process has its whole estimate left; the formula matters in Task 5.RUNNABLE process with the least estimated time left runs next. If several are equal, the smaller pid goes first, even if another one has been waiting longer.RUNNABLE.TODO (trap) reports, the ticks of the running process in its current burst grow, and a pause() that is due wakes up.TODO (q1): pick_shortest() and should_preempt().TODO (trap) and TODO (yield): copy them from q0. Nothing is preempted in this task, so its tests never run your TODO (yield) code; Task 1 tests it.| 108 | // The process with the least estimated time left in its CPU burst. |
| 109 | struct proc * |
| 110 | pick_shortest(int level) |
| 111 | { |
| 112 | // TODO (q1): the RUNNABLE process in queue level that runs next. |
| 113 | return 0; |
| 114 | } |
| 167 | // Charge the tick that just ended to a RUNNING process. |
| 168 | static void |
| 169 | charge(struct proc *p) |
| 170 | { |
| 171 | p->rticks++; |
| 172 | p->slice_used++; |
| 173 | p->cur_burst++; |
| 174 | logat(p, SEV_TICK, ticks - 1); |
| 175 | } |
The field table in Part 6, and charge() above.
cur_burst and rticks both count ticks. When is each one set back to 0? Which one means "the ticks the process has run in its current burst"?
charge() above, and sched_sleep(), schedstart() and allocproc() in the kernel.
before() not look at? Which test checks this?q1 log?| Test | What it checks |
|---|---|
shortest | the job with the least estimated time left runs first |
no-preempt | a running job is not interrupted by a shorter one |
tie | equal estimates: the smaller pid runs first |
tie-arrival | equal estimates: the smaller pid runs first, not the earlier arrival |
wake | a job back from sleep is compared by its estimate again |
Always run the most important process. A process that waits long enough becomes more important, so every process gets to run eventually.
priority argument of schedstart()). 0 is the highest. The priority itself never changes.max(0, priority - waiting time / PRIO_AGE_STEP), with integer division. PRIO_AGE_STEP is 3 (kernel/param.h). So every 3 ticks of waiting make a process one step more important.RUNNABLE process that comes before all the others runs next.RUNNABLE process comes before the running process p, p gives up the CPU. Otherwise p keeps running. This includes the case where the other process has the same effective priority and a smaller pid.RUNNABLE with a waiting time of 0.TODO (trap) reports, the waiting time of every RUNNABLE process grows by 1, and a pause() that is due wakes up.TODO (q2): pick_priority() and should_preempt().TODO (trap) and TODO (yield): copy them from q0. The order here ignores when a process joined, so this task's tests do not check that TODO (yield) puts the process at the end of the queue; Task 1 tests it.| 108 | // The process with the highest priority after aging. |
| 109 | struct proc * |
| 110 | pick_priority(int level) |
| 111 | { |
| 112 | // TODO (q2): the RUNNABLE process in queue level that runs next. |
| 113 | return 0; |
| 114 | } |
| 202 | // Should the running process p give up the CPU at this timer interrupt? |
| 203 | static int |
| 204 | should_preempt(struct proc *p) |
| 205 | { |
| 206 | // TODO (q2): must p give up the CPU now? Return 0 to keep |
| 207 | // running, or PREEMPT_REQUEUE. |
| 208 | return 0; |
| 209 | } |
should_preempt() decide whether some RUNNABLE process comes before p? How much work does your approach do?
Which RUNNABLE process is the hardest one for p to beat?
should_preempt() reads p's fields and also needs information about other processes. Which locks may it hold at each step?
"Locks and interrupts" in Part 3.
age-tie-preempt and age-pid-not-wait, find their jobs in user/schedcheck.c and work out by hand at which tick each job's effective priority changes.
"Write your own test" in Part 8 explains the format of a test.
| Test | What it checks |
|---|---|
priority | the highest priority (smallest number) runs first |
preempt | a job with a higher priority takes the CPU at once |
aging | waiting raises priority, so a low-priority job gets to run |
tie | equal priority: the smaller pid runs first |
tie-arrival | equal priority: the smaller pid runs first, not the earlier arrival |
age-tie-preempt | aging into an equal priority preempts a larger pid |
age-pid-not-wait | on an aged tie, smaller pid beats longer waiting time |
Run processes in the order they become ready. The running process keeps the CPU until it blocks or exits.
TODO (trap) reports, a pause() that is due wakes up.TODO (q3): pick_fifo() and should_preempt().TODO (trap) and TODO (yield): copy them from q0. Nothing is preempted in this task, so its tests never run your TODO (yield) code.| 108 | // The process that has been in the ready queue the longest. |
| 109 | struct proc * |
| 110 | pick_fifo(int level) |
| 111 | { |
| 112 | // TODO (q3): the RUNNABLE process in queue level that runs next. |
| 113 | return 0; |
| 114 | } |
q0 code answers the same question, and which part does not apply here?"The scheduler hooks in sleep and wakeup" in Part 4.
no-preempt, what would the log show if should_preempt() still gave up the CPU after 2 ticks, as in Task 1?| Test | What it checks |
|---|---|
arrival | jobs run in the order they arrive |
no-preempt | a running job keeps the CPU until it blocks or exits |
wake | a job that wakes up joins the end of the queue |
tie | same arrival time: the smaller pid runs first |
Combine the pickers from Tasks 1–4 into a scheduler with four queues. Processes move down when they use a lot of CPU, and up when they sleep or wait too long.
RUNNABLE process with level == k is in Qk.| Queue | Pick | Ties | Time slice | Allotment |
|---|---|---|---|---|
| Q0 | the earliest to join | smaller pid | 1 tick | 2 ticks |
| Q1 | the least estimated remaining burst | smaller pid | — | 4 ticks |
| Q2 | the highest effective priority (smallest number) | smaller pid | 2 ticks | 8 ticks |
| Q3 | the earliest to join | smaller pid | — | unlimited |
max(0, priority - wait_ticks / MLFQ_AGE_STEP), with integer division. priority is 0–9; 0 is the highest.MLFQ_* constants in kernel/param.h. Do not change them.| 20 | #define MLFQ_NLEVEL 4 |
| 21 | #define MLFQ_SLICE0 1 // L0 time slice |
| 22 | #define MLFQ_SLICE2 2 // L2 re-checks priorities after this many ticks |
| 23 | #define MLFQ_ALLOT0 2 // CPU ticks one burst may use in L0 before demotion |
| 24 | #define MLFQ_ALLOT1 4 |
| 25 | #define MLFQ_ALLOT2 8 |
| 26 | #define MLFQ_AGE_STEP 2 // L2: +1 priority per this many waiting ticks |
| 27 | #define MLFQ_STARVE 20 // L3: promote to L2 after waiting this many ticks |
RUNNABLE process that has waited at least 20 ticks in Q3 moves to the end of Q2. Its waiting time is set to 0, and its used allotment starts over.After the accounting, the running process p is handled by this table. top is the highest queue that holds a RUNNABLE process.
| Case | Result |
|---|---|
| top is higher than p, and p is not just demoted | give up the CPU, keep its place |
| top is higher than p, and p is just demoted | give up the CPU, go to the end |
| top is p's queue, and p is just demoted | give up the CPU, go to the end |
| p is in Q0, Q0 has another process, and the time slice is used up | give up the CPU, go to the end |
| p is in Q1, and Q1 has a process that comes before p | give up the CPU, go to the end |
| p is in Q2, the time slice is used up, and Q2 has a process that comes before p | give up the CPU, go to the end |
| otherwise (Q3 is never preempted from within Q3) | keep running |
pick_by(level, before), and top_level(): the highest queue that holds a RUNNABLE process.RUNNABLE process. A pause() that is due wakes up.TODO (q0)–TODO (q3): copy pick_rr(), pick_shortest(), pick_priority(), pick_fifo() and their helpers from q0–q3. The priority picker ages with MLFQ_AGE_STEP instead of PRIO_AGE_STEP.TODO (trap) in kernel/trap.c: copy it. On every timer interrupt, do the accounting first, then the check.TODO (yield): copy it, but also handle PREEMPT_KEEP: the process keeps its place (see Preemption).TODO (MLFQ): the rules above.mlfqcheck replaces schedcheck in this task.
| 148 | // Choose the next process to run. Returns it locked and RUNNABLE, or 0. |
| 149 | struct proc * |
| 150 | sched_pick(void) |
| 151 | { |
| 152 | struct proc *p; |
| 153 | |
| 154 | for (;;) { |
| 155 | // TODO (MLFQ): look at the queues from Q0 to Q3 and take the first |
| 156 | // process found: pick_rr() for Q0, pick_shortest() for Q1, |
| 157 | // pick_priority() for Q2 and pick_fifo() for Q3. For now, only Q0. |
| 158 | p = pick_rr(0); |
| 159 | if (p == 0) |
| 160 | return 0; |
| 161 | acquire(&p->lock); |
| 162 | if (p->state == RUNNABLE) |
| 163 | return p; |
| 164 | release(&p->lock); |
| 165 | } |
| 166 | } |
| 172 | // p is about to block: its CPU burst is over. |
| 173 | void |
| 174 | sched_sleep(struct proc *p) |
| 175 | { |
| 176 | if (p->starting) |
| 177 | return; |
| 178 | // TODO (MLFQ): update p->burst_est and reset the allotment p has used |
| 179 | // in its queue (see "Blocking" in the spec). |
| 180 | p->cur_burst = 0; |
| 181 | schedlog(p, SEV_SLEEP); |
| 182 | } |
| 184 | // Make a SLEEPING process RUNNABLE. |
| 185 | void |
| 186 | sched_wakeup(struct proc *p) |
| 187 | { |
| 188 | p->state = RUNNABLE; |
| 189 | p->enter_tick = ticks; |
| 190 | p->wait_ticks = 0; |
| 191 | if (p->starting) { |
| 192 | p->starting = 0; |
| 193 | p->logenabled = 1; |
| 194 | schedlog(p, SEV_NEW); |
| 195 | return; |
| 196 | } |
| 197 | // TODO (MLFQ): coming back from sleep moves p up (see "Waking up" in |
| 198 | // the spec). |
| 199 | schedlog(p, SEV_WAKE); |
| 200 | } |
| 206 | // Charge the tick that just ended to a RUNNING process. |
| 207 | static void |
| 208 | charge(struct proc *p) |
| 209 | { |
| 210 | p->rticks++; |
| 211 | p->slice_used++; |
| 212 | p->cur_burst++; |
| 213 | logat(p, SEV_TICK, ticks - 1); |
| 214 | // TODO (MLFQ): count this tick against the allotment of p's queue. |
| 215 | if (0 /* TODO (MLFQ): p has used up its allotment and is not in Q3 */) { |
| 216 | // TODO (MLFQ): move p one queue down, to the end of that queue, and |
| 217 | // remember that p was just demoted. |
| 218 | schedlog(p, SEV_DEMOTE); |
| 219 | } |
| 220 | } |
| 222 | // Called by clockintr() on CPU 0 right after ticks++. |
| 223 | void |
| 224 | sched_tick(void) |
| 225 | { |
| 226 | struct proc *p; |
| 227 | |
| 228 | for (p = proc; p < &proc[NPROC]; p++) { |
| 229 | acquire(&p->lock); |
| 230 | if (p->state == RUNNING) { |
| 231 | charge(p); |
| 232 | } else if (p->state == RUNNABLE) { |
| 233 | p->wait_ticks++; |
| 234 | if (0 /* TODO (MLFQ): p has waited too long in Q3 */) { |
| 235 | // TODO (MLFQ): move p up to the end of Q2. |
| 236 | schedlog(p, SEV_PROMOTE); |
| 237 | } |
| 238 | } |
| 239 | // wake up pause() and schedstart() sleepers whose time has come. |
| 240 | if (p->chan == &p->wake_tick && ticks >= p->wake_tick) { |
| 241 | p->chan = 0; |
| 242 | if (p->state == SLEEPING) |
| 243 | sched_wakeup(p); |
| 244 | } |
| 245 | release(&p->lock); |
| 246 | } |
| 247 | } |
| 249 | #define PREEMPT_KEEP 1 // back to the ready queue, keeping its place |
| 250 | #define PREEMPT_REQUEUE 2 // back to the end of the ready queue |
| 251 | |
| 252 | // Highest (smallest) queue holding a RUNNABLE process, or MLFQ_NLEVEL. |
| 253 | static __attribute__((unused)) int |
| 254 | top_level(void) |
| 255 | { |
| 256 | struct proc *p; |
| 257 | int top = MLFQ_NLEVEL; |
| 258 | |
| 259 | for (p = proc; p < &proc[NPROC]; p++) { |
| 260 | acquire(&p->lock); |
| 261 | if (p->state == RUNNABLE && p->level < top) |
| 262 | top = p->level; |
| 263 | release(&p->lock); |
| 264 | } |
| 265 | return top; |
| 266 | } |
| 267 | |
| 268 | // Should the running process p give up the CPU at this timer interrupt? |
| 269 | // Returns 0 (keep running), PREEMPT_KEEP or PREEMPT_REQUEUE. |
| 270 | static int |
| 271 | should_preempt(struct proc *p) |
| 272 | { |
| 273 | // TODO (MLFQ): follow the "Preemption" table in the spec. Also start a |
| 274 | // new time slice when p's slice is used up but p keeps running, and |
| 275 | // forget that p was just demoted. Hold p->lock while you use p's fields, but not |
| 276 | // while you call top_level() or a picker: they lock every process. |
| 277 | return 0; |
| 278 | } |
| 279 | |
| 280 | // Called from usertrap() and kerneltrap() on a timer interrupt. |
| 281 | void |
| 282 | sched_timer(void) |
| 283 | { |
| 284 | struct proc *p = myproc(); |
| 285 | int r = should_preempt(p); |
| 286 | |
| 287 | if (r == 0) |
| 288 | return; |
| 289 | acquire(&p->lock); |
| 290 | // TODO (yield): p gives up the CPU. Put it back in the ready queue: |
| 291 | // at the end of its queue for PREEMPT_REQUEUE, keeping its place for |
| 292 | // PREEMPT_KEEP. |
| 293 | schedlog(p, r == PREEMPT_REQUEUE ? SEV_REQUEUE : SEV_PREEMPT); |
| 294 | // TODO (yield): switch to the scheduler. |
| 295 | release(&p->lock); |
| 296 | } |
sched_pick() now looks only at Q0. How should it choose among the four queues? Why does it lock the result and check its state again?
The TODO comment in sched_pick(), and the note under the hook table in Part 6.
The comments in charge() and in should_preempt() above, and "The ready queue" in Part 1.
Task 2, question 2.
PREEMPT_KEEP and PREEMPT_REQUEUE both put p back in its queue. What makes the difference between keeping its place and going to the end?
Where does the order of Q0 and Q3 come from? See "The ready queue" in Part 1.
The functions under "Already provided" above, and the pickers you copied.
should_preempt() must hold p's lock while it uses p's fields, but not while it calls top_level() or a picker. Plan the order of your reads and calls. Can another process run between them?
schedtest 7 and schedtest 42. Pick a few columns of the Gantt chart and check them against the rules by hand. The hidden seeds use larger workloads like these.
The section schedtest output of q4-spec.md explains the chart.
The tests are grouped by the rule they mainly check; most also depend on how each queue picks. If several tests fail, start with the rule they have in common.
| Rule | Tests |
|---|---|
| Q0: pick and time slice | rr |
| Q1: pick, ties and preemption | shortest, shortest-tie, shortest-preempt |
| Estimated remaining burst; preemption does not end a burst | progress, remaining-history, preempt-estimate |
| New estimate after blocking | estimate, estimate-history |
| Q2: priority, aging, ties and time slice | priority, priority-tie, priority-pid-not-wait, priority-age-tie, slice |
| Q3: order; keep its place when preempted | fifo |
| A higher queue preempts | preempt |
| Allotment and demotion | demote |
| Waiting too long in Q3 | starve |
| Waking up | wake, wake-tail |
schedcheck (mlfqcheck in q4) runs small fixed tests. Each test starts a few jobs, named A, B, C, … in the order of the test, and prints one line per scheduler event:
ev <tick> <job> <event> L<level>
<tick> counts from the start of the test. <level> is the queue of the job after the event; for a tick event, it is the queue the job ran in. The jobs are created in the order A, B, C, so A has the smallest pid. ./grade compares these lines with the expected ones.
| Event | Recorded by | Meaning |
|---|---|---|
new | sched_wakeup(), or schedstart() if the start tick has already passed | The job leaves schedstart() and joins the ready queue for the first time. |
run | scheduler() | The job is picked and starts to run. |
tick | sched_tick() | The job was RUNNING when the tick ended. The whole tick is charged to it, even if it started partway through the tick. |
requeue | sched_timer() | The job gives up the CPU and goes to the end of its queue. |
preempt | sched_timer() | The job gives up the CPU and keeps its place in its queue (q4). |
sleep | sched_sleep() | The job blocks. |
wake | sched_wakeup() | The job wakes up and joins the ready queue again. |
demote, promote | sched_tick() | The job moves down or up one queue (q4). |
exit | kexit() | The job exits. |
This is the expected log of the q0 test late, one timer interrupt at a time. The rule is round robin with a time slice of 2 ticks (Task 1). Jobs A and B arrive at tick 0 and need 6 ticks of CPU each. Job C arrives at tick 3 and needs 2 ticks.
late (q0)tick line)running now, or ran without being charged a tickRUNNABLESLEEPING✕ exitTwo details of the log are easy to misread:
tick event carries the tick that has just ended. sched_tick() records it at the next interrupt, as ticks - 1. Other events carry the tick in which they happen. So the lines from one interrupt can carry two different numbers, and a later line can show a smaller tick. For example, a q2 test prints ev 5 A new before ev 4 C tick. Both come from the interrupt that starts tick 5.sched_tick() records at one interrupt follow the order of proc[]. sched_tick() visits the entries of proc[] from first to last. A requeue or preempt line, and the run line that follows it, always come after them. At tick 3 in the stepper, B's entry comes before C's, so B's tick line comes before C's new line.A job whose last tick of work ends at an interrupt still needs a little CPU time to return to user space and call exit(). This takes much less than one tick, so no tick event comes before its exit event. If the job gave up the CPU at that interrupt, it must be picked again first. Its run and exit events then carry the same tick, as for C at tick 12.
schedtest draws a larger workload as a Gantt chart with one row per queue. The section schedtest output of q4-spec.md explains its format.
When a test fails, ./grade prints the first line where your log differs from the expected log, with the three expected lines before it. For example, with a bug in TODO (yield):
late: FAIL (0/20) -- a job that arrives later joins the end of the queue
first difference at event 6:
ev 0 A tick L0
ev 1 A tick L0
ev 2 A requeue L0
expected: ev 2 B run L0
got: ev 2 A run L0
Events are counted from 0, so event 6 is the seventh ev line of the test. The whole output of the last run is in the file grade.out. The expected lines are in the EXPECTED block near the top of grade.
The viewer draws a log as a chart. Paste your output into the first box and, to compare, the expected lines from grade into the second; the quotes and commas are fine. It starts with the output of the buggy kernel above: A gets the CPU back right after each requeue, and B waits until A has exited.
The public tests are small. The hidden seeds use larger workloads with more ties and more blocking. To check a case that the public tests do not cover, add your own test to user/schedcheck.c (user/mlfqcheck.c in Task 5). Each test is one entry in the table tests[]:
| 17 | #define B(n) { OP_BURN, n } |
| 18 | #define P(n) { OP_PAUSE, n } |
| 19 | |
| 20 | // job: profile, arrive, level, priority, burst_est, nops, ops |
| 21 | static struct test tests[] = { |
| 22 | { "turns", "jobs in the ready queue take turns, RR_SLICE ticks each", |
| 23 | 3, { { "a", 0, 0, 5, 2, 1, { B(4) } }, |
| 24 | { "b", 0, 0, 5, 2, 1, { B(4) } }, |
| 25 | { "c", 0, 0, 5, 2, 1, { B(4) } } } }, |
| 26 | { "alone", "a job alone keeps running after its time slice", |
| 27 | 1, { { "a", 0, 0, 5, 2, 1, { B(5) } } } }, |
| 28 | { "late", "a job that arrives later joins the end of the queue", |
| 29 | 3, { { "a", 0, 0, 5, 2, 1, { B(6) } }, |
| 30 | { "b", 0, 0, 5, 2, 1, { B(6) } }, |
| 31 | { "c", 3, 0, 5, 2, 1, { B(2) } } } }, |
An entry has a name, a description, the number of jobs (at most 4), and the jobs. Each job is { profile, arrive, level, priority, burst_est, nops, ops }:
profile is a name for the job. The log calls the jobs A, B, C, D in the order of the entry.arrive is the tick, counted from the start of the test, at which the job leaves schedstart() and joins the ready queue.level, priority and burst_est are the starting values of these fields. level must be 0 in Tasks 1–4.ops lists what the job does, and nops is the length of the list. B(n) runs on the CPU for n ticks; P(n) calls pause(n).Run it in xv6 with schedcheck and its name, and check the events against the rules by hand. Files in user/ are overwritten when grading, so your tests do not affect your grade.
| What you see | Likely cause |
|---|---|
./grade: "xv6 did not reach the shell" | sched_pick() never returns a process. The first process, init, cannot run. |
A test hangs, or pause() never returns | sched_tick() is not called, so no pause() wakes up. |
Each job runs until it blocks or exits; no requeue events | sched_timer() is not called, or should_preempt() always returns 0. |
panic: acquire | A lock is acquired while the CPU already holds it, often a p->lock held around a picker. |
panic: sched ... | See the table in Part 5. |
| A wrong event order | Compare the logs in the log viewer, then find the rule in the task box that decides the first differing line. |
In the kernel, the print function is printk(). It takes a format string like printf(), and its output goes to the console. Remove your printk() calls before you run ./grade: kernel output mixes with the event lines that ./grade reads. Press Ctrl-p in the xv6 console to print the list of processes with their pids and states.
gdb lets you stop the kernel at any function, look at the call chain and the fields of every process, and stop when a field changes. In the container, start gdb with gdb-multiarch, not gdb. The command gdb there debugs only programs for the CPU of your own computer (x86 or ARM) and cannot read the RISC-V kernel, although make qemu-gdb tells you to run gdb.
You need two terminals in the same container, one for xv6 and one for gdb. How to open the second one depends on how you started the container:
| You started the container with | Open a second terminal with |
|---|---|
| VS Code, "Reopen in Container" | Terminal → Split Terminal. Both terminals are in the container. |
docker run ... --name xv6 ... (from the spec) | A new terminal on your computer, then docker exec -it -w /xv6 xv6 bash |
./lab shell | tmux inside the container (below) |
tmux splits one terminal into panes. Run tmux, then press Ctrl-b and then % to split the window into a left and a right pane. Press Ctrl-b and then an arrow key to move between panes. Press Ctrl-b and then [ to scroll back with the arrow keys, and q to stop scrolling. Type exit in a pane to close it.
make qemu-gdb. Right pane: gdb, connected to QEMU. Type xv6 commands on the left and gdb commands on the right.make qemu-gdb. QEMU starts xv6 and stops it before the first instruction. It waits for gdb.gdb-multiarch. gdb reads the file .gdbinit that make qemu-gdb has written, loads the kernel symbols and connects to QEMU. It stops at address 0x1000, the first instruction.continue in gdb. xv6 boots in the first pane.schedcheck late. Press Ctrl-c in gdb to stop xv6 at any time, and type continue to go on.A session on q0 with a working solution (your line numbers will differ):
(gdb) break sched_timer
Breakpoint 1 at 0x80002760: file kernel/sched.c, line 232.
(gdb) continue
Continuing.
Breakpoint 1, sched_timer () at kernel/sched.c:232
232 {
(gdb) bt # the call chain: the newest call first
#0 sched_timer () at kernel/sched.c:232
#1 0x0000000080002c2e in kerneltrap () at kernel/trap.c:156
#2 0x0000000080005b58 in kernelvec () at kernel/kernelvec.S:38
Backtrace stopped: frame did not save the PC
(gdb) p p
$1 = <optimized out>
(gdb) p cpus[0].proc->name # the running process
$2 = "schedcheck\000\000\000\000\000"
(gdb) p cpus[0].proc->slice_used
$3 = 1
(gdb) p ticks
$4 = 65
(gdb) delete # remove all breakpoints
(gdb) continue
The interrupt came from kerneltrap(): the job was inside the system call cputicks(). The backtrace ends at kernelvec, where the trap entered the kernel; "Backtrace stopped" is normal there. The kernel is compiled with optimization, so gdb cannot show some local variables, such as p here. With one CPU, cpus[0].proc is always the running process (0 when no process runs).
Save the lines below as procs.gdb in the repository folder. In gdb, type source procs.gdb once; after that, the command procs prints every process with its state and scheduling fields. Add other fields, such as priority or allot_used, in the same way.
# procs: print every process in use, with the scheduling fields.
define procs
set $i = 0
while $i < 64
if proc[$i].state != UNUSED
printf "%3d %-10s ", proc[$i].pid, proc[$i].name
output proc[$i].state
printf " level=%d enter_tick=%d slice_used=%d wait_ticks=%d\n", proc[$i].level, proc[$i].enter_tick, proc[$i].slice_used, proc[$i].wait_ticks
end
set $i = $i + 1
end
end
All the jobs of a test are named schedcheck (mlfqcheck in q4). Their pids are in the order A, B, C, …, so in the sessions below (the first test after boot) pid 4 is A, pid 5 is B and pid 6 is C. Use procs to find the pids in your own run.
A test hangs. Press Ctrl-c in gdb, then look at where the CPU is and at the processes. Here the kernel does not call sched_tick():
^C
Program received signal SIGINT, Interrupt.
scheduler () at kernel/proc.c:457
457 intr_on();
(gdb) p ticks
$1 = 109
(gdb) procs
1 init SLEEPING level=0 enter_tick=1 slice_used=0 wait_ticks=0
2 sh SLEEPING level=0 enter_tick=59 slice_used=0 wait_ticks=0
3 schedcheck SLEEPING level=0 enter_tick=59 slice_used=0 wait_ticks=0
4 schedcheck SLEEPING level=0 enter_tick=59 slice_used=0 wait_ticks=0
5 schedcheck SLEEPING level=0 enter_tick=59 slice_used=0 wait_ticks=0
6 schedcheck SLEEPING level=0 enter_tick=59 slice_used=0 wait_ticks=0
(gdb) continue
^C
(gdb) p ticks
$2 = 139
ticks grows, but every process is SLEEPING and the CPU waits in scheduler(). The jobs never leave schedstart(): nothing wakes them up at their start tick.
The kernel panics. Set a breakpoint on panic before you type continue. When the panic happens, bt shows the code that caused it. Here should_preempt() still holds p->lock when it calls pick_rr(). pick_by() then tries to acquire the lock of every process, including p:
(gdb) break panic
(gdb) continue
Breakpoint 1, panic (s=s@entry=0x80008048 "acquire") at kernel/printk.c:139
(gdb) bt
#0 panic (s=s@entry=0x80008048 "acquire") at kernel/printk.c:139
#1 0x0000000080000c28 in acquire (lk=lk@entry=0x80014020 <proc+1248>) at kernel/spinlock.c:26
#2 0x00000000800025d8 in pick_by (level=0, before=<optimized out>) at kernel/sched.c:99
#3 pick_rr (level=level@entry=0) at kernel/sched.c:121
#4 0x0000000080002782 in should_preempt (p=0x80014020 <proc+1248>) at kernel/sched.c:222
#5 sched_timer () at kernel/sched.c:236
#6 0x0000000080002b98 in usertrap () at kernel/trap.c:85
#7 0x0000003ffffff09c in ?? ()
Which code changes a field? watch -l stops whenever a memory location changes, and shows the old and the new value. Here it watches the time slice of the process that is running when you set the watchpoint. -l fixes the address, so it keeps watching that process after a switch:
(gdb) watch -l cpus[0].proc->slice_used
Hardware watchpoint 2: -location cpus[0].proc->slice_used
(gdb) continue
Hardware watchpoint 2: -location cpus[0].proc->slice_used
Old value = 1
New value = 2
charge (p=0x80014030 <proc+1248>) at kernel/sched.c:181
(gdb) bt
#0 charge (p=0x80014030 <proc+1248>) at kernel/sched.c:181
#1 sched_tick () at kernel/sched.c:194
#2 0x0000000080002a1e in clockintr () at kernel/trap.c:172
#3 0x0000000080002a92 in devintr () at kernel/trap.c:214
#4 0x0000000080002bcc in kerneltrap () at kernel/trap.c:148
#5 0x0000000080005b58 in kernelvec () at kernel/kernelvec.S:38
Which process does the picker return? Stop in sched_pick() and type finish. gdb runs to the end of the function and prints the return value. $ is the last value that gdb printed, and $$ the one before it.
(gdb) break sched_pick
(gdb) continue
Breakpoint 2, sched_pick () at kernel/sched.c:127
(gdb) finish
Run till exit from #0 sched_pick () at kernel/sched.c:127
0x0000000080001e24 in scheduler () at kernel/proc.c:461
461 if ((p = sched_pick()) != 0) {
Value returned is $3 = (struct proc *) 0x80014030 <proc+1248>
(gdb) p $->pid
$4 = 4
(gdb) p *$$ # every field of that process
$5 = {lock = {locked = 1, ...}, state = RUNNABLE, ..., pid = 4, level = 0, priority = 5,
burst_est = 2, cur_burst = 2, slice_used = 2, allot_used = 0, wait_ticks = 2,
enter_tick = 66, ...}
Stop for one process only. Add a condition to a breakpoint. Here gdb stops only when job C (pid 6) wakes up:
(gdb) break sched_wakeup if p->pid == 6
(gdb) continue
Breakpoint 1, sched_wakeup (p=p@entry=0x80014370 <proc+2080>) at kernel/sched.c:158
(gdb) bt
#0 sched_wakeup (p=p@entry=0x80014370 <proc+2080>) at kernel/sched.c:158
#1 0x0000000080002746 in sched_tick () at kernel/sched.c:202
#2 0x0000000080002a1e in clockintr () at kernel/trap.c:172
...
| Command | What it does |
|---|---|
continue (c) | Run until the next breakpoint. Ctrl-c stops xv6 at once. |
break f (b f) | Stop when function f starts. break f if cond stops only when cond is true. tbreak f stops once. |
info breakpoints, delete [n] | List the breakpoints; remove one, or all. |
bt | Show the call chain. frame n moves to one frame of it; up and down move by one. |
next (n), step (s), finish | Run one line; run one line and enter calls; run to the end of the function and print its return value. |
p expr | Print a value: p ticks, p proc[3], p *cpus[0].proc, p/x var (in hex). |
display expr | Print expr at every stop. |
watch -l expr | Stop when the memory of expr changes. |
list | Show the source code around the current line. |
sched_tick(), stops at every tick. Add a condition, or remove the breakpoint when you are done with it.quit in gdb. xv6 then keeps running; press Ctrl-a and then x in the first pane to stop QEMU../grade without gdb. It starts its own QEMU.This lab is worth 7 points of the course grade: 3 for the implementation and 4 for the lab quiz.
The five tasks add up to 100 points, scaled to 3. In each task, the public tests give 40% of the points and 10 hidden seeds give 60%.
| Task | Branch | Points | Public (40%) | Private (60%) |
|---|---|---|---|---|
| Task 1 | q0 | 10 | ./grade (5 tests) | 10 hidden seeds |
| Task 2 | q1 | 10 | ./grade (5 tests) | 10 hidden seeds |
| Task 3 | q2 | 10 | ./grade (7 tests) | 10 hidden seeds |
| Task 4 | q3 | 10 | ./grade (4 tests) | 10 hidden seeds |
| Task 5 | q4 | 60 | ./grade (20 tests) | 10 hidden seeds |
./grade and of the files you must not change (Part 0)../grade does not guarantee the hidden seeds. They may include larger workloads, ties and blocking that the public tests do not cover. Write your own tests, try schedtest with other seeds (for example 7 and 42), and check the Gantt charts against the rules by hand.The lab quiz covers the Trace Code section of q4-spec.md, and may also cover your implementation. Be ready to explain each item from the code.
stvec is switched between the two handlers:
prepare_return → uservec → usertrap → devintr → clockintr: the interrupt comes in user mode;usertrap → kernelvec → kerneltrap → devintr → clockintr: the interrupt comes in a system call or in the scheduler loop.timebase-frequency of QEMU virt is 10000000. How long is one tick? When does the hardware raise a timer interrupt? Which line in clockintr() stops it from being pending? What happens when you comment out the line that writes stimecmp? Read clockintr() (its start is in the Task 1 code) and try the experiment.userinit / kfork → allocprocusertrap / kerneltrap → sched_timer → sched → swtch (see also the two hooks)sys_pause → sleep_prepare → sleep → sched (see also sleep and wakeup)clockintr → sched_tick → sched_wakeup (see also pause())sys_exit → kexit → sched (see also exit)scheduler → sched_pick → swtch (see also the scheduler loop)schedcheck (or mlfqcheck), write down the events you expect tick by tick, and compare them with the real output in the log viewer of Part 8.sched(). Start from the hardware that raises the interrupt and go step by step until the CPU switches to the scheduler thread. At each step, know which function runs, what it does, and which locks are held.q4-spec.md, open the files and follow the calls. This page explains the paths, but the quiz asks about the code.schedtest builds the same workload from the same seed on every branch, so run schedtest 42 on q0 to q4 and compare the Gantt charts and the turnaround tables. Which jobs wait longest under each policy? How soon does a short job that arrives late finish? Which policy switches between jobs most often?for b in q0 q1 q2 q3 q4; do git log -1 --oneline $b; donemake clean in the container to keep the zip small.os-xv6-lab-student folder, including the hidden .git folder (it holds all your branches), as <student_id>.zip. Run this in the folder that contains os-xv6-lab-student:
zip -r <student_id>.zip os-xv6-lab-student # macOS / Linux
tar -a -c -f <student_id>.zip os-xv6-lab-student # Windows (PowerShell)<student_id>.zip to the eLearn assignment by 2026/11/10 23:59 (UTC+8).Do not push your work to a public repository, for example a fork of the lab repository: everyone could see your code.