HW5: 220 points total; group project
Submission notes
The submission process is the same as HW3, except the name of the
tarball. That is, one member of a group will submit a single gzipped
tar file named group_xx_hw5.tgz
where xx is your two digit group
number (with leading zeros, if applicable).
The top level directory in the tarball must be hw5
.
Please use the kernel version 4.4.50.
In this part, we will implement an in-kernel key-value store, aka the fridge. The fridge will be accessed using the following four systems calls that you will add to the Linux kernel:
/*
* Initialize the Kernel Key-Value store.
*
* Returns 0 on success, and -1 on failure.
* The flags parameter is currently unused.
*
* The result of initializing twice (without an intervening
* kvv_destroy() call) is undefined.
*/
int kkv_init(int flags);
/*
* Destroy the Kernel Key-Value store, removing all entries
* and deallocating all memories.
*
* Returns the number of entries removed on success, and -1 on failure.
* The flags parameter is currently unused.
*
* After calling kkv_destroy(), the Kernel Key-Value store can be
* re-initialized by calling kkv_init() again.
*
* The result of destroying before initializing is undefined.
*/
int kkv_destroy(int flags);
/*
* If a key-value pair is found for the given key, the pair is
* REMOVED from the Kernel Key-Value store.
*
* The value is copied into the buffer pointed to by the "value" parameter,
* up to max_size bytes. Note that if the value was a string of
* length >= max_size, the string will be truncated and it will not be
* null-terminated.
*
* Returns 0 if a key-value pair was found, removed and returned.
* Returns -1 with errno set to ENOENT if the key was not found.
* Returns -1 on other failures, with errno set accordingly.
* The flags parameter is currently unused.
*
* The result of calling kkv_get() before initializing the Kernel Key-Value
* store is undefined.
*/
int kkv_get(uint32_t key, char *value, size_t max_size, int flags);
/*
* Insert a new key-value pair. The previous value for the key, if any, is
* replaced by the new value.
*
* The "size" bytes from the buffer pointed to by the "value" pointer will
* be copied. If the value is a string, make sure that
* size == strlen(value) + 1, so that the null character is stored as part
* of the value.
*
* Returns 0 on success, and -1 on failure.
* The flags parameter is currently unused.
*
* The result of calling kkv_put() before initializing the Kernel
* Key-Value store is undefined.
*/
int kkv_put(uint32_t key, char *value, size_t size, int flags);
The fridge will use a hash table defined as follows. You must use the data
structures as given. Please do not modify them in any way. The
fridge_data_structures.h
file, provided as skeleton code, contains the
structure definitions.
#define HASH_TABLE_LENGTH 17
/*
* A key-value pair.
*/
struct kkv_pair {
char *value;
uint32_t key;
size_t size;
};
/*
* A node in a linked list. Contains a key-value pair.
*/
struct kkv_ht_entry {
struct list_head entries;
struct kkv_pair kv_pair;
};
/*
* A bucket in the hash table.
* The hash table is an array of HASH_TABLE_LENGTH buckets.
*/
struct kkv_ht_bucket {
spinlock_t lock;
struct list_head entries;
uint32_t count;
};
Please read the following pages on kernel locks:
You will also want to refresh your memory on using kernel linked list
and allocating kernel memory using kmalloc()
. We did them in HW2.
Install libfridge.so
, the user space wrapper library for the fridge
system calls.
The skeleton code includes a tarball, fridge-1.0.tar.gz
, which
contains the source and scripts to build and install the libfridge
wrapper library onto your system.
Extract the tarball in a directory outside of your git repo. Take a
look at fridge.h
and libfridge.c
to understand what this library
is all about. There are many other files in the tarball, but they are
just boilerplate code created by GNU
Autotools.
Now build and install the library by following the instruction in the
README
file.
The last step in the README, sudo make install
, will copy fridge.h
and libfridge.so into /usr/local/include
and /usr/local/lib
,
respectively. It will also invoke ldconfig
, so that programs can
find the new library.
Implement the 4 system calls. Initially, don’t worry about locking. Make it work for single-thread test programs.
Now implement locking, so that put
and get
work with concurrent puts
and gets. Use the spin lock embedded in each bucket. Spin locks should
be grabbed and released as quickly as possible. Make sure to structure
your code to minimize the locked region. Make sure that you are holding
the lock only when necessary.
In this part, you can assume that, while the kkv_put()
and kkv_get()
are crazy concurrent, the kkv_init()
and kkv_destroy()
are called in
an orderly manner. That is, only a single call to kkv_init()
is made
in the beginning, and only a single call to kvv_destroy()
is made at
the end. We will deal with concurrency and misuse of init and
destroy in the next part.
The test programs – the ones provided as well as the ones you write –
invoke the system calls using the libfridge
wrappers, which means your
system call numbers must match those assumed in the library.
Please implement the fridge in a kernel module using the technique from HW4. Your repo will contain the following files:
README.txt
arch/x86/entry/syscalls/syscall_32.tbl
kernel/Makefile
kernel/kkv.c --> contains fridge syscalls
test/module/fridge_data_structures.h --> skeleton code, unmodified
test/module/Makefile --> updated to build fridge module
test/module/fridge.c --> contains fridge module code
test/Makefile
test/fridge_test.h
test/fridge_test_driver.c
test/hot_potato.c
test/simple_sequential.c
test/my-test-1.c
test/my-test-2.c
... --> your own test drivers, named anyway you like
You can either start a brand new sparse repo for HW5 or continue using the repo for HW4. If you continued from the HW4 repo, you’d have the following additional files in your repo which will be ignored for grading:
test/module/supermom.c
kernel/hw4.c
test/hw4-test.c
A working version of fridge.c
that passes the given test programs.
Your own test programs to further test your implementation. The graders will use additional test programs. Test your system calls as rigorously as you can!
The following info in your README.txt
:
What should happen when a user program misuses the system calls? What
happens when kkv_init()
gets called twice in a row? What happens when
kkv_destroy()
is called while the kernel is executing kkv_put()
?
In the API function comments shown above, certain behaviors are left undefined. For user level library functions, an easy way to implement undefined behaviors is to not do anything special, and possibly let the program crash.
This is not the case for system calls. System calls must be robust against all possible uses and misuses. A user program should never be able to crash the system or corrupt kernel data structures no matter what it does.
Modify the code so that the four system calls can be called in any order and
in any degree of concurrency between them. Of course, if a sequence of
calls doesn’t make sense, they don’t have to succeed. Returning -1
with
errno set to EPERM
is a reasonable course of action in those cases.
Your solution in this part should not sacrifice concurrency among puts and gets. Multiple puts and gets should be able to occur simultaneously if they operate on different buckets.
Your solution must allow concurrency between init/destroy and put/get.
For example, calling kkv_detroy()
and kkv_put()
at the same time
should not cause system crash or corruption on internal data structures.
A new version of fridge.c
.
Your own test program to stress-test your system calls. The graders will use additional test programs. Test your system calls as rigorously as you can!
The following info in your README.txt
:
Linux kernel provides the SLAB layer (also called the SLAB allocator) as a
more efficient way to manage memory for kernel-level data structures. It is
specifically designed as a memory management interface to be used for
repeated allocations and deallocations of the same structures, as we are
doing here with kkv_ht_entry
.
Read the following pages on the SLAB allocator:
Set up a SLAB cache for struct kkv_ht_entry
and replace calls to
kmalloc()
and kfree()
with the appropriate SLAB allocator calls for
this struct.
A new version of fridge.c
that correctly uses SLAB layer while
preserving all of the previous parts’ functionality.
Test driver to compare the performances between the previous version from part 2 and the SLAB version from this part.
The following info in your README.txt
:
In this part, we will modify the fridge implementation so that kkv_get()
system call supports blocking.
The following was the API documentation for kkv_get()
from part 1:
/*
* If a key-value pair is found for the given key, the pair is
* REMOVED from the Kernel Key-Value store.
*
* The value is copied into the buffer pointed to by the "value" parameter,
* up to max_size bytes. Note that if the value was a string of
* length >= max_size, the string will be truncated and it will not be
* null-terminated.
*
* Returns 0 if a key-value pair was found, removed and returned.
* Returns -1 with errno set to ENOENT if the key was not found.
* Returns -1 on other failures, with errno set accordingly.
* The flags parameter is currently unused.
*
* The result of calling kkv_get() before initializing the Kernel Key-Value
* store is undefined.
*/
int kkv_get(uint32_t key, char *value, size_t max_size, int flags);
Now, we will use the flags
parameter to specify blocking behavior. When
flags == 0
, kkv_get() will behave exactly the same as before, as described
in the API documentation above.
When flags == 1
, however, kkv_get() will change its behavior as follows:
Returns 0 if a key-value pair was found, removed and returned. This is the same behavior as the non-blocking case.
When a key-value pair for the given key is not present in the kernel key-value store, the calling process will block, waiting for a key-value pair for the key to be added to the key-value store.
When another thread adds a key-value pair for the key by calling
kkv_put()
, the waiting process will then wake up, remove the key-value
pair, copy the value to user land up to max_size bytes, and returns 0.
When multiple threads are waiting for the same key, and another thread adds a key-value pair for the key by calling kkv_put(), only one of the waiting threads will wake up and remove the value. The other threads will keep waiting. (Those other threads might have woken up, found that there is no key-value pair for the key, and gone back to sleep, but it all happens in the kernel; those threads do not return from kkv_get().)
Note that, when multiple threads are waiting on the same key, you should
NOT expect that each kkv_put()
will be matched by a waiting
thread. Recall that kkv_put()
will replace an existing key-value
pair. It is entirely possible that two consecutive kkv_put()
will
happen without an intervening removal of the key-value pair. Keep this
in mind when you write your test driver.
A thread blocked on kkv_get()
should return when it receives a signal.
kkv_get()
returns –1 with errno set to EINTR in that case.
signal_pending()
kernel function will come in handy here.The API descriptions for kkv_put()
, kkv_init()
and kkv_destroy()
remain the same. That is, they will behave exactly the same as in part 1.
However, you may need to change their implementations to support the new
behavior of kkv_get()
.
Here are some requirements and hints:
The new version of fridge_data_structures.h
for this part is shown
below. It is provided as skeleton code, along with some simple test
drivers. You must use the data structures as given. Please do not
modify them in any way.
#define HASH_TABLE_LENGTH 17
#define KKV_NONBLOCK 0
#define KKV_BLOCK 1
/*
* A key-value pair.
*/
struct kkv_pair {
char *value;
uint32_t key;
size_t size;
};
/*
* A node in a linked list. Contains a key-value pair.
*/
struct kkv_ht_entry {
struct list_head entries;
struct kkv_pair kv_pair;
wait_queue_head_t q;
uint32_t q_count;
};
/*
* A bucket in the hash table.
* The hash table is an array of HASH_TABLE_LENGTH buckets.
*/
struct kkv_ht_bucket {
spinlock_t lock;
struct list_head entries;
uint32_t count;
};
Do not lose the concurrency implemented in part 2. That is, a user program should be able to call the four system calls in any order without corrupting kernel data structures.
When kkv_get()
blocks waiting for a key-value pair, this is a slow
system call because the key-value pair may never come. Make sure that
you are not holding any lock when you go to sleep in kkv_get()
.
A new version of fridge.c
that passes the given test programs.
Your own test programs to further test your implementation.
The graders will use additional test programs. Test your system calls
as rigorously as you can!
The following info in your README.txt
:
Good luck!
The initial Fridge API and the test drivers are designed and implemented by the following TAs of COMS W4118 Operating Systems I, Spring 2015, Columbia University:
The Empty Fridge extension to the original Fridge API and the test drivers are written by the following TAs of COMS W4118 Operating Systems I, Spring 2016, Columbia University:
Last updated: 2017–03–09