malloc
OS in Rust
Homework
- Homeworks are due on the Friday after the week they correspond to lecture.
- So 9 days after the corresponding lab.
Requirements
-
- Unless you are storing it (e.g. as an argument to
setter). - Use arrays and raw pointers.
- If you can’t solve it without them, use them, but…
- …at least understand why it’s hard and that there’s an alternative.
- If you hand-implement a vector-like on top of static mut within the overhead allowance, you may use your own vectors
- Unless you are storing it (e.g. as an argument to
main
Code
Update: Don’t Assume
i32
An early submitted solution trivialized the autograder by assuming all provided values to getter and setter would be of type i32.
This assumption side-steps the core learning objective of working with memory objects of arbitrary size.
Ensure your solution does not assume i32, and make some effort to test. You may want to test collection types as well (like Vec<T> and String).
src/main.rs
use malloc::*; // crate name
fn main() {
let p0 = malloc(16).unwrap();
let p1 = malloc(32).unwrap();
let x = 0x44332211;
let y = 0x12345678;
setter(x, p0);
setter(y, p1);
let z: i32 = getter(p0);
let w: i32 = getter(p1);
assert!(x == z);
assert!(y == w);
println!("A+");
// Advanced topics.
// Big alloc should fail
assert!(malloc(2048).is_none());
println!("A++");
// Allocs totaling > SIZE should fail
// We have alloc (16 + 32) * 8 = 384 of 1024
// Try annother small malloc
malloc(32).unwrap();
// And then one too large.
assert!(malloc(64).is_none());
println!("A+++");
// Easiest to test these together:
// - Gets to uninitialized memory should fail
// - You should write free()
// No graceful way to autotest these. Left as an exercise to the interested student.
}Explanation
- This is based on the C language
mallocfunction.- Probably the most important function.
- My slides on C
Malloc
- We begin with calls to our own
malloc:
malloctakes one argument, ausize, the number of bytes to allocate.- It returns a
Option<usize>, the “offset” of those bytes from general reference point. - We can regard this as the address.
- We can ask
mallocfor more memory then is available, and then we getNone.
- It returns a
- Here is my
malloc, with code removed.
src/lib.rs
// Return an index in BUS of s reserved bytes
pub fn malloc(s: usize) -> Option<usize> {
unsafe {
// Ensure BUS is initialized.
< 3 lines snipped >
// Reserve a block of s bytes
< 4 lines snipped >
// Scan for a contigious region of size s
// In s > 8, word level allocation
// "Could be more efficient" it's an exercise!
< ~20 lines snipped>
}
return None;
}- Some interest things here:
- This relies on a new Rust “thing”, a
static mut.- They may hold the record for being the most unsafe.
- Think of it maybe as a Python global.
- I named mine
BUS.
- This relies on a helper function to initialize
BUS- I am requiring the following implementation for your
BUS: - For me, declared at the beginning of
lib
- I am requiring the following implementation for your
- This relies on a new Rust “thing”, a
Static mut
src/lib.rs
- The requirements are:
- A
static mutof a fixed sizeu8array. - Must support any size which is a power of 2.
- May not use any other
static mut- An astute observer will note that then any tracking information related to
BUSmust be stored within BUS itself - This is non-trivial
- An astute observer will note that then any tracking information related to
- A
- I am not aware of high quality reference materials on
static mut, here is an example use apparently.
static mut COUNTER: u32 = 0;
fn add_to_counter(inc: u32) {
// SAFETY: There are no other threads which could be accessing `COUNTER`.
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_counter(42);
// SAFETY: There are no other threads which could be accessing `COUNTER`.
unsafe {
dbg!(COUNTER);
}
}- This fixed-size array of bits is meant to be consistent with underlying hardware, at least theoretically.
Initialization
- You are not required to use a helper for this, but I did.
- I am permitting/encouraging an
assert!that enforced power-of-two sizing. - Naively using
BUSdidn’t work well for me.- I had to check to make sure I wasn’t giving away bits already in use.
- So I had to persist some state
- I had to use memory to provide memory.
- I just reserved some bits at the beginning of the array as a validity bitmask
- If a bit is set to 1, the corresponding byte is in use.
- So a 1-in-8 overhead cost.
- This is why 64 bit words are popular.
- You are permitted have overhead costs as high as 1-in-4 if you need them.
- However, I had to mark within that bitmask that the bitmask was already using memory.
- This formed my
init.
src/lib.rs
// Zero the array except the mask.
fn init() {
unsafe {
// Initialize mask
// The following explodes if SIZE isn't a power of 2
assert!(SIZE & (SIZE - 1) == 0);
// First SIZE >> 3 bits are reserved as a validty byte/bit mask
< snip >
// Which has to reserve enough bytes for itself.
< snip >
// Set to 1
< snip >
// Initialize memory
// Set to zero.
< snip >
}
return;
}Setter
- After the calls to
mallocwithin main, we use a wrapping functionsetterto set memory at the location reserved by the malloc to have certain values.
- This maybe isn’t the most typical way to access memory.
- A more common metaphor is
lwandswmore - I found this metaphor more interesting.
- Words are fixed size, and felt quite trivial.
- A more common metaphor is
- Given some value returned by malloc, store up to that many bytes of information within the
BUS.mallocandsettertogether are responsible for ensuring the correctness and consistency of these bytes.- In this case, I malloc much more than I needed (16 and 32 bytes, respectively, for 4 byte words).
- This is allowed, but wasteful.
- It also makes testing easier.
- We note that
setterdoes not ask have an argument for the size of memory being set.- It is your responsibility to infer this size using the type of the arguments.
- This is to learn Rust, not to learn about memory, so a secondary objective but one I found worthwhile.
src/lib.rs
- The instructional staff is diligently working to infer the most graceful way to handle this operation, and many current solutions potentially introduce undefined behavior.
- I summarize my approach as follows:
- I cast a reference to the value as a raw pointer.
- I cast that raw pointer to
usize. - I use a for loop to performing an assignment operation.
- I use
size_of_valto determine how many times to loop. - I assign a location in the
BUSto be equal to something.- That something is the relevant byte, which I reach through a combination of casts and arithmetic.
- I use
Getter
- After the calls to “set” values, there are calls to retrieve the values (for latter inspection).
src/main.rs
- My code was quite similar to setting, but with the updates in the opposite direction.