xv6, line by line
test yourself

Test yourself · category 11 of 20

Lock order and deadlock

How two locks taken in opposite orders freeze three harts, the one written rule (wait_lock before any p->lock), and the releases, refusals and reference counts that keep every other path in xv6 in a consistent order.

1warm-upChoose one

Hart 1 holds spinlock A and calls acquire(&B). At the same moment hart 2 holds B and calls acquire(&A). Nothing else holds A or B. What happens in this kernel?

kernel/spinlock.c
21void
24 push_off(); // disable interrupts to avoid deadlock.
25 if (holding(lk))
26 panic("acquire");
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 ;
40 // Record info about lock acquisition for holding() and debugging.
41 lk->cpu = mycpu();
2warm-upChoose one

Two processes deadlock on two sleep-locks (each holds one inode lock and waits for the other’s). Compared with a deadlock on two spinlocks, what does the rest of the machine see?

3warm-upChoose all that apply

Which of these mistakes does xv6 turn into a panic at run time?

4warm-upChoose one

kexit and kwait each need wait_lock and some p->lock at the same time. Read the comment. Which nesting does this kernel use?

kernel/proc.c
23// helps ensure that wakeups of wait()ing
24// parents are not lost. helps obey the
25// memory model when using p->parent.
26// must be acquired before any p->lock.
5warm-upChoose all that apply

A leaf lock is one that no code acquires anything else while holding (so it can never be part of a cycle). Which of these are leaves in this kernel?

6warm-upTrue or false, and why

True or false: kexit acquires wait_lock (line 347) and then its own p->lock (line 355), but releases wait_lock first (line 360). Releasing in that order breaks the lock-ordering discipline and could deadlock.

kernel/proc.c
349 // Give any children to init.
352 // Parent might be sleeping in wait().
362 // Jump into the scheduler, never to return.

Why?

7warm-upChoose one

bget finds the cached block on line 66. Why does it release bcache.lock (line 68) before waiting for the buffer’s sleep-lock (line 69)?

kernel/bio.c
57static struct buf *
60 struct buf *b;
64 // Is the block already cached?
65 for (b = bcache.head.next; b != &bcache.head; b = b->next) {
66 if (b->dev == dev && b->blockno == blockno) {
67 b->refcnt++;
70 return b;
71 }
72 }
74 // Not cached.
75 // Recycle the least recently used (LRU) unused buffer.
76 for (b = bcache.head.prev; b != &bcache.head; b = b->prev) {
77 if (b->refcnt == 0) {
78 b->dev = dev;
80 b->valid = 0;
81 b->refcnt = 1;
84 return b;
85 }
86 }
87 panic("bget: no buffers");
8warm-upType a number

kexit calls wakeup(p->parent) on line 353 while holding wait_lock. How many different p->lock spinlocks does that single wakeup call acquire (one after another)?

kernel/proc.c
574// Wake up all processes sleeping on channel chan.
575void
578 struct proc *p;
580 for (p = proc; p < &proc[NPROC]; p++) {
582 if (p->chan == chan) {
583 // If the process is waiting for wakeups on this channel,
584 // signal that the wakeup happened by clearing p->chan.
585 p->chan = 0;
587 // If this waiting process has gotten so far as to actually
588 // go to sleep, also set it back to RUNNING.
589 if (p->state == SLEEPING) {
591 }
592 }
594 }
decimal, 0x hex or 0b binary
9solidClick the line

allocproc returned np with np->lock held. Click the line that keeps kfork from holding a p->lock while it waits for wait_lock.

kernel/proc.c
284 // increment reference counts on open file descriptors.
285 for (i = 0; i < NOFILE; i++)
286 if (p->ofile[i])
288 np->cwd = idup(p->cwd);
290 safestrcpy(np->name, p->name, sizeof(p->name));
304 return pid;

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

10solidChoose one

Between line 294 and line 300, kfork holds no lock on the new child at all. Why is that safe?

kernel/proc.c
284 // increment reference counts on open file descriptors.
285 for (i = 0; i < NOFILE; i++)
286 if (p->ofile[i])
288 np->cwd = idup(p->cwd);
290 safestrcpy(np->name, p->name, sizeof(p->name));
304 return pid;
11solidPut in order

Put the steps of kexit (from line 347 on) in the order they happen.

kernel/proc.c
345 p->cwd = 0;
349 // Give any children to init.
352 // Parent might be sleeping in wait().
362 // Jump into the scheduler, never to return.
364 panic("zombie exit");
  1. release(&wait_lock), then sched()
  2. acquire(&wait_lock)
  3. reparent(p): hand any children to init
  4. p->state = ZOMBIE (with xstate)
  5. wakeup(p->parent)
  6. acquire(&p->lock)
12solidChoose one

Suppose someone moved acquire(&p->lock) (line 355) up to just before wakeup(p->parent) (line 353), so the exiting process holds its own lock during the wakeup. What happens?

kernel/proc.c
349 // Give any children to init.
352 // Parent might be sleeping in wait().
362 // Jump into the scheduler, never to return.
13solidClick the line

sys_unlink holds the parent directory dp locked and may then lock the entry ip inside it. Click the line that stops it from ever locking a parent while holding its child.

kernel/sysfile.c
213 if ((dp = nameiparent(path, name)) == 0) {
215 return -1;
216 }
220 // Cannot unlink "." or "..".
221 if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0)
222 goto bad;
224 if ((ip = dirlookup(dp, name, &off)) == 0)
225 goto bad;
228 if (ip->nlink < 1)
229 panic("unlink: nlink < 1");
230 if (ip->type == T_DIR && !isdirempty(ip)) {
232 goto bad;
233 }

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

14solidChoose one

In create, when the name already exists, line 281 unlocks the parent dp before line 282 locks the existing ip. Which input would hang the process if the two lines were swapped (lock ip while still holding dp)?

kernel/sysfile.c
258static struct inode *
259create(char *path, short type, short major, short minor)
261 struct inode *ip, *dp;
262 char name[DIRSIZ];
264 if ((dp = nameiparent(path, name)) == 0)
265 return 0;
269 if (dp->nlink == 0) {
271 return 0;
272 }
274 // a new directory's ".." would push dp->nlink past its maximum
275 if (type == T_DIR && dp->nlink >= NLINK_MAX) {
277 return 0;
278 }
280 if ((ip = dirlookup(dp, name, 0)) != 0) {
283 if (type == T_FILE && (ip->type == T_FILE || ip->type == T_DEVICE))
284 return ip;
286 return 0;
287 }
289 if ((ip = ialloc(dp->dev, type)) == 0) {
291 return 0;
292 }
15solidFill in the machine state

cat called exit(0) (a system call). kexit has just executed line 360, release(&wait_lock), and is about to call sched on line 363. What is the state of its hart?

kernel/proc.c
349 // Give any children to init.
352 // Parent might be sleeping in wait().
362 // Jump into the scheduler, never to return.
364 panic("zombie exit");
16solidPut in order

rm x decrements x’s link count and iupdate writes the inode through the log. Put the waits and locks of this path in the order they are taken (outermost first). When the inode’s block is new to this transaction, log_write calls bpin, and at that moment all five are in effect at once.

  1. log.lock (log_write)
  2. The inode block’s buffer sleep-lock (bread in iupdate)
  3. bcache.lock (bpin)
  4. x’s inode sleep-lock (ilock)
  5. begin_op: room in the log (outstanding incremented)
17solidMatch the pairs

Each place below avoids a deadlock in a different way. Match each one with its technique.

18solidChoose one

namex locks a directory, looks up the next name, and then (line 720) unlocks it before the next iteration locks the entry it found. Why not keep the directory locked while locking the next one?

kernel/fs.c
691static struct inode *
692namex(char *path, int nameiparent, char *name)
694 struct inode *ip, *next;
696 if (*path == '/')
698 else
701 while ((path = skipelem(path, name)) != 0) {
703 if (ip->type != T_DIR) {
705 return 0;
706 }
707 if (ip->nlink == 0) {
709 return 0;
710 }
711 if (nameiparent && *path == '\0') {
712 // Stop one level early.
714 return ip;
715 }
716 if ((next = dirlookup(ip, name, 0)) == 0) {
718 return 0;
719 }
722 }
725 return 0;
726 }
727 return ip;
19solidTrue or false, and why

True or false: if a process calls ilock on an inode whose lock it already holds, xv6 panics, just as acquire does for a spinlock.

kernel/sleeplock.c
21void
25 while (lk->locked) {
30 }
31 lk->locked = 1;
32 lk->pid = myproc()->pid;

Why?

20deepChoose all that apply

In this kernel, which of these locks are ever acquired while the acquiring hart holds a p->lock?

21deepType a number

Suppose bget's lines 68 and 69 were swapped, so it calls acquiresleep(&b->lock) while still holding bcache.lock, and the buffer is held by another process. Inside acquiresleep, the process reaches sleep and then sched. What is mycpu()->noff when sched checks it on line 487?

kernel/sleeplock.c
21void
25 while (lk->locked) {
30 }
31 lk->locked = 1;
32 lk->pid = myproc()->pid;
decimal, 0x hex or 0b binary
22deepType a number

rm drops the last reference to an unlinked inode, so iput calls acquiresleep(&ip->lock) on line 362 while holding itable.lock. Inside acquiresleep, at line 32 the call to myproc runs its own push_off. What is noff at that moment (inside myproc, before its pop_off)?

kernel/sleeplock.c
21void
25 while (lk->locked) {
30 }
31 lk->locked = 1;
32 lk->pid = myproc()->pid;
decimal, 0x hex or 0b binary
23deepChoose one

Imagine filewrite called ilock before begin_op. The log already holds 15 blocks from this group (LOGBLOCKS is 30, MAXOPBLOCKS 10), and rm x on another hart is inside its own transaction (outstanding 1) and about to lock x. The writer locks x, then calls begin_op. What happens?

kernel/log.c
127// called at the start of each FS system call.
128void
132 while (1) {
138 } else if (log.lh.n + (log.outstanding + 1) * MAXOPBLOCKS > LOGBLOCKS) {
139 // this op might exhaust log space; wait for commit.
144 } else {
147 break;
148 }
149 }
24deepChoose one

A modified kernel deletes kfork's lines 294 and 300, so it takes wait_lock while holding the half-built child’s np->lock. Under a fork/exit stress test it froze. The other hart in the cycle never names that child’s lock in its code. Which code took the child’s lock on the other side?

kernel/proc.c
284 // increment reference counts on open file descriptors.
285 for (i = 0; i < NOFILE; i++)
286 if (p->ofile[i])
288 np->cwd = idup(p->cwd);
290 safestrcpy(np->name, p->name, sizeof(p->name));
304 return pid;
25deepTrue or false, and why

True or false: if you draw a graph of lock classes (an edge A → B whenever some path holds a lock of class A while acquiring one of class B), this kernel’s graph has no cycle.

Why?