-
Notifications
You must be signed in to change notification settings - Fork 93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implementation of a Irq_Mutex (see Issue#160) #162
Open
Shinribo
wants to merge
5
commits into
mvdnes:master
Choose a base branch
from
Shinribo:Implemented-IrqMutex
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3663aa9
Implemented IrqMutex
Juventer d58d055
Added irq_mutex feature flag to readme
Juventer 5562c8b
Changed IqrMutex implementation to be a wrapper around Mutex
Shinribo 6c3871b
Updated Coments, added a missing interrupt restore and made IrqMutex …
Shinribo a3b5daf
Moved the unsafe keyword and corresponding comment from new() to lock()
Shinribo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,291 @@ | ||
use critical_section::RestoreState; | ||
use critical_section::acquire; | ||
use critical_section::release; | ||
|
||
use core::{ | ||
fmt, | ||
ops::{Deref, DerefMut}, | ||
}; | ||
|
||
type InnerMutex<T> = crate::Mutex<T>; | ||
type InnerMutexGuard<'a, T> = crate::MutexGuard<'a, T>; | ||
|
||
/// A spin-based lock providing mutually exclusive access to data. | ||
/// | ||
/// The implementation uses either a ticket mutex or a regular spin mutex depending on whether the `spin_mutex` or | ||
/// `ticket_mutex` feature flag is enabled. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// use spin; | ||
/// | ||
/// let lock = spin::Mutex::new(0); | ||
/// | ||
/// // Modify the data | ||
/// *lock.lock() = 2; | ||
/// | ||
/// // Read the data | ||
/// let answer = *lock.lock(); | ||
/// assert_eq!(answer, 2); | ||
/// ``` | ||
/// | ||
/// # Thread safety example | ||
/// | ||
/// ``` | ||
/// use spin; | ||
/// use std::sync::{Arc, Barrier}; | ||
/// | ||
/// let thread_count = 1000; | ||
/// let spin_mutex = Arc::new(spin::Mutex::new(0)); | ||
/// | ||
/// // We use a barrier to ensure the readout happens after all writing | ||
/// let barrier = Arc::new(Barrier::new(thread_count + 1)); | ||
/// | ||
/// # let mut ts = Vec::new(); | ||
/// for _ in 0..thread_count { | ||
/// let my_barrier = barrier.clone(); | ||
/// let my_lock = spin_mutex.clone(); | ||
/// # let t = | ||
/// std::thread::spawn(move || { | ||
/// let mut guard = my_lock.lock(); | ||
/// *guard += 1; | ||
/// | ||
/// // Release the lock to prevent a deadlock | ||
/// drop(guard); | ||
/// my_barrier.wait(); | ||
/// }); | ||
/// # ts.push(t); | ||
/// } | ||
/// | ||
/// barrier.wait(); | ||
/// | ||
/// let answer = { *spin_mutex.lock() }; | ||
/// assert_eq!(answer, thread_count); | ||
/// | ||
/// # for t in ts { | ||
/// # t.join().unwrap(); | ||
/// # } | ||
/// ``` | ||
pub struct IrqMutex<T: ?Sized> { | ||
inner: InnerMutex<T> | ||
} | ||
|
||
/// A generic guard that will protect some data access and | ||
/// uses either a ticket lock or a normal spin mutex. | ||
/// | ||
/// For more info see [`TicketMutexGuard`] or [`SpinMutexGuard`]. | ||
/// | ||
/// [`TicketMutexGuard`]: ./struct.TicketMutexGuard.html | ||
/// [`SpinMutexGuard`]: ./struct.SpinMutexGuard.html | ||
pub struct IrqMutexGuard<'a, T: 'a + ?Sized> { | ||
inner: InnerMutexGuard<'a, T>, | ||
irq_state: RestoreState | ||
} | ||
|
||
impl<T> IrqMutex<T> { | ||
/// Creates a new [`Mutex`] wrapping the supplied data. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// use spin::Mutex; | ||
/// | ||
/// static MUTEX: Mutex<()> = Mutex::new(()); | ||
/// | ||
/// fn demo() { | ||
/// let lock = MUTEX.lock(); | ||
/// // do something with lock | ||
/// drop(lock); | ||
/// } | ||
/// | ||
/// | ||
/// | ||
/// ``` | ||
#[inline(always)] | ||
pub const fn new(value: T) -> Self { | ||
Self { | ||
inner: InnerMutex::new(value), | ||
} | ||
} | ||
|
||
/// Consumes this [`Mutex`] and unwraps the underlying data. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// let lock = spin::Mutex::new(42); | ||
/// assert_eq!(42, lock.into_inner()); | ||
/// ``` | ||
#[inline(always)] | ||
pub fn into_inner(self) -> T { | ||
self.inner.into_inner() | ||
} | ||
} | ||
|
||
impl<T: ?Sized> IrqMutex<T> { | ||
/// Locks the [`Mutex`] and returns a guard that permits access to the inner data. | ||
/// | ||
/// The returned value may be dereferenced for data access | ||
/// and the lock will be dropped when the guard falls out of scope. | ||
/// | ||
/// ``` | ||
/// let lock = spin::Mutex::new(0); | ||
/// { | ||
/// let mut data = lock.lock(); | ||
/// // The lock is now locked and the data can be accessed | ||
/// *data += 1; | ||
/// // The lock is implicitly dropped at the end of the scope | ||
/// } | ||
/// ``` | ||
/// UNSAFE: | ||
/// When IrqMutex's are nested, the innter IrqMutexGuard must not outlive the outer IrqMutexGuard, a violation leads to Undefinded Interrupt Behavior | ||
#[inline(always)] | ||
pub unsafe fn lock(&self) -> IrqMutexGuard<T> { | ||
let state = unsafe{acquire()}; | ||
IrqMutexGuard { | ||
inner: self.inner.lock(), | ||
irq_state: state | ||
} | ||
} | ||
} | ||
|
||
impl<T: ?Sized> IrqMutex<T> { | ||
/// Returns `true` if the lock is currently held. | ||
/// | ||
/// # Safety | ||
/// | ||
/// This function provides no synchronization guarantees and so its result should be considered 'out of date' | ||
/// the instant it is called. Do not use it for synchronization purposes. However, it may be useful as a heuristic. | ||
#[inline(always)] | ||
pub fn is_locked(&self) -> bool { | ||
self.inner.is_locked() | ||
} | ||
|
||
/// Force unlock this [`Mutex`]. | ||
/// | ||
/// # Safety | ||
/// | ||
/// This is *extremely* unsafe if the lock is not held by the current | ||
/// thread. However, this can be useful in some instances for exposing the | ||
/// lock to FFI that doesn't know how to deal with RAII. | ||
#[inline(always)] | ||
pub unsafe fn force_unlock(&self) { | ||
self.inner.force_unlock() | ||
} | ||
|
||
/// Try to lock this [`Mutex`], returning a lock guard if successful. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// let lock = spin::Mutex::new(42); | ||
/// | ||
/// let maybe_guard = lock.try_lock(); | ||
/// assert!(maybe_guard.is_some()); | ||
/// | ||
/// // `maybe_guard` is still held, so the second call fails | ||
/// let maybe_guard2 = lock.try_lock(); | ||
/// assert!(maybe_guard2.is_none()); | ||
/// ``` | ||
#[inline(always)] | ||
pub fn try_lock(&self) -> Option<IrqMutexGuard<T>> { | ||
let state = unsafe{acquire()}; | ||
let maybe_guard = self.inner | ||
.try_lock() | ||
.map(|guard| IrqMutexGuard { inner: guard, irq_state: state }); | ||
if maybe_guard.is_none() { | ||
unsafe{release(state)}; | ||
} | ||
maybe_guard | ||
} | ||
|
||
/// Returns a mutable reference to the underlying data. | ||
/// | ||
/// Since this call borrows the [`Mutex`] mutably, and a mutable reference is guaranteed to be exclusive in Rust, | ||
/// no actual locking needs to take place -- the mutable borrow statically guarantees no locks exist. As such, | ||
/// this is a 'zero-cost' operation. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// let mut lock = spin::Mutex::new(0); | ||
/// *lock.get_mut() = 10; | ||
/// assert_eq!(*lock.lock(), 10); | ||
/// ``` | ||
#[inline(always)] | ||
pub fn get_mut(&mut self) -> &mut T { | ||
self.inner.get_mut() | ||
} | ||
} | ||
|
||
impl<T: ?Sized + fmt::Debug> fmt::Debug for IrqMutex<T> { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
fmt::Debug::fmt(&self.inner, f) | ||
} | ||
} | ||
|
||
impl<T: ?Sized + Default> Default for IrqMutex<T> { | ||
fn default() -> Self { | ||
Self::new(Default::default()) | ||
} | ||
} | ||
|
||
impl<T> From<T> for IrqMutex<T> { | ||
fn from(data: T) -> Self { | ||
Self::new(data) | ||
} | ||
} | ||
|
||
impl<'a, T: ?Sized> IrqMutexGuard<'a, T> { | ||
/// Leak the lock guard, yielding a mutable reference to the underlying data. | ||
/// | ||
/// Note that this function will permanently lock the original [`Mutex`]. | ||
/// Restores the Interrupt State | ||
/// | ||
/// ``` | ||
/// let mylock = spin::Mutex::new(0); | ||
/// | ||
/// let data: &mut i32 = spin::MutexGuard::leak(mylock.lock()); | ||
/// | ||
/// *data = 1; | ||
/// assert_eq!(*data, 1); | ||
/// ``` | ||
#[inline(always)] | ||
pub fn leak(this: Self) -> &'a mut T { | ||
unsafe { release(this.irq_state) } | ||
InnerMutexGuard::leak(this.inner) | ||
} | ||
} | ||
|
||
impl<'a, T: ?Sized + fmt::Debug> fmt::Debug for IrqMutexGuard<'a, T> { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
fmt::Debug::fmt(&**self, f) | ||
} | ||
} | ||
|
||
impl<'a, T: ?Sized + fmt::Display> fmt::Display for IrqMutexGuard<'a, T> { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
fmt::Display::fmt(&**self, f) | ||
} | ||
} | ||
|
||
impl<'a, T: ?Sized> Deref for IrqMutexGuard<'a, T> { | ||
type Target = T; | ||
fn deref(&self) -> &T { | ||
&*self.inner | ||
} | ||
} | ||
|
||
impl<'a, T: ?Sized> DerefMut for IrqMutexGuard<'a, T> { | ||
fn deref_mut(&mut self) -> &mut T { | ||
&mut *self.inner | ||
} | ||
} | ||
|
||
impl<'a, T: ?Sized> Drop for IrqMutexGuard<'a, T> { | ||
/// The dropping of the IrqMutexGuard will release the lock it was created from and restore Interrupts to its former value | ||
fn drop(&mut self) { | ||
unsafe{release(self.irq_state)}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this violates the safety invariants of
critical-section
. From the docs:Right now, this API permits us to do the following:
It seems like the
Mutex
implementation here gets away with it by using a critical section token generated via a closure which is, to my knowledge, the only way to safely guarantee proper ordering in Rust.Initially I thought that an option might be to also store a counter along with the token and just panic if the token is out of order, but sadly that's unsound because you'd need to have some sort of global static counter, and there's no guarantee that the user isn't compiling several versions of
spin
into the same executable, resulting in multiple statics.Eurgh, this is indeed a tough one. My feeling is that we might need to do something a bit funky API-wise and have
lock
accept a closure, like so:It's a bit hairy, but at least it gets us safety back again, and it's probably not the worst crime imaginable.
Of course, there's another factor at play here: the
critical-section
's safety requirements are probably overzealous given that it's making the assumption that within a critical section you're going to run off and do some unsynchronised data access.The fact that we don't ever do that (because we're still using a mutex internally) means that - for practical cases - this is still perfectly safe provided you don't use
critical-section
elsewhere, polluting that state. This is one of those rare cases where, due to specific and non-local hardware behaviour, the safety of this abstraction does not compose, I think.I'm really in two minds on what to do here. Although I do think that
Mutex::lock(&self, impl Fn)
is a safe abstraction in isolation (as is the implementation you already have here, incidentally) the net effect is one of unsafety unless we are very careful to document exactly what the abstraction does to the global interrupt state and when.I'd be interested to know your thoughts!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the ordering requirement might be because you have to remember the original interrupt state. So releasing the first acquire first would immediately reenable interrupts, and the next release would disable them again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depending on the Critical-Section implementation its also possible that the second mutex doesnt change the interrupt state as it hasnt changed the interrupt state on entry. Nontheless the content of the second Mutex could be accessed without interrupt masking probably causing random UB.
The only solution i currently see is to make the IrqMutex unsafe and document that when nesting them the inner MutexGuard should never outlive the outer MutexGuard
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think only
lock
needs to beunsafe
, it would be possible to provide a safe closure-based API similar tocritical-section
. Arguably, this is the API thatstd::sync::Mutex
really should have had from the very start...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved the Keyword and corresponding Comment