mirror of
https://github.com/Mathewnd/Astral.git
synced 2026-10-27 12:48:22 +00:00
mm: vmmcache -> mm_cache
This commit is contained in:
@@ -0,0 +1,491 @@
|
||||
#include <kernel/mm.h>
|
||||
#include <kernel/vmm.h>
|
||||
#include <util.h>
|
||||
#include <logging.h>
|
||||
#include <kernel/timekeeper.h>
|
||||
#include <kernel/event.h>
|
||||
#include <kernel/init.h>
|
||||
|
||||
#define TABLE_SIZE 4096
|
||||
#define WRITER_TICK_SECONDS 15
|
||||
|
||||
static mutex_t mutex;
|
||||
static page_t **table;
|
||||
|
||||
static thread_t *writerthread;
|
||||
static semaphore_t sync;
|
||||
static eventheader_t syncevent;
|
||||
static eventheader_t pagereadyevent;
|
||||
size_t mm_cache_cached_pages;
|
||||
|
||||
#define HOLD_LOCK() \
|
||||
MUTEX_ACQUIRE(&mutex);
|
||||
|
||||
#define RELEASE_LOCK() \
|
||||
MUTEX_RELEASE(&mutex);
|
||||
|
||||
static inline uint64_t fnv1ahash(void *buffer, size_t size);
|
||||
static uintmax_t getentry(vnode_t *vnode, uintmax_t offset) {
|
||||
struct {
|
||||
vnode_t *vnode;
|
||||
uintmax_t offset;
|
||||
} tmp;
|
||||
|
||||
tmp.vnode = vnode;
|
||||
tmp.offset = offset;
|
||||
|
||||
return fnv1ahash(&tmp, sizeof(tmp)) % TABLE_SIZE;
|
||||
}
|
||||
|
||||
// assumes lock is held
|
||||
static page_t *findpage(vnode_t *vnode, uintmax_t offset) {
|
||||
uintmax_t entry = getentry(vnode, offset);
|
||||
|
||||
page_t *page = table[entry];
|
||||
while (page) {
|
||||
if (page->backing == vnode && page->offset == offset)
|
||||
break;
|
||||
page = page->hash_next;
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
// assumes lock is held
|
||||
static void putpage(page_t *page) {
|
||||
uintmax_t entry = getentry(page->backing, page->offset);
|
||||
// add to table list
|
||||
page->hash_prev = NULL;
|
||||
page->hash_next = table[entry];
|
||||
if (table[entry])
|
||||
table[entry]->hash_prev = page;
|
||||
|
||||
table[entry] = page;
|
||||
|
||||
// add to vnode list
|
||||
// XXX this could be a bit slow if a large portion of the file is in memory and it gets purged
|
||||
page->vnode_prev = NULL;
|
||||
page->vnode_next = page->backing->pages;
|
||||
if (page->backing->pages)
|
||||
page->backing->pages->vnode_prev = page;
|
||||
|
||||
page->backing->pages = page;
|
||||
++mm_cache_cached_pages;
|
||||
}
|
||||
|
||||
// assumes lock is held
|
||||
static void removepage(page_t *page) {
|
||||
uintmax_t entry = getentry(page->backing, page->offset);
|
||||
|
||||
// remove from table list
|
||||
if (page->hash_next)
|
||||
page->hash_next->hash_prev = page->hash_prev;
|
||||
|
||||
if (page->hash_prev)
|
||||
page->hash_prev->hash_next = page->hash_next;
|
||||
else
|
||||
table[entry] = page->hash_next;
|
||||
|
||||
page->hash_next = NULL;
|
||||
page->hash_prev = NULL;
|
||||
|
||||
// remove from vnode list
|
||||
if (page->vnode_next)
|
||||
page->vnode_next->vnode_prev = page->vnode_prev;
|
||||
|
||||
if (page->vnode_prev)
|
||||
page->vnode_prev->vnode_next = page->vnode_next;
|
||||
else
|
||||
page->backing->pages = page->vnode_next;
|
||||
|
||||
page->vnode_next = NULL;
|
||||
page->vnode_prev = NULL;
|
||||
--mm_cache_cached_pages;
|
||||
}
|
||||
|
||||
int mm_cache_get_page(vnode_t *vnode, uintmax_t offset, page_t **res) {
|
||||
__assert(vnode->type == V_TYPE_REGULAR || vnode->type == V_TYPE_BLKDEV);
|
||||
__assert((offset % PAGE_SIZE) == 0);
|
||||
retry_err:
|
||||
HOLD_LOCK();
|
||||
|
||||
page_t *newpage = NULL;
|
||||
volatile page_t *page = findpage(vnode, offset);
|
||||
retry:
|
||||
if (page) {
|
||||
// page is present in the page cache
|
||||
mm_hold_page(mm_get_page_address((page_t *)page));
|
||||
RELEASE_LOCK();
|
||||
|
||||
// in the case of a retry, release the allocated page here
|
||||
if (newpage)
|
||||
mm_release_page(mm_get_page_address(newpage));
|
||||
|
||||
eventlistener_t listener;
|
||||
EVENT_INITLISTENER(&listener);
|
||||
EVENT_ATTACH(&listener, &pagereadyevent);
|
||||
|
||||
// wait for page to be ready
|
||||
while ((page->flags & (PAGE_FLAGS_READY | PAGE_FLAGS_ERROR)) == 0)
|
||||
EVENT_WAIT(&listener, 0);
|
||||
|
||||
EVENT_DETACHALL(&listener);
|
||||
|
||||
if (page->flags & PAGE_FLAGS_ERROR) {
|
||||
// the thread handling the page in failed to read it, we should retry it and see whats up
|
||||
mm_release_page(mm_get_page_address((page_t *)page));
|
||||
goto retry_err;
|
||||
}
|
||||
|
||||
*res = (page_t *)page;
|
||||
} else {
|
||||
// page is not present in the cache, we will have to load it in
|
||||
RELEASE_LOCK();
|
||||
|
||||
void *address = mm_alloc_page(MEMORY_SECTION_DEFAULT);
|
||||
if (address == NULL)
|
||||
return ENOMEM;
|
||||
|
||||
newpage = mm_get_page(address);
|
||||
|
||||
HOLD_LOCK();
|
||||
|
||||
// while the lock wasn't being held, the page could have potentially been added to the cache
|
||||
// check for it again and return from the function as if it was always there in the first place
|
||||
page = findpage(vnode, offset);
|
||||
if (page)
|
||||
goto retry;
|
||||
|
||||
newpage->backing = vnode;
|
||||
newpage->offset = offset;
|
||||
|
||||
// add it to the page cache
|
||||
putpage(newpage);
|
||||
|
||||
RELEASE_LOCK();
|
||||
|
||||
VOP_LOCK(vnode);
|
||||
int error = VOP_GETPAGE(vnode, offset, newpage);
|
||||
VOP_UNLOCK(vnode);
|
||||
|
||||
if (error) {
|
||||
// an error happened with GETPAGE, remove the page from the cache,
|
||||
// tell the sleeping threads that something happened and free the page
|
||||
// by setting backing to null so it gets treated as an anonymous page again
|
||||
HOLD_LOCK();
|
||||
removepage(newpage);
|
||||
|
||||
newpage->flags |= PAGE_FLAGS_ERROR;
|
||||
newpage->backing = NULL;
|
||||
newpage->offset = 0;
|
||||
|
||||
RELEASE_LOCK();
|
||||
mm_release_page(mm_get_page_address(newpage));
|
||||
EVENT_SIGNAL(&pagereadyevent);
|
||||
return error;
|
||||
}
|
||||
|
||||
HOLD_LOCK();
|
||||
newpage->flags |= PAGE_FLAGS_READY;
|
||||
RELEASE_LOCK();
|
||||
|
||||
EVENT_SIGNAL(&pagereadyevent);
|
||||
*res = newpage;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// adds a page to the cache in a specific offset if its not already there
|
||||
int mm_cache_push_page(vnode_t *vnode, uintmax_t offset, page_t *page) {
|
||||
__assert((offset % PAGE_SIZE) == 0);
|
||||
HOLD_LOCK();
|
||||
|
||||
page_t *pagetest = findpage(vnode, offset);
|
||||
if (pagetest) {
|
||||
RELEASE_LOCK();
|
||||
return EAGAIN;
|
||||
}
|
||||
|
||||
page->backing = vnode;
|
||||
page->offset = offset;
|
||||
page->flags |= PAGE_FLAGS_READY;
|
||||
|
||||
putpage(page);
|
||||
|
||||
RELEASE_LOCK();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// removes a page from the cache AND turns it into anonymous memory
|
||||
int mm_cache_evict(page_t *page) {
|
||||
HOLD_LOCK();
|
||||
if (page->refcount > 1) {
|
||||
RELEASE_LOCK();
|
||||
return EAGAIN;
|
||||
}
|
||||
__assert(page->refcount == 1);
|
||||
__assert((page->flags & PAGE_FLAGS_DIRTY) == 0);
|
||||
|
||||
if ((page->flags & PAGE_FLAGS_TRUNCATED) == 0) {
|
||||
// the page needs to be removed from the cache to continue
|
||||
removepage(page);
|
||||
}
|
||||
|
||||
page->flags = 0;
|
||||
page->backing = NULL;
|
||||
page->offset = 0;
|
||||
|
||||
RELEASE_LOCK();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// removes a page from the cache *but doesn't do anything to it*
|
||||
int mm_cache_take_page(page_t *page) {
|
||||
HOLD_LOCK();
|
||||
// someone called mm_cache_get_page() and got this page while the lock wasn't held
|
||||
// return an error status to the caller
|
||||
if (page->refcount > 1) {
|
||||
RELEASE_LOCK();
|
||||
return EAGAIN;
|
||||
}
|
||||
|
||||
__assert(page->refcount == 1);
|
||||
__assert((page->flags & PAGE_FLAGS_DIRTY) == 0);
|
||||
|
||||
// XXX maybe just not allow truncated pages to take up space like this?
|
||||
if ((page->flags & PAGE_FLAGS_TRUNCATED) == 0) {
|
||||
// the page needs to be removed from the cache to continue
|
||||
removepage(page);
|
||||
}
|
||||
|
||||
RELEASE_LOCK();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mm_cache_truncate(vnode_t *vnode, uintmax_t offset) {
|
||||
HOLD_LOCK();
|
||||
page_t *pagelist = NULL;
|
||||
page_t *page = vnode->pages;
|
||||
|
||||
while (page) {
|
||||
page_t *oldpage = page;
|
||||
page = page->vnode_next;
|
||||
|
||||
// only truncate past a certain offset
|
||||
if (oldpage->offset < offset)
|
||||
continue;
|
||||
|
||||
oldpage->flags |= PAGE_FLAGS_TRUNCATED;
|
||||
removepage(oldpage);
|
||||
oldpage->vnode_next = pagelist;
|
||||
pagelist = oldpage;
|
||||
}
|
||||
|
||||
RELEASE_LOCK();
|
||||
|
||||
// make sure to unref if they are pinned
|
||||
while (pagelist) {
|
||||
page_t *page = pagelist;
|
||||
pagelist = pagelist->vnode_next;
|
||||
if (page->flags & PAGE_FLAGS_PINNED)
|
||||
mm_release_page(mm_get_page_address(page));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// called with lock held
|
||||
// returns with lock released
|
||||
// expects backing lock to be held
|
||||
static int syncpage(page_t *page, bool backinglock) {
|
||||
__assert(page->flags & PAGE_FLAGS_DIRTY);
|
||||
page->flags &= ~PAGE_FLAGS_DIRTY;
|
||||
RELEASE_LOCK();
|
||||
int e = 0;
|
||||
if ((page->flags & PAGE_FLAGS_TRUNCATED) == 0) {
|
||||
if (backinglock)
|
||||
VOP_LOCK(page->backing);
|
||||
|
||||
e = VOP_PUTPAGE(page->backing, page->offset, (page_t *)page);
|
||||
|
||||
if (backinglock)
|
||||
VOP_UNLOCK(page->backing);
|
||||
VOP_RELEASE(page->backing);
|
||||
} else {
|
||||
// page got truncated from the file while waiting to be written to disk
|
||||
// its still holding a reference to the vnode, so release that
|
||||
VOP_RELEASE(page->backing);
|
||||
}
|
||||
|
||||
mm_release_page(mm_get_page_address((page_t *)page));
|
||||
return e;
|
||||
}
|
||||
|
||||
static page_t *dirtylist;
|
||||
static page_t *dirtylistend;
|
||||
|
||||
// expects vnode to be held
|
||||
int mm_cache_sync_vnode(vnode_t *vnode, uintmax_t offset, size_t size) {
|
||||
offset = ROUND_DOWN(offset, PAGE_SIZE);
|
||||
uintmax_t top = offset + size;
|
||||
// overflow check
|
||||
__assert(top > offset);
|
||||
HOLD_LOCK();
|
||||
|
||||
// loop through all vnode pages in memory and check which ones are in the range and are dirty
|
||||
// TODO create a proper vnode dirty list as to not have to loop through the ENTIRE thing in memory
|
||||
page_t *page = vnode->pages;
|
||||
page_t *vnodedirtylist = NULL;
|
||||
for (; page; page = page->vnode_next) {
|
||||
if (page->offset < offset || page->offset >= top || (page->flags & PAGE_FLAGS_DIRTY) == 0 || (page->flags & PAGE_FLAGS_VNODE_SYNCING))
|
||||
continue;
|
||||
|
||||
// remove from write list and add to an internal list using the write pointers
|
||||
// in a singly linked list way
|
||||
if (page->write_next)
|
||||
page->write_next->write_prev = page->write_prev;
|
||||
else
|
||||
dirtylistend = page->write_prev;
|
||||
|
||||
if (page->write_prev)
|
||||
page->write_prev->write_next = page->write_next;
|
||||
else
|
||||
dirtylist = page->write_next;
|
||||
|
||||
page->write_next = vnodedirtylist;
|
||||
page->write_prev = NULL;
|
||||
page->flags |= PAGE_FLAGS_VNODE_SYNCING;
|
||||
vnodedirtylist = page;
|
||||
}
|
||||
|
||||
RELEASE_LOCK();
|
||||
|
||||
int e = 0;
|
||||
while (vnodedirtylist) {
|
||||
// in the case of failure, only the first error to occur will be reported and we will not
|
||||
// retry the write and keep on syncing the pages to disk
|
||||
HOLD_LOCK();
|
||||
page_t *page = vnodedirtylist;
|
||||
vnodedirtylist = vnodedirtylist->write_next;
|
||||
page->write_next = NULL;
|
||||
page->flags &= ~PAGE_FLAGS_VNODE_SYNCING;
|
||||
|
||||
// another thread could already have synced this page, verify if it is still dirty
|
||||
if (page->flags & PAGE_FLAGS_DIRTY) {
|
||||
int error = syncpage(page, false);
|
||||
|
||||
if (e == 0)
|
||||
e = error;
|
||||
} else {
|
||||
RELEASE_LOCK();
|
||||
}
|
||||
// syncpage returns with lock released
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
int mm_cache_sync(void) {
|
||||
eventlistener_t eventlistener;
|
||||
EVENT_INITLISTENER(&eventlistener);
|
||||
HOLD_LOCK();
|
||||
if (dirtylist == NULL) {
|
||||
// no dirty pages
|
||||
RELEASE_LOCK();
|
||||
return 0;
|
||||
}
|
||||
|
||||
EVENT_ATTACH(&eventlistener, &syncevent);
|
||||
semaphore_signal(&sync);
|
||||
RELEASE_LOCK();
|
||||
|
||||
EVENT_WAIT(&eventlistener, 0);
|
||||
|
||||
EVENT_DETACHALL(&eventlistener);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// backing expected locked
|
||||
int mm_cache_make_dirty(page_t *page) {
|
||||
bool madedirty = false;
|
||||
HOLD_LOCK();
|
||||
|
||||
if ((page->flags & (PAGE_FLAGS_DIRTY | PAGE_FLAGS_TRUNCATED)) == 0) {
|
||||
madedirty = true;
|
||||
// page is neither dirty nor truncated, add to dirty list and hold the page and vnode
|
||||
page->flags |= PAGE_FLAGS_DIRTY;
|
||||
|
||||
if ((page->flags & PAGE_FLAGS_VNODE_SYNCING) == 0) {
|
||||
page->write_prev = NULL;
|
||||
page->write_next = dirtylist;
|
||||
if (dirtylist)
|
||||
dirtylist->write_prev = page;
|
||||
else
|
||||
dirtylistend = page;
|
||||
|
||||
dirtylist = page;
|
||||
}
|
||||
|
||||
mm_hold_page(mm_get_page_address(page));
|
||||
__assert(page->backing);
|
||||
VOP_HOLD(page->backing);
|
||||
}
|
||||
|
||||
RELEASE_LOCK();
|
||||
if (madedirty) {
|
||||
vattr_t attr;
|
||||
attr.mtime = timekeeper_time();
|
||||
VOP_SETATTR(page->backing, &attr, V_ATTR_MTIME, NULL);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void tick(context_t *, dpcarg_t arg) {
|
||||
semaphore_signal(&sync);
|
||||
}
|
||||
|
||||
static void writer() {
|
||||
timerentry_t timerentry;
|
||||
// this will be inserted on some random cpu's timer, but it will always work after that
|
||||
interrupt_set(false);
|
||||
timer_insert(current_cpu()->timer, &timerentry, tick, NULL, (uintmax_t)WRITER_TICK_SECONDS * 1000000, true);
|
||||
interrupt_set(true);
|
||||
for (;;) {
|
||||
HOLD_LOCK();
|
||||
volatile page_t *page = dirtylistend;
|
||||
if (page == NULL) {
|
||||
EVENT_SIGNAL(&syncevent);
|
||||
RELEASE_LOCK();
|
||||
semaphore_wait(&sync, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
dirtylistend = page->write_prev;
|
||||
if (dirtylistend)
|
||||
dirtylistend->write_next = NULL;
|
||||
else
|
||||
dirtylist = NULL;
|
||||
|
||||
page->write_prev = NULL;
|
||||
// TODO notify error on mm_cache_sync_vnode
|
||||
syncpage((page_t *)page, true);
|
||||
}
|
||||
}
|
||||
|
||||
void mm_cache_init(void) {
|
||||
MUTEX_INIT(&mutex);
|
||||
table = vmm_map(NULL, TABLE_SIZE * sizeof(page_t *), VMM_FLAGS_ALLOCATE, ARCH_MMU_FLAGS_READ | ARCH_MMU_FLAGS_WRITE | ARCH_MMU_FLAGS_NOEXEC, NULL);
|
||||
__assert(table);
|
||||
memset(table, 0, TABLE_SIZE * sizeof(page_t *));
|
||||
|
||||
SEMAPHORE_INIT(&sync, 0);
|
||||
writerthread = sched_newthread(writer, PAGE_SIZE * 16, 1, NULL, NULL);
|
||||
__assert(writerthread);
|
||||
sched_queue(writerthread);
|
||||
mm_cache_sync();
|
||||
EVENT_INITHEADER(&syncevent);
|
||||
EVENT_INITHEADER(&pagereadyevent);
|
||||
}
|
||||
|
||||
INIT_ROUTINE_DEFINE(mm_cache, INIT_ROUTINE_FLAGS_NONE, mm_cache_init, scheduler);
|
||||
Reference in New Issue
Block a user