xv6, line by line
test yourself

Test yourself · category 12 of 20

Memory ordering and atomics

The one atomic swap and the handful of fences xv6 relies on, what RVWMO lets other harts see, why the started flag needs release and acquire, what volatile does and does not do, and how locks carry data (and saved registers) from hart to hart.

1warm-upChoose one

Why can’t a spinlock be written as plain C, like this?

while (lk->locked)
  ;
lk->locked = 1;
2warm-upChoose one

Line 37 compiles to amoswap.w.aq a5,a5,(s1) with a5 = 1 and s1 = &lk->locked. What does this one instruction do?

kernel/spinlock.c
28 // On RISC-V, __atomic_exchange_n turns into an atomic swap:
29 // a5 = 1
30 // s1 = &lk->locked
31 // amoswap.w.aq a5, a5, (s1)
32 //
33 // Passing __ATOMIC_ACQUIRE to __atomic_exchange_n tells
34 // the C compiler and the processor to not move loads or stores
35 // past this point, to ensure that the critical section's memory
36 // references happen strictly after the lock is acquired.
37 while (__atomic_exchange_n(&lk->locked, 1, __ATOMIC_ACQUIRE) != 0)
38 ;
3warm-upTrue or false, and why

True or false: started is declared volatile on line 7, so the boot handoff would still be correct on RISC-V if lines 33 and 35 used plain started = 1; and while (started == 0) ;.

kernel/main.c
7volatile static int started = 0;
9// start() jumps here in supervisor mode on all CPUs.
10void
13 if (cpuid() == 0) {
16 printk("\n");
17 printk("xv6 kernel is booting\n");
18 printk("\n");
19 kinit(); // physical page allocator
20 kvminit(); // create kernel page table
21 kvminithart(); // turn on paging
22 procinit(); // process table
23 trapinit(); // trap vectors
24 trapinithart(); // install kernel trap vector
25 plicinit(); // set up interrupt controller
26 plicinithart(); // ask PLIC for device interrupts
27 binit(); // buffer cache
28 iinit(); // inode table
29 fileinit(); // file table
30 virtio_disk_init(); // emulated hard disk
31 userinit(); // first user process
33 __atomic_store_n(&started, 1, __ATOMIC_RELEASE);
34 } else {
35 while (__atomic_load_n(&started, __ATOMIC_ACQUIRE) == 0)
36 ;

Why?

4warm-upChoose one

Hart 0 executes two stores to different addresses, first to x and then to y, with no fence between them. Under RISC-V’s memory model (RVWMO), which statement is right?

5warm-upChoose all that apply

Why does release free the lock with __atomic_store_n(&lk->locked, 0, __ATOMIC_RELEASE) rather than lk->locked = 0;? Choose all that apply.

kernel/spinlock.c
44// Release the lock.
45void
48 if (!holding(lk))
49 panic("release");
51 lk->cpu = 0;
53 // Release the lock, equivalent to lk->locked = 0.
54 //
55 // This code doesn't use a C assignment, since the C standard
56 // implies that an assignment might be implemented with
57 // multiple store instructions.
58 //
59 // On RISC-V, __atomic_store_n turns into a single atomic store:
60 // s1 = &lk->locked
61 // fence rw,w
62 // sw zero,0(s1)
63 //
64 // The __ATOMIC_RELEASE argument to __atomic_store_n tells the
65 // the C compiler and the CPU to not move loads or stores past
66 // this point, to ensure that all the stores in the critical
67 // section are visible to other CPUs before the lock is released,
68 // and that loads in the critical section occur strictly before
69 // the lock is released.
70 //
71 // On RISC-V, this generates a fence instruction before the store:
72 // fence rw,w
73 __atomic_store_n(&lk->locked, 0, __ATOMIC_RELEASE);
6warm-upClick the line

Click the line of release that compiles to the fence rw,w at 0x80000c86.

kernel/spinlock.c
44// Release the lock.
45void
48 if (!holding(lk))
49 panic("release");
51 lk->cpu = 0;
53 // Release the lock, equivalent to lk->locked = 0.
54 //
55 // This code doesn't use a C assignment, since the C standard
56 // implies that an assignment might be implemented with
57 // multiple store instructions.
58 //
59 // On RISC-V, __atomic_store_n turns into a single atomic store:
60 // s1 = &lk->locked
61 // fence rw,w
62 // sw zero,0(s1)
63 //
64 // The __ATOMIC_RELEASE argument to __atomic_store_n tells the
65 // the C compiler and the CPU to not move loads or stores past
66 // this point, to ensure that all the stores in the critical
67 // section are visible to other CPUs before the lock is released,
68 // and that loads in the critical section occur strictly before
69 // the lock is released.
70 //
71 // On RISC-V, this generates a fence instruction before the store:
72 // fence rw,w
73 __atomic_store_n(&lk->locked, 0, __ATOMIC_RELEASE);

Your pick: none yet (click a line in the code)

7warm-upChoose one

kalloc updates kmem.freelist with ordinary loads and stores and no fence of its own, yet three harts call it. Why do the harts see each other’s updates correctly?

kernel/kalloc.c
68void *
69kalloc(void)
71 struct run *r;
75 if (r)
79 if (r)
80 memset((char *)r, 5, PGSIZE); // fill with junk
81 return (void *)r;
8warm-upType a number

Hart 2 waits in acquire for a lock held by hart 1. Its swap on line 37 runs 1,000 times: the first 999 return 1, the last returns 0. How many times does hart 2 write to lk->locked in total?

kernel/spinlock.c
37 while (__atomic_exchange_n(&lk->locked, 1, __ATOMIC_ACQUIRE) != 0)
38 ;
decimal, 0x hex or 0b binary
9solidDecode the bits

kernel.asm shows the spin instruction in acquire as 80000c04: 0cf4a7af amoswap.w.aq a5,a5,(s1). Decode the 32-bit word. (AMO format: funct5 in bits 31–27, aq bit 26, rl bit 25, rs2 24–20, rs1 19–15, funct3 14–12, rd 11–7, opcode 6–0.)

Value: 0x0cf4a7af

10solidDecode the bits

kernel.asm contains 0310000f fence rw,w at 0x80000c86 and again at 0x80000f0a. Decode the word. (FENCE format: fm in bits 31–28; the predecessor set PI PO PR PW in bits 27–24; the successor set SI SO SR SW in bits 23–20; opcode in bits 6–0.)

Value: 0x0310000f

11solidClick the line

Click the line whose compiled code includes fence r,rw.

kernel/main.c
31 userinit(); // first user process
33 __atomic_store_n(&started, 1, __ATOMIC_RELEASE);
34 } else {
35 while (__atomic_load_n(&started, __ATOMIC_ACQUIRE) == 0)
36 ;
38 printk("hart %d starting\n", cpuid());
39 kvminithart(); // turn on paging
40 trapinithart(); // install kernel trap vector
41 plicinithart(); // ask PLIC for device interrupts
42 }

Your pick: none yet (click a line in the code)

12solidMatch the pairs

Match each ordering instruction in this build’s kernel.asm with where it comes from.

13solidType a number

A hart calls acquire(&kmem.lock) and, a few lines later, release(&kmem.lock). The lock was free, so the swap succeeds at once. How many instructions whose mnemonic is fence (not sfence.vma, not fence.i) does the hart execute inside these two calls?

decimal, 0x hex or 0b binary
14solidType a number

How many atomic memory operation instructions (AMOs such as amoswap, amoadd, or LR/SC pairs counted as one) does this build’s kernel/kernel.asm contain?

decimal, 0x hex or 0b binary
15solidChoose one

Compile static int started; ... while (started == 0) ; (a plain int, no volatile, no atomics) with this toolchain and xv6’s flags (-O). What does GCC emit for the loop?

16solidChoose all that apply

volatile is used on its own for the UART’s registers (Reg() in kernel/uart.c:17) and for the panicked flag. Which of these does volatile guarantee? Choose all that apply.

17solidFill in the machine state

Hart 1 is spinning on line 35 of main, waiting for started, while hart 0 is still building the kernel. What is hart 1’s state?

kernel/main.c
31 userinit(); // first user process
33 __atomic_store_n(&started, 1, __ATOMIC_RELEASE);
34 } else {
35 while (__atomic_load_n(&started, __ATOMIC_ACQUIRE) == 0)
36 ;
38 printk("hart %d starting\n", cpuid());
39 kvminithart(); // turn on paging
40 trapinithart(); // install kernel trap vector
41 plicinithart(); // ask PLIC for device interrupts
42 }
18solidChoose one

release clears lk->cpu on line 51 before the store on line 73 frees the lock. What would go wrong if the order were reversed (free the lock first, then lk->cpu = 0)?

kernel/spinlock.c
44// Release the lock.
45void
48 if (!holding(lk))
49 panic("release");
51 lk->cpu = 0;
53 // Release the lock, equivalent to lk->locked = 0.
54 //
55 // This code doesn't use a C assignment, since the C standard
56 // implies that an assignment might be implemented with
57 // multiple store instructions.
58 //
59 // On RISC-V, __atomic_store_n turns into a single atomic store:
60 // s1 = &lk->locked
61 // fence rw,w
62 // sw zero,0(s1)
63 //
64 // The __ATOMIC_RELEASE argument to __atomic_store_n tells the
65 // the C compiler and the CPU to not move loads or stores past
66 // this point, to ensure that all the stores in the critical
67 // section are visible to other CPUs before the lock is released,
68 // and that loads in the critical section occur strictly before
69 // the lock is released.
70 //
71 // On RISC-V, this generates a fence instruction before the store:
72 // fence rw,w
73 __atomic_store_n(&lk->locked, 0, __ATOMIC_RELEASE);
19solidTrue or false, and why

True or false: panicked (kernel/printk.c:19) is a plain volatile int with no atomics or fences, and that is enough for what it is used for.

kernel/printk.c
137void
138panic(char *s)
141 printk("panic: ");
142 printk("%s\n", s);
143 panicked = 1; // freeze uart output from other CPUs
144 for (;;)
145 ;

Why?

20deepChoose one

Hart 0’s store to started is a release (fence rw,w; sw). Why must harts 1 and 2 also use an acquire load (lw; fence r,rw) instead of a plain lw in the loop?

kernel/main.c
31 userinit(); // first user process
33 __atomic_store_n(&started, 1, __ATOMIC_RELEASE);
34 } else {
35 while (__atomic_load_n(&started, __ATOMIC_ACQUIRE) == 0)
36 ;
38 printk("hart %d starting\n", cpuid());
39 kvminithart(); // turn on paging
40 trapinithart(); // install kernel trap vector
41 plicinithart(); // ask PLIC for device interrupts
42 }
21deepPut in order

Put these events in the order that makes hart 1’s first use of the kernel page table safe at boot.

  1. Hart 1: fence r,rw (0x80000e76)
  2. Hart 1: sfence.vma at the start of kvminithart
  3. Hart 0: sw sets started to 1
  4. Hart 1: csrw satp turns paging on
  5. Hart 1: lw reads started == 1
  6. Hart 0: fence rw,w (0x80000f0a)
  7. Hart 0: kvminit's ordinary stores build the kernel page table
22deepChoose one

A timer interrupt makes sh yield on hart 1: swtch saves its 14 registers with ordinary sd instructions into p->context, and hart 1’s scheduler releases sh’s p->lock. Later hart 0’s scheduler acquires that lock, sees RUNNABLE, and swtch loads p->context. What guarantees hart 0 loads the values hart 1 saved, not older ones?

kernel/swtch.S
10 sd ra, 0(a0)
11 sd sp, 8(a0)
12 sd s0, 16(a0)
13 sd s1, 24(a0)
14 sd s2, 32(a0)
15 sd s3, 40(a0)
16 sd s4, 48(a0)
17 sd s5, 56(a0)
18 sd s6, 64(a0)
19 sd s7, 72(a0)
20 sd s8, 80(a0)
21 sd s9, 88(a0)
22 sd s10, 96(a0)
23 sd s11, 104(a0)
25 ld ra, 0(a1)
26 ld sp, 8(a1)
23deepChoose one

virtio_disk_rw holds disk.vdisk_lock, a spinlock with acquire and release ordering. Why does it still need io_fence() between writing avail->idx and writing the QUEUE_NOTIFY register?

kernel/virtio_disk.c
274 // tell the device the first index in our chain of descriptors.
279 // tell the device another avail ring entry is available.
280 disk.avail->idx += 1; // not % NUM ...
284 *R(VIRTIO_MMIO_QUEUE_NOTIFY) = 0; // value is queue number
24deepChoose one

An experiment built a copy of the kernel with __ATOMIC_RELAXED on release's line 73, so the compiled release has no fence at all. Run under QEMU on an x86-64 host, it passed usertests -q three times and two stress runs. What does that show?

kernel/spinlock.c
71 // On RISC-V, this generates a fence instruction before the store:
72 // fence rw,w
73 __atomic_store_n(&lk->locked, 0, __ATOMIC_RELEASE);
25deepChoose all that apply

Consider the fence rw,w that release executes before sw zero,0(s1). Which of these orderings does it guarantee, as other harts observe them? Choose all that apply.

kernel/spinlock.c
44// Release the lock.
45void
48 if (!holding(lk))
49 panic("release");
51 lk->cpu = 0;
53 // Release the lock, equivalent to lk->locked = 0.
54 //
55 // This code doesn't use a C assignment, since the C standard
56 // implies that an assignment might be implemented with
57 // multiple store instructions.
58 //
59 // On RISC-V, __atomic_store_n turns into a single atomic store:
60 // s1 = &lk->locked
61 // fence rw,w
62 // sw zero,0(s1)
63 //
64 // The __ATOMIC_RELEASE argument to __atomic_store_n tells the
65 // the C compiler and the CPU to not move loads or stores past
66 // this point, to ensure that all the stores in the critical
67 // section are visible to other CPUs before the lock is released,
68 // and that loads in the critical section occur strictly before
69 // the lock is released.
70 //
71 // On RISC-V, this generates a fence instruction before the store:
72 // fence rw,w
73 __atomic_store_n(&lk->locked, 0, __ATOMIC_RELEASE);