Allocation in Chickadee
- WeensyOS has a dirt-simple page allocator
- An array
physpages
tracks which pages are freerefcount == 0
means free
- Chickadee’s handout allocator uses a similar design
WeensyOS allocator
void* kalloc(size_t sz) {
if (sz > PAGESIZE) {
return nullptr;
}
int pageno = 0;
for (int tries = 0; tries != NPAGES; ++tries) {
uintptr_t pa = pageno * PAGESIZE;
if (allocatable_physical_address(pa)
&& physpages[pageno].refcount == 0) {
++physpages[pageno].refcount;
memset((void*) pa, 0xCC, PAGESIZE);
return (void*) pa;
}
pageno = (pageno + 1) % NPAGES;
}
return nullptr;
}
Chickadee allocator
void* kalloc(size_t sz) {
if (sz == 0 || sz > PAGESIZE) {
return nullptr;
}
auto irqs = page_lock.lock();
void* ptr = nullptr;
// skip over reserved and kernel memory
auto range = physical_ranges.find(next_free_pa);
while (range != physical_ranges.end()) {
if (range->type() == mem_available) {
// use this page
ptr = pa2kptr<void*>(next_free_pa);
next_free_pa += PAGESIZE;
break;
} else {
// move to next range
next_free_pa = range->last();
++range;
}
}
page_lock.unlock(irqs);
if (ptr) {
// tell sanitizers the allocated page is accessible
asan_mark_memory(ka2pa(ptr), PAGESIZE, false);
// initialize to `int3`
memset(ptr, 0xCC, PAGESIZE);
}
return ptr;
}
- What explains the differences?
Disadvantages of this allocator
- Finding a free page takes \(O(N)\) time, where \(N\) is the number of physical pages in the system
- Which might be zillions
- Impossible to allocate objects larger than a page
- Some devices, such as AHCI disks, require structures that are more than one physical page
Buddy allocation
- Supports arbitrary-sized allocations
- Finding a free page takes \(O(\log M)\) time, where
\(M\) is the maximum allocatable size
- So, like, at most 30 operations, rather than billions
- Freeing an allocation likewise takes \(O(\log M)\) time
- Quick!
- Disadvantage: Wasted space
- Small allocations and fragmentation
Blocks and buddies
- Divide allocatable memory into blocks with sizes equal to powers of 2
- For instance, sizes \(2^{12}, 2^{13}, 2^{14}, \dots, 2^{30}\)
- A block of size \(2^o\) can be split into two blocks of size \(2^{o-1}\), or coalesced with an adjacent block of size \(2^o\) into a single block of size \(2^{o+1}\)
- Limit coalescing to buddies
- Each order-\(o\) block has exactly one order-\(o\) buddy
- Splitting turns one block into two buddies
- A block can only coalesce with its buddy
- Sometimes adjacent free blocks can’t be coalesced
- Monogamous coalescing?
- To allocate \(s\) bytes, find a free block
of minimum adequate size
- That is, a block of size \(2^o\) where \(2^{o-1} < s \leq 2^o\)
- Might require splitting blocks!
Example: Split to allocate 4095 bytes
-
Start with an order-15 block
-
Split into two order-14 buddies
-
Left buddy splits into two order-13 buddies
-
Left buddy splits into two order-12 buddies
-
Allocate one of those
-
The next allocation of order 12 doesn’t need to split, since a free block of the proper order is already available!
-
But the one after that will need to split an order-13 block into order-12 buddies…
-
…one of which will be allocated
Freeing
- To free an allocation, mark its containing block as free and add it back to the pool of free blocks
- Invariant: Blocks must be maximally coalesced
- If a block and its buddy are both free, they are coalesced into a single larger block
- Which might itself be coalesced!
Example: Free some order-12 allocations
-
Initial state
-
Free one block; no coalescing since buddy is not free
-
Free another block; no coalescing since buddy is not free—even though two free blocks of order 12 are adjacent
-
Another free creates adjacent free buddies…
-
…which must be coalesced
-
The final free starts a chain of coalescing…
-
…
-
…
-
…until just the original order-15 block is left
Buddy allocation data structures (class discussion)
- What questions does the buddy allocation algorithm need to ask?
- What data structures can answer those questions?
Questions asked by the buddy allocation algorithm
- Which blocks of order \(o\) are free?
- Data structure needed: free list that links free blocks of order \(o\)
- Example:
list<T, &T::links> free_list[MAXORDER];
- Secondary question 1: Where are the
list_links
stored? - Secondary question 2: Is a doubly-linked list necessary? (Yes!)
- What is the address of a particular block’s buddy?
- No data structure needed!
- Bitwise arithmetic suffices
- Order-\(o\) block with physical address \(a\) has buddy at physical address \(a \oplus 2^o\)
- In C++, that’s
a ^ (1 << o)
- Note: This assumes that all blocks are “aligned at 0,” meaning that the physical address of any order-\(o\) block is a multiple of \(2^o\). If you do not align your blocks, the math is more complicated.
- Is a particular block’s buddy free or allocated?
- The buddy is at a specific address
- Want \(O(1)\) lookup (otherwise buddy allocation will run slower than \(O(\log M)\))
- Data structure needed: page array
- Indexed by address (i.e., page number)
- Contains allocated bit
- For
kfree
: What size object is being freed?- Again, need \(O(1)\) lookup by address
- Page array can contain order of allocated block
Testing and invariants
- We want programs to work right
- Let’s program so that our programs are more likely to work right
- Validation: “a process designed to increase our confidence that a program will function as we intend it to” [LG86]
- Debugging: “the process of ascertaining why a program is not functioning properly”
- Defensive programming: “the practice of writing programs in a way designed specifically to ease the process of validation and debugging”
Validation 1: Testing
- Write down the correct answer
- Check that the code produces the correct answer
- Requires a notion of a correct answer—the specification
Unit tests
- Check that a program unit, such as a function or class, works according to its spec
- Black-box testing: Uses the interface to the program unit, not the implementation
- Grey-box or white-box testing: Uses both the interface and the implementation
Integration tests
- Check that a whole program behaves as expected
- Often harder to create than unit tests because a whole program’s spec is usually big and unwieldy
- Unix nerds represent
- But there are shortcuts
- Simply encoding a program’s current behavior can help catch unexpected changes
Testing goals
- Test pragmatically
- Testing is an ethical necessity
- But complete testing can be impossible and mistaken
- Finite budget of time available for testing and development
- How to use that budget most effectively?
- Want to find all the bugs
- Relates to the different ways bugs can occur
- Want to find the most important bugs
- Relates to frequency, consequences of failure, …
- “Our goal must be to find a reasonably small set of tests that will allow us to approximate the information we would have obtained through exhaustive testing.”
How should we measure a test suite?
Coverage
- Metric for test suite quality
- Specifications and implementations both have paths or branches
- Coverage measures how many of those distinct paths are tested by the test suite
- Specification path coverage: paths through the specification
- Implementation path coverage: paths through the code
- Implementation statement coverage: fraction of lines of code evaluated
Validation 2: Proving
- Prove formally that code matches its spec
- Awesome progress recently, still (too) rare in practice
- Focus of research effort in programming languages
- “Beware of bugs in the above code; I have only proved it correct, not tried it.” —Don Knuth
- Some forms of proving are common!
- Type safety eliminates some errors (assuming compiler correctness)
- Memory safety, static checking, …
Invariants
- Logical statements about a program that must hold in every correct execution
Classes of invariant
- Representation invariants
- Property of a data structure’s representation
- Functions that modify the data structure are allowed to break the invariant, but they must restore it before returning
- Preconditions
- Property of arguments to a function, or system state before a function is called
- Postconditions
- Property of function return values, or system state after the function returns
- Loop invariants, etc.
Assertions
- Executable check that an invariant holds
- In C/C++:
assert(EXPR)
EXPR
should not have side effects
- Ref: Use of Assertions
Assertion patterns
-
Invariant checks
- Precondition invariants at top of function
- Representation invariants: extract to a separate method, such as
check
; call on entry, on exit, wherever - Postcondition invariants at bottom of function
-
Failsafes
- “I think that
X
is true at this point in the code” - But
X
is not immediately obvious from context - Common to add and remove these as you debug
- “I think that
-
Assertion expansion
-
Assertion failure messages may not have sufficient context
-
gdb
can help (set a breakpoint atpanic
, use backtrace) -
Or expand the assertion:
assert(!yields_ || contains(yields_)); => if (!(!yields_ || contains(yields_))) { log_printf("CRAP %p vs. %p\n", this, yields_); } assert(!yields_ || contains(yields_));
-
Or use
assert_eq
,assert_ge
, etc.
-
Test suite construction
- A great test suite is repeatable
- Otherwise, can’t evaluate the fix
- Randomness can achieve good coverage without much thinking
- Use deterministic randomness for repeatability:
srand
- Use deterministic randomness for repeatability:
- Unit should export functions useful for testing
- “it is worth your while to write a considerable amount of code whose only purpose is to help you examine intermediate results”
- For example, a function that prints statistics about your buddy allocator to the log—or even the contents of your free lists
Property testing
- Boundary conditions
- Property testing (e.g., QuickCheck)
- Enumerate some interesting examples of objects of type T
- Example: integers?
- 0, 1, -1, 2,
INT_MIN
,INT_MAX
, …
- 0, 1, -1, 2,
- Example: lists?
- Empty list, singleton list, list with or without duplicates, sorted list, unsorted list, …
- Use this source library when testing a function that takes a T
- Can drive to interesting test cases faster than random testing
- Examples from buddy allocation?
Some buddy allocation properties
- Buddy-ness is an interesting property
- Test that frees buddies, or doesn’t
- Running out of free space is a boundary condition
- Write a test that allocates all free space, then frees it all, several times to catch leaks
Invariant-based testing
- Add assertions for important invariants that follow from the specification
- Look for ways to add new ones
- E.g., Keep a statistic that can be computed more than one way; assert the two calculations equal
- Examples from buddy allocation?
Some buddy allocation invariants
- The order-\(o\) free list contains pages whose order is \(o\)
- All free pages of order \(o\) are on the order-\(o\) free list
- No free list contains two adjacent buddies (they should have been coalesced)
- The sums of the sizes of all free blocks should equal the amount of free space (i.e., the amount of allocatable space at initialization time minus the amount of allocated space)
Debugging
- Debugging is science
- “The crux of the scientific method is to
- begin by studying already available data,
- form a hypothesis that is consistent with those data, and
- design and run a repeatable experiment that has the potential to refute the hypothesis.”
- Use hypotheses to narrow the problem down
- “This bug is caused by multiprocessor interactions.”
- Refutation: run with
NCPU=1
- Refutation: run with
- “This bug is caused by system calls.”
- Refutation: try to cause it with interrupts
- “This bug is caused by multiprocessor interactions.”
- Sometimes you don’t even know where to start!
- The engineer’s hypothesis: “This bug will be fixed by this fix.”
- Not infrequently, we don’t understand the bug until it’s fixed!
- But try to understand the bug even after “fixing” it!