You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

147 lines
4.9 KiB
C

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* Copyright (C) 2021,2022 fef <owo@fef.moe>. All rights reserved. */
/*
* This is an implementation of the nonblocking queue algorithm presented in:
* M. M. Michael and M. L. Scott, "Nonblocking Algorithms and Preemption-Safe
* Locking on Multiprogrammed Shared Memory Multiprocessors,"
* Journal of Parallel and Distributed Computing, vol 51, no 1, pp 126, 1998.
*
* <https://doi.org/10.1006/jpdc.1998.1446>
* <https://www.cs.rochester.edu/u/scott/papers/1998_JPDC_nonblocking.pdf>
*/
#include <arch/atom.h>
#include <arch/cpufunc.h>
#include <gay/kqueue.h>
#include <gay/mm.h>
#include <gay/types.h>
#include <gay/util.h>
#if __SIZEOF_POINTER__ == 4
#define VERSION_BITS 2
#elif __SIZEOF_POINTER__ == 8
#define VERSION_BITS 3
#else
#error "Unsupported pointer size"
#endif
/**
* We use the least significant bits of pointers as a "version number" in order
* to (kind of) solve the ABA problem while still being able to write out both
* version and pointer in one atomic instruction.
* It is safe to use these bits because both the stack and memory returned by
* `kmalloc()` are always aligned to least a full pointer size, and structure
* members always get aligned to their own size as well. So, on 32-bit we
* have 2 bits and on 64-bit we have 3 bits that are known to always be zero.
*
* This is still going to break if exactly `n * (1 << VERSION_BITS)` items are
* added/removed consecutively between our own read/exchange cycle, *and* the
* last item has the same address as the original one. I assume it's *very*
* unlikely this is ever gonna happen though, since it would already be unusual
* for the same item to be enqueued more than once at all.
*/
union verptr {
unsigned ver:VERSION_BITS;
struct kq_node *ptr;
};
/** Get the actual pointer value, without the version number. */
#define VERPTR(verptr) align_floor((verptr).ptr, 1 << VERSION_BITS)
struct kqueue *kq_create(enum mflags flags)
{
struct kqueue *kq = kmalloc(sizeof(*kq), flags);
if (kq != nil)
kq_init(kq);
return kq;
}
void kq_add(struct kqueue *kq, struct kq_node *_node)
{
patom_init(&_node->next, nil);
union verptr node, tail, next;
node.ptr = _node;
spin_loop {
tail.ptr = patom_read(&kq->tail);
/* there is always at least one entry in the queue (the dummy),
* so we know for sure that the tail can never be nil */
next.ptr = patom_read(&VERPTR(tail)->next);
/* If the head has changed during the time we read the other pointer,
* it means another thread has added an item. The tail and next
* pointers are inconsistent in that case, so we have to try again. */
if (tail.ptr == patom_read(&kq->tail)) {
/* Check if tail really points to the last node */
if (VERPTR(next) == nil) {
/* tail is up-to-date, try appending the new node */
node.ver = next.ver + 1u;
void *tmp = patom_cmp_xchg(&VERPTR(tail)->next, next.ptr, node.ptr);
if (tmp == next.ptr)
break;
} else {
/* tail is falling behind, try to make it catch up
* (this loops until it points to the actual end) */
next.ver = tail.ver + 1u;
patom_cmp_xchg(&kq->tail, tail.ptr, next.ptr);
}
}
}
/*
* When we reach this, the new node has been inserted into the actual
* linked list. What's left is to update the tail pointer, which we
* optimistically try once. If that fails, we take care of the mess
* next time an item is added or removed from the queue, because it's
* not *critical* that the tail always stays consistent. It's only
* there so we don't need to iterate over the entire list whenever we
* insert something.
*/
node.ver = tail.ver + 1u;
patom_cmp_xchg(&kq->tail, tail.ptr, node.ptr);
}
struct kq_node *kq_del(struct kqueue *kq)
{
union verptr head, tail, next, ret;
spin_loop {
head.ptr = patom_read(&kq->head);
tail.ptr = patom_read(&kq->tail);
/* there is always at least one entry in the queue (the dummy),
* so we know for sure that the head can never be nil */
next.ptr = patom_read(&VERPTR(head)->next);
/* If the head has changed during the time we read the other two pointers,
* it means another thread has removed an item. The head, tail, and next
* pointers are inconsistent in that case, so we have to try again. */
if (head.ptr == patom_read(&kq->head)) {
/* Check if the queue is empty or if the tail is falling behind,
* the latter happens if the last kq_add() failed to update it */
if (head.ptr == tail.ptr) {
/* Check if the queue is empty */
if (VERPTR(next) == nil) {
/* it is, so we are done here */
ret.ptr = nil;
break;
} else {
/* it is not, so try to update the tail */
next.ver = tail.ver + 1u;
patom_cmp_xchg(&kq->tail, tail.ptr, next.ptr);
}
} else {
/* Queue is not empty and the tail is consistent,
* that means we can try removing the first node */
ret.ptr = next.ptr;
next.ver = head.ver + 1u;
void *tmp = patom_cmp_xchg(&kq->head, head.ptr, next.ptr);
if (tmp == head.ptr)
break;
}
}
}
return VERPTR(ret);
}