-
Notifications
You must be signed in to change notification settings - Fork 38
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
Implement an eventfd-based ping source for Linux. #92
Conversation
Codecov Report
@@ Coverage Diff @@
## master #92 +/- ##
==========================================
- Coverage 89.75% 89.74% -0.02%
==========================================
Files 15 17 +2
Lines 1582 1658 +76
==========================================
+ Hits 1420 1488 +68
- Misses 162 170 +8
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
From Also, the way it works is due to closing triggering wakeup due to fd event leading to read from pipe read fd, while the write end is closed resulting in getting |
Oh thanks, now I know! Unfortunately it doesn't apply to eventfd AFAICT. I don't think closing the |
I think there's still an issue with this though. The read loop in the event source will drain all bytes from the pipe on an event, probably as it should. But if you imagine a sequence of events on the sender: ping, followed by drop/close, before the next dispatch cycle... I think you'll get one event, read the bytes, finish on the |
With your new code yes, the old code handled that fine. So you should follow its behavior. let mut action = PostAction::Continue;
loop {
match read(fd, &mut buf) {
Ok(0) => {
// The other end of the pipe was closed, mark ourselved to for removal
action = PostAction::Remove;
break;
}
Ok(_) => read_something = true,
Err(e) => {
let e: std::io::Error = e.into();
if e.kind() == std::io::ErrorKind::WouldBlock {
// nothing more to read
break;
} else {
// propagate error
return Err(e);
}
}
}
}
if read_something {
callback((), &mut ());
}
Ok(action) As you can see it has 2 flags |
I didn't think my new code changed the functionality of the pipe-based ping source? |
I mean you clearly did, since you're not dropping event source in the case you've described above, while the current ping source will drop, look at the loop it had before with |
Okay I wrote this out and see I dropped the |
3eae6f8
to
537d859
Compare
The pipe-based ping behaviour is restored. The eventfd source still has that issue though. I think adding a final ping send to the drop handler for the sending handle might work. |
I don't see major issue with it? The point is to have less file descriptors open, no? if you share bool flag between sender and receiver it should be fine? Also, you could bench pipe and eventfd I guess, not sure what the way of doing so should be. |
I'm not sure what detail you mean here. The problem I'm trying to avoid is event sources that don't return
I don't really care about performance, in the vague sense of the word, but maybe Calloop and its other users have a specific idea of things they want to avoid. |
You can ping in drop right before setting bool flag, check that bool flag is set and instead of calling callback drop the source. |
And in case you ping and drop right away you can pass bitmask along the line e.g. |
I was implementing this and I realised we can do this purely with the event counter! Write 2 for "real" ping events, and 1 for drop events. Then we just have to bit-test the LSB and if it's set, we're closed. |
537d859
to
a8f60c4
Compare
See latest for this implementation and remarks. It's still a draft because I'd really like to add a test for this. I'll have time for that in a few hours. |
0c9cb1b
to
d892e39
Compare
I've added two tests for (a) ping source removal and also (b) simultaneous ping and source removal. They fail for my previous code and pass for the fixed code. No longer a draft, feedback welcome! |
What if you concurrently drop 2 ping sources? You'll add |
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.
That looks very nice, thanks a lot 👍 I just have a few nitpicks.
src/sources/ping/eventfd.rs
Outdated
// We only have one fd for the eventfd. If the sending end closes it when | ||
// all copies are dropped, the receiving end will be closed as well. We can | ||
// avoid this by duplicating the FD. | ||
let write = dup(read)?; |
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.
If this fails we are leaking a file descriptor
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.
Can we use Arc
instead of dup
here? I don't see a reason to dup it. We could have Weak<Fd>
pointers in senders and check in receiver ref count and drop fd it goes to zero.
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.
We could have Weak pointers in senders and check in receiver ref count and drop fd it goes to zero.
This would work if we can be sure the weak count (ie. number of senders) will never go to zero and then come back up again. Can anyone think of a situation where that might happen? (I can't.)
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'm not quite sure I get how your suggestion would work out @kchibisov
Do you mean checking the refcount every time the source is woken up, and drop the source if it reaches zero? It seems to me that it implies waking up the source every time a Ping
is dropped, and how would we be able to distinguish that from a regular ping()
? Dropping a Ping
handle would cause spurious events on the event source, no?
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.
Yeah I just tried this approach and hit this exact problem. Spurious events aren't such a big deal for me (you get a lot of them with the async executor for example, depending on what's in there), but yes, there's that. And then there's the "what kind of wakeup was this" issue, which means we'd still need the FlagOnDrop
trick, which means two levels of Arc
(on for sharing the fd between senders and source, and one for sharing it between just the senders).
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.
Do I understand that the only point of using Weak
here is so that the FD is closed as soon as the PingSource
is dropped, rather than once the PingSource
and all Ping
s are dropped?
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.
Sort of. It was partly that, and also so that simply holding on to a Ping
did not keep the PingSource
's underlying eventfd alive.
I see your point though, the implementation would be simpler if it was Arc
s all the way.
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.
Latest version has full Arc
s all the way through. I've left the intermediate commits in in case you want to compare, but I'll squash them when we're happy with it.
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 don't think that's a blocking point though, that was just to make sure I understand the point.
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 I prefer the full-Arc version, it is simpler in general.
Eventfd works as an atomic counter, not a pipe, so the numbers are added. The result of that sequence would be that the event source reads 5, and the masking does the rest. |
That's what I'm saying. The |
On the other side, I don't really think that automatic drop should be performed in the first place? The close should occur in the If we've dropped all the senders we shouldn't remove source, since I think it could make sense to have ability to recreate ping wake up sources later on? Anyway, it's more up to @vberger I guess wrt ping design, I'm mostly saying that automatic drop is a bit intense here. |
Note that the |
Isn't that covered by just creating a new |
The removal via
Note that this is still the case: if |
fe9306e
to
f3accfc
Compare
b223766
to
d3e16d4
Compare
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.
LGTM, maybe could you just add a note about this change in the changelog?
Cargo.toml
Outdated
@@ -15,6 +15,7 @@ readme = "README.md" | |||
codecov = { repository = "Smithay/calloop" } | |||
|
|||
[dependencies] | |||
cfg-if = "1.0.0" |
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.
Do we really need cfg if? Right now we're handling cases without it just fine. Not sure it worth?
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.
Sure, it was necessary when I split out the macos pipe vs. non-macos pipe2 implementations, but probably not any more.
pub struct Ping { | ||
// This is an Arc because it's potentially shared with clones. The last one | ||
// dropped needs to signal to the event source via the eventfd. | ||
event: Arc<FlagOnDrop>, |
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.
There is no need for nested arcs? The FlagOnDrop is already an arc? Otherwise whats the purpose of nesting arcs?
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.
If you clone the FlagOnDrop
throughout all the senders, a different one will be dropped every time a sender is dropped. This will (a) wake the source up every time with send_ping()
in its drop handler and (b) hit the problem you mentioned elsewhere, where the counter may be incremented by 2 (or an even number) instead of 1 if two are dropped in between loop dispatches and (c) remove the ability to even know when the last sender is dropped.
There must strictly be one FlagOnDrop
in existence for any given PingSource
for the signalling to work.
To put it another way: Arc
s are for shared ownership.
- The inner level of
Arc
is for shared ownership of the eventfd by both thePingSource
and allPing
s, original or cloned. - The outer level of
Arc
is for shared ownership of the single "thing that adds 1 to the eventfd counter" and "thing that knows if more than zero senders exist" amongst all thePing
clones.
They do not conceptually share ownership of the same thing, so you need two levels.
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.
d3e16d4
to
ac059e4
Compare
Which change? Github won't show me what this refers to. Edit: oh you meant the whole thing, yes, good point. |
Actually, when dropping ping, can you check the ref count. And if it is the last ping to drop you write 1, otherwise you just silently drop? I think it should work and simplify a lot avoiding wakeups. |
Thats some pseudo code but you should get an idea struct Ping {
fd: Weak<EventFd>,
}
impl Ping {
fn ping() {
self.fd.write(2);
}
}
impl Drop for Ping {
fn drop(&mut self) {
if fd.weak_count() == 1 {
self.fd.write(1);
}
}
}
impl PingSource {
fn process_events() {
// ....
bool should_close = false;
match fd.read() {
Ok(n) if n == 1 {
should_close = true;
}
Ok(n) {
callback();
should_close = n % 2 == 0;
}
_ => // nothing...
}
if should_close {
std::mem::drop(self.fd);
}
// ....
}
} |
impl Drop for Ping {
fn drop(&mut self) {
if fd.weak_count() == 1 {
self.fd.write(1);
}
}
} I think that part is racy, if the last two |
Yeah, and I don't see a way to decrement and get value of ref count. There could be a Since right now if you drop from 2 threads at the same time you won't drop, since you write Yeah, even if you try speculating with |
I'm not sure what you mean by "right now", the PR as it is currently does not have this issue, thanks to the two layers of |
Yeah, maybe double arc isn't that bad. |
In any case, I think this is deep into optimization land, and worst case this can always be improved in a future PR if we find a nice way to do it. |
FYI this is the promise of both an Re. raciness, see this note from the
|
Never mind, I was reading it backwards. |
ac059e4
to
a67260f
Compare
Changelog updated. |
This separates and reorganises the underlying mechanics for the ping source, and implements an eventfd-based ping for Linux.
a67260f
to
4a442f2
Compare
Alright thanks, let's merge this. 👍 |
This separates and reorganises the underlying mechanics for the ping source, and implements an eventfd-based ping for Linux.
Currently a draft, but close to final.Partly addresses #15, maybe there's an analogue for BSD-ishes?Eventfd has a bit of a quirk I didn't think about until I got halfway through this. It gives you one fd instead of two like a pipe. This means that if you close that fd, eg. through
CloseOnDrop
, the receiving end's fd becomes invalid and so does theGeneric
source wrapping it. In practice you just get someEBADFD
errors, but theoretically if the fd is reassigned you could get some extreme weirdness.I solved this by (a) adding a
dup()
system call at creation to create separate reading and writing fds. Closing a duplicated fd does not affect others that are open, solving the first problem.The other problem is that the source's fd has no way of signalling that the other fd has been closed. Adding another event source to handle this seemed like it would cancel out the performance/complexity benefits of using eventfd in the first place. I have attempted to solve this by using an
AtomicBool
flag wrapped in anArc
, keeping the sending handle thread-safe and cloneable.This sort of begged the question for me though, how does the pipe-based ping handle this? The event-source-code seems to suggest that reading zero bytes from the pipe after it becomes readable means that the other end was closed. But I can't find any documentation saying that closing a pipe end will make the other end become readable in the first place. Anyway, it might be the case that there's no foolproof way to handle this. I'd welcome any suggestions.
Other notes:
#[inline]
, not so much for performance reasons but more to indicate that they're just used as interchangeable pieces in the other helper functions.send_ping()
,drain_ping()
etc) even though these are private functions. It's not a suggestion that this structure be enforced. It's just that breaking those functions out of the pipe impl helped me build them back up for the eventfd impl. If you prefer them back in the event source, that's fine by me.Docs/links: