Controller hardware reset#268
Conversation
e5dd689 to
d7f95a0
Compare
snue
left a comment
There was a problem hiding this comment.
Really cool! I suspect you have tested that the controller reset works now when the guest reloads the xhci driver? This would make for a good integration test, or maybe addition to an existing test.
I left some comments. Looks pretty good already 👍
It is also broken in that state 😉 Thanks for fixing this 🎉 |
d7f95a0 to
61df7dd
Compare
snue
left a comment
There was a problem hiding this comment.
Pretty nice now. Functionally this seems sufficient, but I did not test it yet. I left some more suggestions, and I'd like to have a test (unit or integration or both) before merge.
The event worker used to wait for ERSTBA once, configure the event ring, and then keep using that configuration forever. After a host controller reset, the guest driver writes ERSTBA/ERSTSZ/ERDP again. The worker must stop using the old event ring state and wait for the new configuration. Split the worker loop into an unconfigured phase and a configured phase. On Reset, clear the EventRing state and return to waiting for ERSTBA.
a083bba to
1167813
Compare
lbeierlieb
left a comment
There was a problem hiding this comment.
Thanks for the PR! I tested the feature with rmmod xhci_pci; modprobe xhci_pci and the controller came back to life successfully. The implement logic looks alright, but I do not fully agree with which logic is implemented where. I have to checked all commits yet.
| } | ||
|
|
||
| pub fn reset(&self) -> anyhow::Result<()> { | ||
| self.running.store(false, Ordering::Relaxed); |
There was a problem hiding this comment.
The worker sets running to false when processing the message, so this assignment seems superfluous. This change also improves separation of concerns--pub fn reset() is only responsible for relaying the reset request to the worker, and the worker implements the logic.
| WorkerMessage::Stop => { | ||
| self.state = WorkerState::Stopping; | ||
| break; | ||
| } |
There was a problem hiding this comment.
Not directly related to the PR, but we should probably also set the commandring_running to false when stopping.
"It [the CRR bit] is cleared to ‘0’ when the Command Ring is “stopped” after writing a ‘1’
to the Command Stop (CS) or Command Abort (CA) flags, or if the R/S bit is cleared to ‘0’" (XHCI spec)
There was a problem hiding this comment.
Sorry, please remove again 🙈 we set the state to Stopping, in which we set CRR to false and then transition to Stopped.
| self.interrupt_line = interrupt_line; | ||
| debug!("Updated interrupt line"); | ||
| } | ||
| async fn process_configured_message(&mut self) -> anyhow::Result<bool> { |
There was a problem hiding this comment.
I would appreciate a short doc comment about the bool return value.
| self.interrupt_line = interrupt_line; | ||
| debug!("Updated interrupt line"); | ||
| } | ||
| async fn process_configured_message(&mut self) -> anyhow::Result<bool> { |
There was a problem hiding this comment.
process_configured_message sounds like the method processes a configured message. However, the method is the method for processing messages while the event ring is configured.
There was a problem hiding this comment.
Right. Maybe just process_message()? And document the fact that this is called in configured state in the comment together with explaining the return value?
There was a problem hiding this comment.
on second thought: Would it not be more straightforward to loop in process_messages/run_configured, with a break when we detect a reset message? Getting rid of the bool return value and the while loop in the run_loop.
| if value & usbcmd::HCRST == usbcmd::HCRST { | ||
| info!("Host Controller Reset requested"); | ||
| self.command_ring | ||
| .reset() | ||
| .map_err(|err| error!("failed to reset command ring: {err}")) | ||
| .ok(); | ||
| self.interrupter | ||
| .reset() | ||
| .map_err(|err| error!("failed to reset event ring: {err}")) | ||
| .ok(); | ||
| self.slot_manager | ||
| .reset() | ||
| .map_err(|err| error!("failed to reset slots: {err}")) | ||
| .ok(); | ||
| } |
There was a problem hiding this comment.
The XhciController is supposed to only connect the components, not implement any logic itself. The usbcmd.write should recognize the reset bit and then either send instruct command ring, interrupter, and slot manager to reset itself, or we introduce a small dedicated reset component, which the usbcmd knows and which is responsible for informing all relevant other components.
There was a problem hiding this comment.
Yes, this sounds like a clean design, but also a bit overengineered IMHO. So far I am fine with the controller implementing the logic that concerns orchestrating multiple components and leaving any register access side effects and everything that is internal to a component to these. Our register abstractions cannot really deal with causing side effects currently, and I would like to keep it that way for now.
There was a problem hiding this comment.
We have
#[derive(Debug, Default, Clone)]
pub struct ErstbaRegister {
value: Arc<AtomicU64>,
notify: Arc<Notify>,
}
impl ErstbaRegister {
pub fn read(&self) -> u64 {
self.value.load(Ordering::Relaxed)
}
pub fn erstba(&self) -> u64 {
self.value.load(Ordering::Relaxed) & !0x1f
}
pub fn write(&self, new_value: u64) {
self.value.store(new_value, Ordering::Relaxed);
self.notify.notify_waiters();
}
...
so that the interrupter can notice writes.
A reset component could listen the same way for usbcmd writes. The majority of notifications would not result in an action, but the total number of usbcmd writes is low, so overall no performance issue.
I would really like to keep any logic out of the XhciController. Controller reset is probably the case that would be the most acceptable to handle directly, but I fear it's a slippery slope and will cause more logic creeping into the top-level controller.
| offset::USBCMD => self.usbcmd.write(value), | ||
| offset::USBCMD => { | ||
| self.usbcmd.write(value); | ||
| if value & usbcmd::HCRST == usbcmd::HCRST { |
There was a problem hiding this comment.
We recognize that the bit is set here, but I did not see setting the HCRST bit to fulfill the spec's HCRST description: "This bit is cleared to ‘0’ by the Host Controller when the reset process is complete".
Did I miss the HCRST zero write?
Even then, the timing must be off. We should wait until all components that we instruct to reset report that they have finished resetting (might take a while, we are passing messages around), and then update HCRST to zero.
There was a problem hiding this comment.
We only check the written value and never actually store the bit, as the guest has no guarantee it will be able to see the bit flip (as far as I interpret the specification). When this was a noop, we were just "instantly done". We should probably document this behavior here, but I would like to see a stress test hitting register writes when we are not ready for them before actually implementing the observable set/clear on this bit.
There was a problem hiding this comment.
If we want to make this bit observable, the special Reset component owned by usbcmd you proposed above suddenly becomes a more appealing option 😉
There was a problem hiding this comment.
suggestion:
usbcmd::writenotices HCRST set, notifies reset component;writealso preserves HCRST- reset component triggers reset in other components
- reset component waits for all components to report that they finished the reset operation
- reset component sets HCRST bit to zero
There was a problem hiding this comment.
* `usbcmd::write` notices HCRST set, notifies reset component; `write` also preserves HCRST
Yes.
* reset component triggers reset in other components
Through shared references to the relevant components I presume? Or just message passing?
* reset component waits for all components to report that they finished the reset operation
Yes. Just await a bunch of Futures with the reset component being a separate task. Do we need extra cleanup for it?
* reset component sets HCRST bit to zero
It also has access to the Arc<AtomicU64> value of usbcmd for that and take care only to handle HCRST? Or do we go for separate AtomicBool and assemble them in usbcmd? We could revert the recent refactor of the running bit from bool, as we have different "audiences" for these bits.
There was a problem hiding this comment.
Through shared references to the relevant components I presume? Or just message passing?
The same way as currently the XhciController does it. All components are split in split in half--the "front end" is what the controller stores, storing atomics and message senders, the "back end" is a worker that owns state and typically processes messages coming from the front end. The reset component could take the front ends directly and call reset on them, which will send reset messages to the back-end worker. Alternatively, the front ends offer a create_reset_sender function, a thin wrapper around the sender to the respective back end.
Yes. Just
awaita bunch of Futures with the reset component being a separate task. Do we need extra cleanup for it?
cleanup for what? 🙈
It also has access to the
Arc<AtomicU64>value ofusbcmdfor that and take care only to handleHCRST? Or do we go for separateAtomicBooland assemble them inusbcmd? We could revert the recent refactor of therunningbit frombool, as we have different "audiences" for these bits.
Either direct access to the Arc<AtomicU32> with fetch_and or implement UsbcmdRegister::unsetHcrst and share the UsbcmdRegister between controller and reset component with an Arc.
There was a problem hiding this comment.
cleanup for what? 🙈
All good 😁 the design checks out. Just a fetch_and on the Atomic sounds right as well.
There was a problem hiding this comment.
I drafted a bit.
trait ResetSender {
fn send_reset(&self, completion_notifier: oneshot::Sender<()>) -> anyhow::Result<()>;
fn reset(&self) -> oneshot::Receiver<()> {
let (send, recv) = oneshot::channel();
self.send_reset(send);
recv
}
}
Each resetable component has to implement a sender and a function that returns such a sender. Command-ring example:
struct CommandRingResetSender {
sender_to_worker: mpsc::UnboundedSender<WorkerMessage>,
}
impl ResetSender for CommandRingResetSender {
fn send_reset(&self, completion_notifier: oneshot::Sender<()>) -> anyhow::Result<()> {
self.sender_to_worker.send(WorkerMessage::Reset(completion_notifier)).map_err(|e| e.into())
}
}
impl CommandRing {
pub fn reset_sender(&self) -> CommandRingResetSender {
CommandRingResetSender {
sender_to_worker: self.sender_to_worker.clone(),
}
}
}
The reset component:
struct ResetCoordinator<const N: usize> {
hcrst_notify: Notify,
reset_senders: [Box<dyn ResetSender>; N],
usbcmd: Arc<AtomicU32>,
}
impl<const N: usize> ResetCoordinator<N> {
async fn run_loop(&self) -> anyhow::Result<()> {
loop {
self.hcrst_notify.notified().await;
for reset_sender in &self.reset_senders {
reset_sender.reset().await?;
}
self.usbcmd
.fetch_and(!(usbcmd::HCRST as u32), Ordering::Relaxed);
}
}
}
There was a problem hiding this comment.
Ok, that's a nice separation of concerns. And the only reason for the reset().await to fail is when a channel is closed, in which case we don't need to bother sending the other resets or ever clearing the bit?
There was a problem hiding this comment.
yep, exactly; reset() return value is directly the oneshot::Receiver, whose Future returns RecvError when the sender is dropped.
USBCMD no longer needs to log HCRST itself. The reset side effects are handled in the xHCI MMIO write path, so log the reset request there instead.
813c2f9 to
f96fc07
Compare
clean the extra running setting
set the commandring_running to false when stopping
Move configured-state message processing into run_configured() and break on reset instead of returning a bool to control the outer loop.
Rework host controller reset based on the PR design discussion. Previously, XhciController detected USBCMD.HCRST and directly reset the command ring, interrupter, and slot manager. Now USBCMD preserves HCRST and notifies a ResetCoordinator, which resets components through thin ResetSender handles and waits for completion before clearing HCRST. This keeps reset orchestration out of the top-level controller, gives each component ownership of its reset side effects, and makes HCRST observable until the reset process has completed.
lbeierlieb
left a comment
There was a problem hiding this comment.
Thank you for integrating the suggested changes! We are getting there, but there is some cleanup necessary to properly divide responsibilities.
| WorkerMessage::Stop => { | ||
| self.state = WorkerState::Stopping; | ||
| break; | ||
| } |
There was a problem hiding this comment.
Sorry, please remove again 🙈 we set the state to Stopping, in which we set CRR to false and then transition to Stopped.
| use std::sync::Arc; | ||
|
|
||
| #[derive(Debug)] | ||
| #[derive(Debug, Clone)] |
There was a problem hiding this comment.
Why this change? You never use the clone functionality, the code works perfectly without Clone.
| msg_sender: mpsc::UnboundedSender<InterrupterMessage>, | ||
| } | ||
|
|
||
| #[derive(Debug, Clone)] |
There was a problem hiding this comment.
The idea is to create an InterrupterResetSender once and give it to the ResetCoordinator. There is no need for cloning intended, so I would rather not remove the Clone from here.
| self.registers.interrupt_management.write(0); | ||
| self.registers | ||
| .interrupt_moderation_interval | ||
| .write(IMOD_DEFAULT); | ||
| self.registers.erst_base_address.reset(); | ||
| self.registers.erst_size.write(0); | ||
| self.registers.eventring_dequeue_pointer.write(0); |
There was a problem hiding this comment.
The interrupter worker should execute all this code once it receives the message. send_reset should really only do sending the reset message to the worker.
| InterrupterMessage::Reset => self.event_ring.reset(), | ||
| InterrupterMessage::Reset(completion) => { | ||
| self.event_ring.reset(); | ||
| completion.send(()).ok(); |
There was a problem hiding this comment.
Please use completion.send_anyhow(())?; (needs use crate::oneshot_anyhow::SendWithAnyhowError;)
| #[derive(Debug, Clone)] | ||
| pub struct UsbcmdRegister { | ||
| value: Arc<AtomicU32>, | ||
| notify: Arc<Notify>, |
There was a problem hiding this comment.
This notify is used only for reset notify, so reset_notify or similar would be a better name, then one look at the struct is enough to understand the purpose; otherwise, you have to look through the code and the usages of notify to understand the purpose.
| }; | ||
|
|
||
| #[derive(Debug)] | ||
| #[derive(Debug, Clone)] |
There was a problem hiding this comment.
another unnecessary Clone
| self.config_reg.reset(); | ||
| self.dcbaap.reset(); |
There was a problem hiding this comment.
should be in the worker
| config_reg: ConfigureRegister, | ||
| dcbaap: DcbaapRegister, |
There was a problem hiding this comment.
not necessary when all the reset logic is in the worker
| let result = self.reset().await; | ||
| completion.send(()).ok(); | ||
| result?; |
There was a problem hiding this comment.
if the reset failed, then we don't necessarily want to signal successful completion of the reset.
Suggestion:
self.reset().await?;
completion.send_anyhow(())?;
| for slot in self.slots.iter_mut().filter_map(Option::as_mut) { | ||
| result = result.and(slot.pre_drop().await); | ||
| } | ||
| self.slots = [const { None }; MAX_SLOTS as usize].into(); |
There was a problem hiding this comment.
We can get rid of this by also just using Option::take like below, right?
| .store(value.try_into().unwrap(), Ordering::Relaxed); | ||
| .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| { | ||
| let hcrst = current as u64 & usbcmd::HCRST; | ||
| let value = (value & BITMASK_PRESERVED) | (value & usbcmd::HCRST) | hcrst; |
There was a problem hiding this comment.
| let value = (value & BITMASK_PRESERVED) | (value & usbcmd::HCRST) | hcrst; | |
| let value = (value & BITMASK_WRITABLE) | hcrst; |
| // Preserve an ongoing reset until the reset coordinator clears HCRST. | ||
| self.value | ||
| .store(value.try_into().unwrap(), Ordering::Relaxed); | ||
| .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| { |
There was a problem hiding this comment.
Please double-check with @lbeierlieb whether Relaxed is really sufficient here.
When we tried to unload and reload the
xhci_pcimodule in Linux, the warningWARN usbvfiod::device::xhci::command_ring: received useless write to CRCR while runningcan be triggered.The reason is that we didn't really implement host controller reset before. When the Linux guest unloaded and reloaded
xhci_pci, the driver reset the host controller and then reprogrammed the command/event/slot state. Since usbvfiod still kept the old state, the command ring could still be considered running and CRCR writes during re-initialization produced warnings.This PR fixes this error by