253 lines
10 KiB
Markdown
253 lines
10 KiB
Markdown
|
|
||
|
This program, afs.py, allows you to experiment with the cache consistency
|
||
|
behavior of the Andrew File System (AFS). The program generates random client
|
||
|
traces (of file opens, reads, writes, and closes), enabling the user to see if
|
||
|
they can predict what values end up in various files.
|
||
|
|
||
|
Here is an example run:
|
||
|
|
||
|
prompt> ./afs.py -C 2 -n 1 -s 12
|
||
|
|
||
|
Server c0 c1
|
||
|
file:a contains:0
|
||
|
open:a [fd:0]
|
||
|
write:0 value? -> 1
|
||
|
close:0
|
||
|
open:a [fd:0]
|
||
|
read:0 -> value?
|
||
|
close:0
|
||
|
file:a contains:?
|
||
|
prompt>
|
||
|
|
||
|
The trace is fairly simple to read. On the left is the server, and each column
|
||
|
shows actions being taken on each of two clients (use -C <clients> to specify
|
||
|
a different number). Each client generates one random action (-n 1), which is
|
||
|
either the open/read/close of a file or the open/write/close of a file. The
|
||
|
contents of a file, for simplicity, is always just a single number.
|
||
|
|
||
|
To generate different traces, use '-s' (for a random seed), as always. Here we
|
||
|
set it to 12 to get this specific trace.
|
||
|
|
||
|
In the trace, the server shows the initial contents of all the files in the
|
||
|
system:
|
||
|
|
||
|
file:a contains:0
|
||
|
|
||
|
As you can see in this trace, there is just one file (a) and it contains the
|
||
|
value 0.
|
||
|
|
||
|
Time increases downwards, and what is next is client 0 (c0) opening the file
|
||
|
'a' (which returns a file descriptor, 0 in this case), writing to that
|
||
|
descriptor, and then closing the file.
|
||
|
|
||
|
Immediately you see the first question posed to you:
|
||
|
|
||
|
write:0 value? -> 1
|
||
|
|
||
|
When writing to descriptor 0, you are overwriting an existing value with the
|
||
|
new value of 1. What was that old value? (pretty easy in this case: 0).
|
||
|
|
||
|
Then client 1 begins doing some work (c1). It opens the file, reads it, and
|
||
|
closes it. Again, we have a question to answer:
|
||
|
|
||
|
read:0 -> value?
|
||
|
|
||
|
When reading from this file, what value should client 1 see? Again, given AFS
|
||
|
consistency, the answer is straightforward: 1 (the value placed in the file
|
||
|
when c0 closed the file and updated the server).
|
||
|
|
||
|
The final question in the trace is the final value of the file on the server:
|
||
|
|
||
|
file:a contains:?
|
||
|
|
||
|
Again, the answer here is easy: 1 (as generated by c0).
|
||
|
|
||
|
To see if you have answered these questions correctly, run with the '-c' flag
|
||
|
(or '--compute'), as follows:
|
||
|
|
||
|
prompt> ./afs.py -C 2 -n 1 -s 12 -c
|
||
|
|
||
|
Server c0 c1
|
||
|
file:a contains:0
|
||
|
open:a [fd:0]
|
||
|
write:0 0 -> 1
|
||
|
close:0
|
||
|
open:a [fd:0]
|
||
|
read:0 -> 1
|
||
|
close:0
|
||
|
file:a contains:1
|
||
|
prompt>
|
||
|
|
||
|
From this trace, you can see that all the question marks have been filled in
|
||
|
with answers.
|
||
|
|
||
|
More detail is available on what has happened too, with the '-d' ('--detail')
|
||
|
flag. Here is an example that shows when each client issued a get or put of a
|
||
|
file to the server:
|
||
|
|
||
|
prompt> ./afs.py -C 2 -n 1 -s 12 -c -d 1
|
||
|
|
||
|
Server c0 c1
|
||
|
file:a contains:0
|
||
|
open:a [fd:0]
|
||
|
getfile:a c:c0 [0]
|
||
|
|
||
|
write:0 0 -> 1
|
||
|
|
||
|
close:0
|
||
|
putfile:a c:c0 [1]
|
||
|
|
||
|
open:a [fd:0]
|
||
|
getfile:a c:c1 [1]
|
||
|
|
||
|
read:0 -> 1
|
||
|
|
||
|
close:0
|
||
|
|
||
|
file:a contains:1
|
||
|
prompt>
|
||
|
|
||
|
You can show more with higher levels of detail, including cache invalidations,
|
||
|
the exact client cache state after each step, and extra diagnostic
|
||
|
information. We'll show these in one more example below.
|
||
|
|
||
|
Random client actions are useful to generate new problems and try to solve
|
||
|
them; however, in some cases it is useful to control exactly what each client
|
||
|
does in order to see specific AFS behaviors. To do this, you can use the '-A'
|
||
|
and '-S' flags (either together or in tandem).
|
||
|
|
||
|
The '-S' flag lets you control the exact schedule of client actions. Assume our
|
||
|
example above. Let's say we wish to run client 1 in entirety first; to achieve
|
||
|
this end, we simply run the following:
|
||
|
|
||
|
prompt> ./afs.py -C 2 -n 1 -s 12 -S 111000
|
||
|
|
||
|
Server c0 c1
|
||
|
file:a contains:0
|
||
|
open:a [fd:0]
|
||
|
read:0 -> value?
|
||
|
close:0
|
||
|
open:a [fd:0]
|
||
|
write:0 value? -> 1
|
||
|
close:0
|
||
|
file:a contains:?
|
||
|
prompt>
|
||
|
|
||
|
The -S flag here is passed "111000" which means "run client 1, then client 1,
|
||
|
then 1 again, then 0, 0, 0, and then repeat (if need be)". The result in this
|
||
|
case is client 1 reading file a before client 1 writes it.
|
||
|
|
||
|
The '-A' flag gives exact control over which actions the clients take. Here is
|
||
|
an example:
|
||
|
|
||
|
prompt> ./afs.py -s 12 -S 011100 -A oa1:r1:c1,oa1:w1:c1
|
||
|
|
||
|
Server c0 c1
|
||
|
file:a contains:0
|
||
|
open:a [fd:1]
|
||
|
open:a [fd:1]
|
||
|
write:1 value? -> 1
|
||
|
close:1
|
||
|
read:1 -> value?
|
||
|
close:1
|
||
|
file:a contains:?
|
||
|
prompt>
|
||
|
|
||
|
In this example, we have specified the following via -A "oa1:r1:c1,oa1:w1:c1".
|
||
|
The list splits each clients actions by a comma; thus, client 0 should do
|
||
|
whatever "oa1:r1:c1" indicates, whereas client 1 should do whatever the string
|
||
|
"oa1:w1:c1" indicates. Parsing each command string is straightforward: "oa1"
|
||
|
means open file 'a' and assign it file descriptor 1; "r1" or "w1" means read
|
||
|
or write file descriptor 1; "c1" means close file descriptor 1.
|
||
|
|
||
|
So what value will the read on client 0 return?
|
||
|
|
||
|
We can also see the cache state, callbacks, and invalidations with a few extra
|
||
|
flags (-d 7):
|
||
|
|
||
|
prompt> ./afs.py -s 12 -S 011100 -A oa1:r1:c1,oa1:w1:c1 -c -d 7
|
||
|
|
||
|
Server c0 c1
|
||
|
file:a contains:0
|
||
|
open:a [fd:1]
|
||
|
getfile:a c:c0 [0]
|
||
|
[a: 0 (v=1,d=0,r=1)]
|
||
|
|
||
|
open:a [fd:1]
|
||
|
getfile:a c:c1 [0]
|
||
|
[a: 0 (v=1,d=0,r=1)]
|
||
|
|
||
|
write:1 0 -> 1
|
||
|
[a: 1 (v=1,d=1,r=1)]
|
||
|
|
||
|
close:1
|
||
|
putfile:a c:c1 [1]
|
||
|
callback: c:c0 file:a
|
||
|
invalidate a
|
||
|
[a: 0 (v=0,d=0,r=1)]
|
||
|
[a: 1 (v=1,d=0,r=0)]
|
||
|
|
||
|
read:1 -> 0
|
||
|
[a: 0 (v=0,d=0,r=1)]
|
||
|
|
||
|
close:1
|
||
|
|
||
|
file:a contains:1
|
||
|
prompt>
|
||
|
|
||
|
From this trace, we can see what happens when client 1 closes the (modified)
|
||
|
file. At that point, c1 puts the file to the server. The server knows that c0
|
||
|
has the file cached, and thus sends an invalidation to c0. However, c0 already
|
||
|
has the file open; as a result, the cache keeps the old contents until the
|
||
|
file is closed.
|
||
|
|
||
|
You can see this in tracking the cache contents throughout the trace
|
||
|
(available with the correct -d flag, in particular any value which sets the
|
||
|
3rd least significant bit to 1, such as -d 4, -d 5, -d 6, -d 7, etc.). When
|
||
|
client 0 opens the file, you see the following cache state after the open is
|
||
|
finished:
|
||
|
|
||
|
[a: 0 (v=1,d=0,r=1)]
|
||
|
|
||
|
This means file 'a' is in the cache with value '0', and has three bits of
|
||
|
state associated with it: v (valid), d (dirty), and r (reference count). The
|
||
|
valid bit tracks whether the contents are valid; it is now, because the cache
|
||
|
has not been invalidated by a callback (yet). The dirty bit changes when the
|
||
|
file has been written to and must be flushed back to the server when
|
||
|
closed. Finally, the reference count tracks how many times the file has been
|
||
|
opened (but not yet closed); this is used to ensure the client gets the old
|
||
|
value of the file until it's been closed by all readers and then re-opened.
|
||
|
|
||
|
The full list of options is available here:
|
||
|
|
||
|
Options:
|
||
|
-h, --help show this help message and exit
|
||
|
-s SEED, --seed=SEED the random seed
|
||
|
-C NUMCLIENTS, --clients=NUMCLIENTS
|
||
|
number of clients
|
||
|
-n NUMSTEPS, --numsteps=NUMSTEPS
|
||
|
ops each client will do
|
||
|
-f NUMFILES, --numfiles=NUMFILES
|
||
|
number of files in server
|
||
|
-r READRATIO, --readratio=READRATIO
|
||
|
ratio of reads/writes
|
||
|
-A ACTIONS, --actions=ACTIONS
|
||
|
client actions exactly specified, e.g.,
|
||
|
oa1:r1:c1,oa1:w1:c1 specifies two clients; each opens
|
||
|
the file a, client 0 reads it whereas client 1 writes
|
||
|
it, and then each closes it
|
||
|
-S SCHEDULE, --schedule=SCHEDULE
|
||
|
exact schedule to run; 01 alternates round robin
|
||
|
between clients 0 and 1. Left unspecified leads to
|
||
|
random scheduling
|
||
|
-p, --printstats print extra stats
|
||
|
-c, --compute compute answers for me
|
||
|
-d DETAIL, --detail=DETAIL
|
||
|
detail level when giving answers (1:server
|
||
|
actions,2:invalidations,4:client cache,8:extra
|
||
|
labels); OR together for multiple
|
||
|
|
||
|
Read the AFS chapter, and answer the questions at the back, or just explore
|
||
|
this simulator more on your own to increase your understanding of AFS.
|
||
|
|