Concurrency in Xv6
Locking patterns
Pattern 1: Design locked data structure
One lock for the set of items, plus one lock per item.
Example: The filesystemβs block cache.
Each cached block is stored in a struct buf
(kernel/buf.h:1). A struct buf
has a lock field which helps ensure that only one process uses a given disk block at a time.
When checking if a block is cached, or change the set of cached blocks, must hold the bcache.lock
; after the code founds the needed struct buf
, it can release bcache.lock
.
Pattern 2: Design locks to run in different threads
A lock is acquired at the start of a sequence that must appear atomic, and released when that sequence ends. If the sequence starts and ends in different functions, or different threads, or on different CPUs, then the lock acquire and release must do the same.
Example
acquire
processβ lock in yield
, but release in the scheduler thread rather than the acquiring process.
Another example is the acquiresleep
in ilock
(kernel/fs.c:290); this code often sleeps while reading the disk; it may wake up on a different CPU, which means the lock may be acquired and released on different CPUs.
Pattern 3: How to free an object
Problem
Freeing an object that is protected by a lock is hard. If other thread is waiting in acquire
to use the object; freeing the object when unlock will cause the waiting thread to malfunction.
Solution
Track how many references to the object exist.
Example
See pipeclose
(kernel/pipe.c:59) for an example; pi->readopen
and pi->writeopen
track whether the pipe has file descriptors referring to it.
Lock-like patterns
A lock protects the flag or reference count, it is the latter that prevents the object from being prematurely freed.
Example: Filesystem looks up path
namex
holds lock for each directory, release it before acquiring the next directory. Otherwise, it might deadlock itself when accessing a/./b
.
Solution
The solution is for the loop to carry the directory inode
over to the next iteration with its reference count incremented, but not locked.
Explain
next = dirlookup(ip, name, 0)
calls iget
, which increment ref count.
Each iteration only release current inode
, and set next inode
pointer, but not acquiring next directory lock.
Other lock-like patterns
Protect by implicit structure rather than locks
A data object may be protected from concurrency in different ways at different points in its lifetime, and the protection may take the form of implicit structure rather than explicit locks.
For example, when a physical page is free, it is protected by kmem.lock
(kernel/kalloc.c:24). If the page is then allocated as a pipe (kernel/pipe.c:23), it is protected by a different lock (the embedded pi->lock
). If the page is re-allocated for a new processβs user memory, it is not protected by a lock at all.
Disable interrupts
Disabling interrupts causes the calling code to be atomic with respect to timer interrupts that could force a context switch, and thus move the process to a different CPU.
No locks at all
A few places in xv6 share mutable data with no locks at all.
The started
variable in main.c (kernel/main.c:7), used to prevent other CPUs from running until CPU zero has finished initializing xv6; the volatile
ensures that the compiler actually generates load and store instructions.
Study Topics
c volatile
keyword
volatile
keyword[volatile (computer programming) - Wikipedia](https://en.wikipedia.org/wiki/Volatile_(computer_programming))
The volatile [keyword](https://en.wikipedia.org/wiki/Keyword_(computer_programming)) indicates that a [value](https://en.wikipedia.org/wiki/Value_(computer_science)) may change between different accesses, even if it does not appear to be modified. This keyword prevents an optimizing compiler from optimizing away subsequent reads or writes and thus incorrectly reusing a stale value or omitting writes. Volatile values primarily arise in hardware access ( memory-mapped I/O ), where reading from or writing to memory is used to communicate with peripheral devices , and in [threading](https://en.wikipedia.org/wiki/Thread_(computing)) , where a different thread may have modified a value.
There are memory barrier operations available on platforms (which are exposed in C++11) that should be preferred instead of volatile .
In C#, When a field is marked volatile, the compiler is instructed to generate a βmemory barrierβ or βfenceβ around it, which prevents instruction reordering or caching tied to the field.
In XV6, the volatile ensures that the compiler actually generates load and store instructions. Together with __sync_synchronize
, XV6 ensures started
is modified by CPU 0, and shares to other CPUs with no memory re-order. A similar function is __sync_fetch_and_add
.
Parallelism
Locking is primarily about suppressing parallelism in the interests of correctness. Because performance is also important, kernel designers often have to think about how to use locks in a way that achieves both correctness and good parallelism.
Last updated