-
Notifications
You must be signed in to change notification settings - Fork 5.5k
SocketAsyncEngine.Unix: Use eventfd(2) to signal shutdown #1538
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| internal static partial class Interop | ||
| { | ||
| internal static partial class Sys | ||
| { | ||
| // | ||
| // Since eventfd is a Linux-only feature, there's no need to define our own | ||
| // flag values: pass these values through as the Linux system call expects. | ||
| // | ||
| [Flags] | ||
| internal enum EventFdFlags | ||
| { | ||
| EFD_SEMAPHORE = 0x1, | ||
| EFD_CLOEXEC = 0x80000, | ||
| EFD_NONBLOCK = 0x800, | ||
| } | ||
|
|
||
| [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_EventFD", SetLastError = true)] | ||
| internal static extern unsafe int EventFD(uint initialVal, EventFdFlags flags = 0); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -421,6 +421,14 @@ DLLEXPORT int32_t SystemNative_CloseDir(DIR* dir); | |||||
| DLLEXPORT int32_t SystemNative_Pipe(int32_t pipefd[2], // [out] pipefds[0] gets read end, pipefd[1] gets write end. | ||||||
| int32_t flags); // 0 for defaults or PAL_O_CLOEXEC for close-on-exec | ||||||
|
|
||||||
| /** | ||||||
| * Creates an eventfd on Linux. Returns error everywhere else. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| * | ||||||
| * Returns 0 for success, -1 for failure. Sets errno on failure. | ||||||
| */ | ||||||
| DLLEXPORT int32_t SystemNative_EventFD(uint32_t initialValue, | ||||||
| int32_t flags); // Passes through to eventfd() without conversion | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Going along with my previous comment:
Suggested change
|
||||||
|
|
||||||
| // NOTE: Rather than a general fcntl shim, we opt to export separate functions | ||||||
| // for each command. This allows use to have strongly typed arguments and saves | ||||||
| // complexity around converting command codes. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -237,6 +237,32 @@ private void FreeHandle(IntPtr handle) | |
| } | ||
| } | ||
|
|
||
| private void CreateShutdownPipeOrEventFD(out int readFD, out int writeFD) | ||
| { | ||
| // | ||
| // If this system supports eventfd(2), use it to signal shutdown, and save us | ||
| // a file descriptor per engine. Otherwise, fall back to using a pipe. | ||
| // | ||
| int eventFD = Interop.Sys.EventFD(0, Interop.Sys.EventFdFlags.EFD_CLOEXEC | Interop.Sys.EventFdFlags.EFD_SEMAPHORE); | ||
|
|
||
| if (eventFD >= 0) | ||
| { | ||
| readFD = writeFD = eventFD; | ||
| } | ||
| else | ||
| { | ||
| int* pipeFds = stackalloc int[2]; | ||
| int pipeResult = Interop.Sys.Pipe(pipeFds, Interop.Sys.PipeFlags.O_CLOEXEC); | ||
| if (pipeResult != 0) | ||
| { | ||
| throw new InternalException(pipeResult); | ||
| } | ||
|
|
||
| readFD = pipeFds[Interop.Sys.ReadEndOfPipe]; | ||
| writeFD = pipeFds[Interop.Sys.WriteEndOfPipe]; | ||
| } | ||
| } | ||
|
|
||
| private SocketAsyncEngine() | ||
| { | ||
| _port = (IntPtr)(-1); | ||
|
|
@@ -258,18 +284,7 @@ private SocketAsyncEngine() | |
| throw new InternalException(err); | ||
| } | ||
|
|
||
| // | ||
| // Create the pipe for signaling shutdown, and register for "read" events for the pipe. Now writing | ||
| // to the pipe will send an event to the event loop. | ||
| // | ||
| int* pipeFds = stackalloc int[2]; | ||
| int pipeResult = Interop.Sys.Pipe(pipeFds, Interop.Sys.PipeFlags.O_CLOEXEC); | ||
| if (pipeResult != 0) | ||
| { | ||
| throw new InternalException(pipeResult); | ||
| } | ||
| _shutdownReadPipe = pipeFds[Interop.Sys.ReadEndOfPipe]; | ||
| _shutdownWritePipe = pipeFds[Interop.Sys.WriteEndOfPipe]; | ||
| CreateShutdownPipeOrEventFD(out _shutdownReadPipe, out _shutdownWritePipe); | ||
|
|
||
| err = Interop.Sys.TryChangeSocketEventRegistration(_port, (IntPtr)_shutdownReadPipe, Interop.Sys.SocketEvents.None, Interop.Sys.SocketEvents.Read, ShutdownHandle); | ||
| if (err != Interop.Error.SUCCESS) | ||
|
|
@@ -351,25 +366,40 @@ private void EventLoop() | |
| private void RequestEventLoopShutdown() | ||
| { | ||
| // | ||
| // Write to the pipe, which will wake up the event loop and cause it to exit. | ||
| // Write to the pipe or eventfd, which will wake up the event loop and cause it to exit. | ||
| // (Need to write 8 bytes in case we're on a system where eventfd is supported.) | ||
| // | ||
| byte b = 1; | ||
| int bytesWritten = Interop.Sys.Write(_shutdownWritePipe, &b, 1); | ||
| if (bytesWritten != 1) | ||
| byte[] wakeThreadUp = {0, 0, 0, 0, 0, 0, 0, 1}; | ||
| fixed (byte *b = &wakeThreadUp[0]) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This allocates an 8-byte array on the heap. That isn't necessary. You can instead make it: byte* wakeThreadUp = stackalloc byte[8] { 0, 0, 0, 0, 0, 0, 0, 1 };and not need the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. Still working on the intuition to understand how C# code is generated/executed. |
||
| { | ||
| throw new InternalException(bytesWritten); | ||
| int bytesWritten = Interop.Sys.Write(_shutdownWritePipe, b, 8); | ||
| if (bytesWritten != 8) | ||
| { | ||
| throw new InternalException(bytesWritten); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks suspect. In general
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm aware of that. However, for eventfd, this is always fine (it's always ready to accept a 64-bit int). When using pipes, it's very unlikely that the buffer wouldn't have at least 8 bytes free, especially since these pipes are used only during shutdown and this is the only thing that's ever written to them (to make it more robust on Linux the pipe could be opened with O_DIRECT, so read/writes are atomic, but this doesn't work under WSL1 and BSDs AFAICT). So this is fine.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't think "very unlikely" is sufficient for code that we need to be extremely robust. I suggest instead doing: Debug.Assert(bytesWritten == 8);
if (bytesWritten < 1)
{
throw new InternalException(bytesWritten);
}or something like that.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pipe buffers are at least a page in all systems we care about. It's not very unlikely, it's very unlikely. But, yeah, the proposed change is fine. Even if the assert is removed in release builds, this would fine: the amount of bytes written here (on pipes, where short writes are possible) doesn't really matter as it's only used to wake the thread blocked on poll(). |
||
| } | ||
| } | ||
| } | ||
|
|
||
| private void FreeNativeResources() | ||
| { | ||
| if (_shutdownReadPipe != -1) | ||
| if (_shutdownReadPipe == _shutdownWritePipe && _shutdownReadPipe != -1) | ||
| { | ||
| // | ||
| // When using eventfd, read/write ends of the "pipe" have the same file | ||
| // descriptor. | ||
| // | ||
| Interop.Sys.Close((IntPtr)_shutdownReadPipe); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be better as: if (_shutdownReadPipe == _shutdownWritePipe)
{
if (_shutdownReadPipe != -1)
{
...
}
} |
||
| } | ||
| if (_shutdownWritePipe != -1) | ||
| else | ||
| { | ||
| Interop.Sys.Close((IntPtr)_shutdownWritePipe); | ||
| if (_shutdownReadPipe != -1) | ||
| { | ||
| Interop.Sys.Close((IntPtr)_shutdownReadPipe); | ||
| } | ||
| if (_shutdownWritePipe != -1) | ||
| { | ||
| Interop.Sys.Close((IntPtr)_shutdownWritePipe); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or better yet: if (_shutdownReadPipe != -1)
{
Interop.Sys.Close((IntPtr)_shutdownReadPipe);
}
if (_shutdownReadPipe != _shutdownWritePipe && _shutdownWritePipe != -1)
{
Interop.Sys.Close((IntPtr)_shutdownWritePipe);
} |
||
| } | ||
| if (_buffer != null) | ||
| { | ||
|
|
||
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 like to treat the shims as platform-agnostic, and the values we pass in here be for the PAL rather than for a particular platform. If the values we define happen to be the same as for that platform, then the PAL implementation can avoid doing any casting and just pass them through, but that's up to the PAL.
What that actually means for your changes:
_ Add static asserts that the PAL EFD_SEMAPHORE matches the target platform's EFD_SEMAPHORE
That will keep the PAL platform-agnostic from a managed code perspective while also not adding runtime overhead.
We do this elsewhere.