9/11/16: Caches: Writing and the Single Slot Cache
But first -- some midterm review!
A couple of midterm review problems
Q1
Your friend shows you a binary bomb and explains that the number to be entered has to match the integer stored at the address 0x601048.
- What command would you type to see what number to enter?
- Your friend shows you the following display:
0x601048 <arr+8>: 0xe0 0xd1 0xee 0x0b 0x43 0x65 0x87 0x09
What number would you suggest entering? (You may express it in hexidecimal.)
They key here is that this is a little endian machine, so the byte at 0x601048 is the least significant byte. So, the value would need to be 0x0beed1e0
Q2
Another friend is working on the debugging malloc and shows you the following code (assume sz is properly defined and refers to the size the caller to the debugging malloc wishes to allocate):
void *m, *payload;
m = malloc(sizeof(struct meta) + sz); payload = m + sizeof(struct meta);
Your friend says that this runs just fine on the appliance but crashes on a different machine. Explain why?
Pointer arithmetic on void *
types is undefined behavior -- the
compiler is allowed to do anything it wants! If you want to add
sizeof(meta)
, you should be using a char *
; but better still is to
make the variable m
be of type struct meta *
and then set payload
(properly cast) to m + 1
.
Q3
In class you worked on a garbage collector implementation. Even if you did the exercise entirely correctly, there are cases where you might not successfully reclaim all unused space. Explain how this could happen.
We mark any allocation that might still be active and we determine that by scanning all the places variables might be and looking for things that look like addresses. However, if I have some value that happens to match the address of an allocation, the collector will assume that that value is, in fact, a pointer to the allocated region and will not free it. The allocator must make this conservative assumption to avoid destroying data that is still being used.
And now back to caching
We are going to continue to work in the context of Thursday's exercise. Once again, write out your answers using pencil and paper -- don't worry about compiling and running this.
Part 1: Writing
Let's see if we can make our single slot cache do some writing. For the
remainder of this exercise, let's build on the pos_tag, end_tag
representation from last time (modifying meanings slightly):
struct io61_file {
int fd;
unsigned char cbuf[BUFSIZ];
off_t tag; // file offset of the first character in the cache
off_t end_tag; // file offset of first invalid position in cache
off_t pos_tag; // file offset of next char to write
};
Q1: If we open a file for writing, how should we initialize the three tag fields of the io61_file structure?
We need to position our buffer at the beginning of the file and the buffer should not contain anything, so:
f->tag = f->pos_tag = f->end_tag = 0;
Now let's look at the write call:
// io61_write(f, buf, sz)
` // Write `sz` characters from `buf` to `f`. Returns the number of `
` // characters written on success; normally this is `sz`. Returns -1 if `
// an error occurred before any characters were written.
ssize_t io61_write(io61_file* f, const char* buf, size_t sz) {
}
Q2: For now, let's just worry about sequential writing; we'll get to seeking later. We first need to determine if there is space in the buffer to write any data. What expression checks for that condition?
if (f->pos_tag - f->tag < BUFSIZ)
Q3: What condition indicates that our buffer is full and we need to write data to the backing file?
if (f->pos_tag - f->tag == BUFSIZ)
Because standard IO implements a write-back cache, we may need to invoke
the write
system call at times in addition to inside io61_write
.
Q4: Can you think of two places (other than inside io61_write
)
where we may need to write data back?
When the user asks us to
When we close the file
Rather than invoke the write
system call directly from io61_write
,
we'll invoke in a function called io61_flush
, which looks like this:
// io61_flush(f)
` // Write all buffered data written to `f`. `
// Leaves tag, pos_tag, and end_tag in position to handle a sequential write.
Q5: Write the io61_flush(f)
function:
int io61_flush(io61_file* f) {
if (f->end_tag != f->tag) {
ssize_t n = write(f->fd, f->cbuf, f->end_tag - f->tag);
assert(n == f->end_tag - f->tag);
}
f->pos_tag = f->tag = f->end_tag;
return 0;
}
Q6: Now that you have io61_flush
written, complete the
io61_write
function.
If you want some guidance on implementing io61_write
, consider the two
cases:
- The data you are writing fits nicely in your buffer.
- The data you are writing does not fit into your current buffer.
As we did with the io61_read
call, you should be able to implement
this function calling memcpy
and io61_flush
from just one place.
ssize_t io61_write(io61_file* f, const char* buf, size_t sz) {
size_t pos = 0;
while (pos != sz) {
if (f->pos_tag - f->tag < BUFSIZ) { // There is still space in the buffer
ssize_t n = sz - pos; // Calculate bytes left to write
if (BUFSIZ - (f->pos_tag - f->tag) < n)
n = BUFSIZ - (f->pos_tag - f->tag);
memcpy(&f->cbuf[f->pos_tag - f->tag], &buf[pos], n);
f->pos_tag += n;
if (f->pos_tag > f->end_tag)
f->end_tag = f->pos_tag;
pos += n;
}
// The position should never exceed the end tag.
assert(f->pos_tag <= f->end_tag);
// Check if we've filled the buffer and if so, call flush to write data.
if (f->pos_tag - f->tag == BUFSIZ)
io61_flush(f);
}
return pos;
}
Part 2
It's time to handle seeks.
Here is our seek routine from last time:
int io61_seek(io61_file* f, off_t off) {
if (off < f->tag || off > f->end_tag) {
off_t r = lseek(f->fd, off, SEEK_SET);
if (r != off)
return -1;
f->tag = f->end_tag = off;
}
f->pos_tag = off;
return 0;
}
Q7: What must you do before calling lseek
? (Hint: think about
the possible state of your buffer.)
If you were writing this file, then you might have dirty data left in your buffer. Before you go seeking somewhere else and writing new data into your buffer, you'd better flush what's there.
Q8: Add a new field to your io61_file
structure so you can track
the information necessary to deal with Q7. (Hint: Take a look at the
arguments to io61_fdopen
in Assignment
3.)
int mode;
Q9: List all the functions you should update to reflect this new field.
io61_fdopen (set the mode)
io61_write (check the mode)
io61_close (check mode and call flush)
io61_flush (check mode for flushing)
Q10: Now modify io61_seek
to work in the presence of writes.
int io61_seek(io61_file* f, off_t off) {
if ((f->mode & O_ACCMODE) != O_RDONLY)
io61_flush(f);
if (off < f->tag || off > f->end_tag
|| (f->mode & O_ACCMODE) != O_RDONLY) {
off_t r = lseek(f->fd, off, SEEK_SET);
if (r != off)
return -1;
f->tag = f->end_tag = off;
}
f->pos_tag = off;
return 0;
}
Part 3
At the very end of last exercise, we offered the following Hint:
Ensure that f->pos_tag
is always aligned on a multiple of BUFSIZ
.
Q11: Why might you want the tag to be a multiple of BUFSIZ
?
There are at least two good reasons to make the the tag a multiple of
the BUFSIZ
. First, note that in the sequential case, this will happen
naturally. By enforcing this we make sure that sequential and random
access work well together. Second, if we look ahead to making our
single-slot cache a multi-slot cache, then making tags be a multiple of
BUFSIZ
ensures that we'll never have data from a specific offset in
the file reside in two different slots (and we will never have to
explicitly check for this condition).
Q12: In the exercise, we left BUFSIZ
unspecified. Let's now talk
about some constraints we might want to impose on BUFSIZ
. We started
off by suggesting you consider a BUFSIZ
of 8. Do you think this is a
good BUFSIZ
for Assignment 3? Why/Why not?
This is probably way too small to provide good performance. Eight bytes can just hold a long or a pointer! We want to amortize the cost of each disk IO by improving the likelihood that we'll get a lot of cache hits. So, we probably want something much larger than eight bytes.
Q13: Do you think 10 MB would be a good value for BUFSIZ
? Why/Why
not?
OK, we said larger, but 10 MB seems a bit on the large side. That would mean that even for a tiny file, we'd allocate multiple MB of memory. While memories are large, that seems quite wasteful.
Q14: Can you think of some constraints you want to place on BUFSIZ
or some guidelines you'd like to use to select its value?
So, how do we pick a BUFSIZ
? What features would we like to have in
our buffer size?
BUFSIZ
should be larger than the minimum block size of the storage layer from which we are obtaining data. In other words, if we can't actually access data from the lower layer in units smaller than N bytes, we should make ourBUFSIZ
at least N bytes. While standard IO can read any number of bytes from the operating system, we might want to think of designing for an underlying file system. Today's file systems rarely support blocks smaller than 4 KB, so that might be a reasonable minimum size.- Hmmm -- notice that 4 KB, we seem to naturally be thinking in powers of 2 -- that seems like a good idea too.
- Ideally we'd like to get enough data when we make the system call that we amortize the fixed overhead across a lot of bytes.
- At the same time, we don't want to read so much data that we consume an exorbitant amount of memory.
- So, we probably want some relatively small multiple of 4 KB