From 6b6e6da0609125d8186891aebe1479dafd9c8e41 Mon Sep 17 00:00:00 2001 From: Louis-Philippe Gauthier Date: Sat, 15 Aug 2026 13:44:27 -0400 Subject: [PATCH 1/4] erts: Skip dirty scheduler on non-lingering close close() can only block when SO_LINGER is set with a positive timeout, but nif_finalize_close always runs on a dirty scheduler, costing two scheduler migrations per closed socket. Check the linger option and only take the dirty path when close can block. --- erts/emulator/nifs/common/prim_socket_nif.c | 53 +++++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/erts/emulator/nifs/common/prim_socket_nif.c b/erts/emulator/nifs/common/prim_socket_nif.c index db477e17bb93..a5a1fe84830b 100644 --- a/erts/emulator/nifs/common/prim_socket_nif.c +++ b/erts/emulator/nifs/common/prim_socket_nif.c @@ -7606,9 +7606,9 @@ ERL_NIF_TERM nif_close(ErlNifEnv* env, * Socket (ref) - Points to the socket descriptor. */ static -ERL_NIF_TERM nif_finalize_close(ErlNifEnv* env, - int argc, - const ERL_NIF_TERM argv[]) +ERL_NIF_TERM nif_finalize_close_dirty(ErlNifEnv* env, + int argc, + const ERL_NIF_TERM argv[]) { ESockDescriptor* descP; ERL_NIF_TERM result; @@ -7644,6 +7644,51 @@ ERL_NIF_TERM nif_finalize_close(ErlNifEnv* env, } +/* Only a lingering close - SO_LINGER {onoff = true, linger > 0} - + * can block in close(), so only then do we need the dirty scheduler. + * The check reads descP->sock without the socket locks; a racing + * close/down makes the getsockopt fail and we fall back to the dirty + * path, where the proper state checks run under the locks as before. + */ +static +BOOLEAN_T finalize_close_may_block(ESockDescriptor* descP) +{ + struct linger lval; + SOCKLEN_T lsz = sizeof(lval); + + if (descP->sock == INVALID_SOCKET) + return FALSE; + + if (sock_getopt(descP->sock, SOL_SOCKET, SO_LINGER, + (void*) &lval, &lsz) != 0) + return TRUE; + + return (lval.l_onoff != 0) && (lval.l_linger > 0); +} + + +static +ERL_NIF_TERM nif_finalize_close(ErlNifEnv* env, + int argc, + const ERL_NIF_TERM argv[]) +{ + ESockDescriptor* descP; + + ESOCK_ASSERT( argc == 1 ); + + if (! ESOCK_GET_RESOURCE(env, argv[0], (void**) &descP)) { + return enif_make_badarg(env); + } + + if (finalize_close_may_block(descP)) + return enif_schedule_nif(env, "nif_finalize_close", + ERL_NIF_DIRTY_JOB_IO_BOUND, + nif_finalize_close_dirty, argc, argv); + + return nif_finalize_close_dirty(env, argc, argv); +} + + extern int esock_close_socket(ErlNifEnv* env, ESockDescriptor* descP, @@ -18744,7 +18789,7 @@ ErlNifFunc esock_funcs[] = * is called after the close *select* has "completed". */ {"nif_cancel", 3, nif_cancel, 0}, - {"nif_finalize_close", 1, nif_finalize_close, ERL_NIF_DIRTY_JOB_IO_BOUND} + {"nif_finalize_close", 1, nif_finalize_close, 0} }; From ea8f22b2a0153ded638d0fb045d38a7bf25b9c56 Mon Sep 17 00:00:00 2001 From: Louis-Philippe Gauthier Date: Sat, 15 Aug 2026 13:44:27 -0400 Subject: [PATCH 2/4] erts: Accept sockets non-blocking with accept4 Passing SOCK_NONBLOCK to accept4() makes the SET_NONBLOCKING call on every accepted socket redundant, saving a syscall per accept on platforms that have accept4(). --- erts/config.h.in | 3 +++ erts/configure | 8 ++++++++ erts/configure.ac | 4 ++++ erts/emulator/nifs/unix/unix_socket_syncio.c | 9 ++++++--- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/erts/config.h.in b/erts/config.h.in index ba077f7b9c7a..3660d419eeaa 100644 --- a/erts/config.h.in +++ b/erts/config.h.in @@ -462,6 +462,9 @@ /* Define to 1 if _Float16 can be converted from/to double. */ #undef FLOAT16_IS_CONVERTIBLE +/* Define to 1 if you have the 'accept4' function. */ +#undef HAVE_ACCEPT4 + /* Define to 1 if you have the header file. */ #undef HAVE_ARPA_NAMESER_H diff --git a/erts/configure b/erts/configure index afa96f23fd0b..9ca2da32c3c4 100755 --- a/erts/configure +++ b/erts/configure @@ -21341,6 +21341,14 @@ fi ;; esac +ac_fn_c_check_func "$LINENO" "accept4" "ac_cv_func_accept4" +if test "x$ac_cv_func_accept4" = xyes +then : + printf "%s\n" "#define HAVE_ACCEPT4 1" >>confdefs.h + +fi + + saved_cppflags=$CPPFLAGS diff --git a/erts/configure.ac b/erts/configure.ac index c5113a595312..7d8b48ae8756 100644 --- a/erts/configure.ac +++ b/erts/configure.ac @@ -2243,6 +2243,10 @@ AS_CASE([$host_os], [[#include ]]) ]) +dnl Check for accept4, which accepts a socket and sets it non-blocking +dnl and close-on-exec in one call +AC_CHECK_FUNCS([accept4]) + dnl ---------------------------------------------------------------------- dnl Checks for library functions. diff --git a/erts/emulator/nifs/unix/unix_socket_syncio.c b/erts/emulator/nifs/unix/unix_socket_syncio.c index 3049d287f775..091adbb3a040 100644 --- a/erts/emulator/nifs/unix/unix_socket_syncio.c +++ b/erts/emulator/nifs/unix/unix_socket_syncio.c @@ -195,10 +195,11 @@ * ======================================================================== * */ -#ifdef HAS_ACCEPT4 -// We have to figure out what the flags are... +#if defined(HAVE_ACCEPT4) && defined(SOCK_CLOEXEC) && defined(SOCK_NONBLOCK) #define sock_accept(s, addr, len) \ - accept4((s), (addr), (len), (SOCK_CLOEXEC)) + accept4((s), (addr), (len), (SOCK_CLOEXEC | SOCK_NONBLOCK)) +/* The accepted socket is already non-blocking */ +#define ESSIO_ACCEPTED_NONBLOCK 1 #else #define sock_accept(s, addr, len) accept((s), (addr), (len)) #endif @@ -2863,7 +2864,9 @@ BOOLEAN_T essio_accept_accepted(ErlNifEnv* env, &accDescP->ctrlPid, &accDescP->ctrlMon) == 0 ); +#ifndef ESSIO_ACCEPTED_NONBLOCK SET_NONBLOCKING(accDescP->sock); +#endif accDescP->writeState |= ESOCK_STATE_CONNECTED; From 6be6a8e00807d92a96afc1ce45bd051286a28198 Mon Sep 17 00:00:00 2001 From: Louis-Philippe Gauthier Date: Sun, 16 Aug 2026 19:44:06 -0400 Subject: [PATCH 3/4] erts: Only save the priority around setting IP_TOS Every setsockopt went through a save and restore of both SO_PRIORITY and IP_TOS, on the grounds that "if any other option is set after tos, tos might be zeroed" - a comment carried over from the inet driver. That costs two getsockopt calls and up to two more setsockopt calls on every option set. Only half of it holds. Setting IP_TOS does make the kernel derive a new SO_PRIORITY from it, so that one option has to put the priority back. Setting SO_PRIORITY leaves the tos alone, and so does setting anything else: verified on Linux 7.0 with TCP_NODELAY, SO_RCVBUF and SO_LINGER, none of which disturbed either value. So do it only for IP_TOS, and only for the priority. Setting an option on an accepted connection goes from four system calls to one. The socket options a connection sets still look independent to the user, which is what the dance was there for. --- erts/emulator/nifs/common/prim_socket_nif.c | 73 +++++++++------------ 1 file changed, 30 insertions(+), 43 deletions(-) diff --git a/erts/emulator/nifs/common/prim_socket_nif.c b/erts/emulator/nifs/common/prim_socket_nif.c index a5a1fe84830b..91195a7b5dfb 100644 --- a/erts/emulator/nifs/common/prim_socket_nif.c +++ b/erts/emulator/nifs/common/prim_socket_nif.c @@ -11471,57 +11471,44 @@ int socket_setopt(int sock, int level, int opt, int res; #if defined(IP_TOS) && defined(SOL_IP) && defined(SO_PRIORITY) - int tmpIValPRIO = 0; - int tmpIValTOS = 0; - int resPRIO; - int resTOS; - SOCKOPTLEN_T tmpArgSzPRIO = sizeof(tmpIValPRIO); - SOCKOPTLEN_T tmpArgSzTOS = sizeof(tmpIValTOS); - - resPRIO = sock_getopt(sock, SOL_SOCKET, SO_PRIORITY, - &tmpIValPRIO, &tmpArgSzPRIO); - resTOS = sock_getopt(sock, SOL_IP, IP_TOS, - &tmpIValTOS, &tmpArgSzTOS); + /* Setting IP_TOS makes the kernel derive a new SO_PRIORITY from it, + * so that one option - and only that one - has to have the priority + * saved and put back to keep the two looking independent to the + * user. Setting anything else leaves both alone, and paying a + * getsockopt for each of them plus a setsockopt to restore on every + * option set was most of the system calls a connection made. + */ + if ((level == SOL_IP) && (opt == IP_TOS)) { + int savedPRIO; + int resPRIO; + SOCKOPTLEN_T savedSzPRIO = sizeof(savedPRIO); - res = sock_setopt(sock, level, opt, optVal, optLen); - if (res == 0) { + resPRIO = sock_getopt(sock, SOL_SOCKET, SO_PRIORITY, + &savedPRIO, &savedSzPRIO); - /* Ok, now we *maybe* need to "maybe" restore PRIO and TOS... - * maybe, possibly, ... - */ + res = sock_setopt(sock, level, opt, optVal, optLen); - if (opt != SO_PRIORITY) { - if ((opt != IP_TOS) && (resTOS == 0)) { - resTOS = sock_setopt(sock, SOL_IP, IP_TOS, - (void *) &tmpIValTOS, - tmpArgSzTOS); - res = resTOS; - } - if ((res == 0) && (resPRIO == 0)) { - resPRIO = sock_setopt(sock, SOL_SOCKET, SO_PRIORITY, - &tmpIValPRIO, - tmpArgSzPRIO); - - /* Some kernels set a SO_PRIORITY by default - * that you are not permitted to reset, - * silently ignore this error condition. - */ - - if ((resPRIO != 0) && (sock_errno() == EPERM)) { - res = 0; - } else { - res = resPRIO; - } - } - } - } + if ((res == 0) && (resPRIO == 0)) { + resPRIO = sock_setopt(sock, SOL_SOCKET, SO_PRIORITY, + &savedPRIO, savedSzPRIO); -#else + /* Some kernels set a SO_PRIORITY by default + * that you are not permitted to reset, + * silently ignore this error condition. + */ - res = sock_setopt(sock, level, opt, optVal, optLen); + if ((resPRIO != 0) && (sock_errno() == EPERM)) + res = 0; + else + res = resPRIO; + } + return res; + } #endif + res = sock_setopt(sock, level, opt, optVal, optLen); + return res; } From 9b0fdcfd13561d2e34498054be7d551558cd6fdb Mon Sep 17 00:00:00 2001 From: Louis-Philippe Gauthier Date: Fri, 21 Aug 2026 12:37:42 -0400 Subject: [PATCH 4/4] erts: Guard the linger check for portability SO_LINGER is guarded everywhere else in the file, so guard the new check too. Without the option a close cannot linger, so the dirty scheduler is skipped. Also use SOCKOPTLEN_T for the option length like every other getsockopt call in the file; SOCKLEN_T falls back to size_t on Windows where getsockopt writes through an int pointer. --- erts/emulator/nifs/common/prim_socket_nif.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erts/emulator/nifs/common/prim_socket_nif.c b/erts/emulator/nifs/common/prim_socket_nif.c index 91195a7b5dfb..baca3b38c719 100644 --- a/erts/emulator/nifs/common/prim_socket_nif.c +++ b/erts/emulator/nifs/common/prim_socket_nif.c @@ -7653,8 +7653,9 @@ ERL_NIF_TERM nif_finalize_close_dirty(ErlNifEnv* env, static BOOLEAN_T finalize_close_may_block(ESockDescriptor* descP) { +#if defined(SO_LINGER) struct linger lval; - SOCKLEN_T lsz = sizeof(lval); + SOCKOPTLEN_T lsz = sizeof(lval); if (descP->sock == INVALID_SOCKET) return FALSE; @@ -7664,6 +7665,10 @@ BOOLEAN_T finalize_close_may_block(ESockDescriptor* descP) return TRUE; return (lval.l_onoff != 0) && (lval.l_linger > 0); +#else + /* Without SO_LINGER a close cannot linger, so it cannot block */ + return FALSE; +#endif }