From 25c5299ba69ea030c122243e46b72e130ccf92b1 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:51:41 -0500 Subject: [PATCH 01/15] Use rb_thread_call_without_gvl. --- ext/proc/wait3.c | 291 ++++++++++++++++++++++++++++------------------- 1 file changed, 176 insertions(+), 115 deletions(-) diff --git a/ext/proc/wait3.c b/ext/proc/wait3.c index e6b2f12..baea0c4 100644 --- a/ext/proc/wait3.c +++ b/ext/proc/wait3.c @@ -1,4 +1,5 @@ #include +#include #include #include @@ -31,6 +32,67 @@ VALUE v_procstat_struct, v_siginfo_struct, v_usage_struct; static void sigproc(int signum, siginfo_t* info, void* ucontext); +/* Structs for rb_thread_call_without_gvl wrappers */ +struct wait3_args { + int status; + int flags; + struct rusage rusage; + pid_t pid; +}; + +#ifdef HAVE_WAIT4 +struct wait4_args { + pid_t pid; + int status; + int flags; + struct rusage rusage; + pid_t result; +}; +#endif + +#ifdef HAVE_WAITID +struct waitid_args { + idtype_t idtype; + id_t id; + siginfo_t infop; + int options; + int result; +}; +#endif + +struct pause_args { + int result; +}; + +/* GVL-free wrapper functions */ +static void* wait3_without_gvl(void* data) { + struct wait3_args* args = (struct wait3_args*)data; + args->pid = wait3(&args->status, args->flags, &args->rusage); + return NULL; +} + +#ifdef HAVE_WAIT4 +static void* wait4_without_gvl(void* data) { + struct wait4_args* args = (struct wait4_args*)data; + args->result = wait4(args->pid, &args->status, args->flags, &args->rusage); + return NULL; +} +#endif + +#ifdef HAVE_WAITID +static void* waitid_without_gvl(void* data) { + struct waitid_args* args = (struct waitid_args*)data; + args->result = waitid(args->idtype, args->id, &args->infop, args->options); + return NULL; +} +#endif + +static void* pause_without_gvl(void* data) { + struct pause_args* args = (struct pause_args*)data; + args->result = pause(); + return NULL; +} + /* * Returns true if this process is stopped. This is only returned * returned if the corresponding wait() call had the WUNTRACED flag @@ -142,55 +204,53 @@ static VALUE pst_wstopsig(int status) * set. Raises a SystemError if there are no child processes. */ static VALUE proc_wait3(int argc, VALUE *argv, VALUE mod){ - int status; - int flags = 0; - struct rusage r; - pid_t pid; + struct wait3_args args; VALUE v_flags = Qnil; rb_scan_args(argc,argv,"01",&v_flags); + memset(&args, 0, sizeof(args)); + if(Qnil != v_flags){ - flags = NUM2INT(v_flags); + args.flags = NUM2INT(v_flags); } - bzero(&r, sizeof(r)); - pid = wait3(&status, flags, &r); + rb_thread_call_without_gvl(wait3_without_gvl, &args, RUBY_UBF_PROCESS, NULL); - if(pid < 0){ + if(args.pid < 0){ rb_sys_fail("wait3"); } - else if(pid > 0){ + else if(args.pid > 0){ v_last_status = rb_struct_new(v_procstat_struct, - INT2FIX(pid), - INT2FIX(status), - rb_float_new((double)r.ru_utime.tv_sec+(double)r.ru_utime.tv_usec/1e6), - rb_float_new((double)r.ru_stime.tv_sec+(double)r.ru_stime.tv_usec/1e6), - LONG2NUM(r.ru_maxrss), - LONG2NUM(r.ru_ixrss), - LONG2NUM(r.ru_idrss), - LONG2NUM(r.ru_isrss), - LONG2NUM(r.ru_minflt), - LONG2NUM(r.ru_majflt), - LONG2NUM(r.ru_nswap), - LONG2NUM(r.ru_inblock), - LONG2NUM(r.ru_oublock), - LONG2NUM(r.ru_msgsnd), - LONG2NUM(r.ru_msgrcv), - LONG2NUM(r.ru_nsignals), - LONG2NUM(r.ru_nvcsw), - LONG2NUM(r.ru_nivcsw), - pst_wifstopped(status), - pst_wifsignaled(status), - pst_wifexited(status), - pst_success_p(status), - pst_wcoredump(status), - pst_wexitstatus(status), - pst_wtermsig(status), - pst_wstopsig(status) + INT2FIX(args.pid), + INT2FIX(args.status), + rb_float_new((double)args.rusage.ru_utime.tv_sec+(double)args.rusage.ru_utime.tv_usec/1e6), + rb_float_new((double)args.rusage.ru_stime.tv_sec+(double)args.rusage.ru_stime.tv_usec/1e6), + LONG2NUM(args.rusage.ru_maxrss), + LONG2NUM(args.rusage.ru_ixrss), + LONG2NUM(args.rusage.ru_idrss), + LONG2NUM(args.rusage.ru_isrss), + LONG2NUM(args.rusage.ru_minflt), + LONG2NUM(args.rusage.ru_majflt), + LONG2NUM(args.rusage.ru_nswap), + LONG2NUM(args.rusage.ru_inblock), + LONG2NUM(args.rusage.ru_oublock), + LONG2NUM(args.rusage.ru_msgsnd), + LONG2NUM(args.rusage.ru_msgrcv), + LONG2NUM(args.rusage.ru_nsignals), + LONG2NUM(args.rusage.ru_nvcsw), + LONG2NUM(args.rusage.ru_nivcsw), + pst_wifstopped(args.status), + pst_wifsignaled(args.status), + pst_wifexited(args.status), + pst_success_p(args.status), + pst_wcoredump(args.status), + pst_wexitstatus(args.status), + pst_wtermsig(args.status), + pst_wstopsig(args.status) ); - rb_last_status_set(status, pid); + rb_last_status_set(args.status, args.pid); OBJ_FREEZE(v_last_status); return v_last_status; @@ -213,57 +273,54 @@ static VALUE proc_wait3(int argc, VALUE *argv, VALUE mod){ * Some +flags+ are not supported on all platforms. */ static VALUE proc_wait4(int argc, VALUE *argv, VALUE mod){ - int status; - int flags = 0; - struct rusage r; - pid_t pid; + struct wait4_args args; VALUE v_pid; VALUE v_flags = Qnil; rb_scan_args(argc, argv, "11", &v_pid, &v_flags); - pid = NUM2INT(v_pid); + memset(&args, 0, sizeof(args)); + args.pid = NUM2INT(v_pid); if(RTEST(v_flags)) - flags = NUM2INT(v_flags); + args.flags = NUM2INT(v_flags); - bzero(&r, sizeof(r)); - pid = wait4(pid, &status, flags, &r); + rb_thread_call_without_gvl(wait4_without_gvl, &args, RUBY_UBF_PROCESS, NULL); - if(pid < 0){ + if(args.result < 0){ rb_sys_fail("wait4"); } - else if(pid > 0){ + else if(args.result > 0){ v_last_status = rb_struct_new(v_procstat_struct, - INT2FIX(pid), - INT2FIX(status), - rb_float_new((double)r.ru_utime.tv_sec+(double)r.ru_utime.tv_usec/1e6), - rb_float_new((double)r.ru_stime.tv_sec+(double)r.ru_stime.tv_usec/1e6), - LONG2NUM(r.ru_maxrss), - LONG2NUM(r.ru_ixrss), - LONG2NUM(r.ru_idrss), - LONG2NUM(r.ru_isrss), - LONG2NUM(r.ru_minflt), - LONG2NUM(r.ru_majflt), - LONG2NUM(r.ru_nswap), - LONG2NUM(r.ru_inblock), - LONG2NUM(r.ru_oublock), - LONG2NUM(r.ru_msgsnd), - LONG2NUM(r.ru_msgrcv), - LONG2NUM(r.ru_nsignals), - LONG2NUM(r.ru_nvcsw), - LONG2NUM(r.ru_nivcsw), - pst_wifstopped(status), - pst_wifsignaled(status), - pst_wifexited(status), - pst_success_p(status), - pst_wcoredump(status), - pst_wexitstatus(status), - pst_wtermsig(status), - pst_wstopsig(status) + INT2FIX(args.result), + INT2FIX(args.status), + rb_float_new((double)args.rusage.ru_utime.tv_sec+(double)args.rusage.ru_utime.tv_usec/1e6), + rb_float_new((double)args.rusage.ru_stime.tv_sec+(double)args.rusage.ru_stime.tv_usec/1e6), + LONG2NUM(args.rusage.ru_maxrss), + LONG2NUM(args.rusage.ru_ixrss), + LONG2NUM(args.rusage.ru_idrss), + LONG2NUM(args.rusage.ru_isrss), + LONG2NUM(args.rusage.ru_minflt), + LONG2NUM(args.rusage.ru_majflt), + LONG2NUM(args.rusage.ru_nswap), + LONG2NUM(args.rusage.ru_inblock), + LONG2NUM(args.rusage.ru_oublock), + LONG2NUM(args.rusage.ru_msgsnd), + LONG2NUM(args.rusage.ru_msgrcv), + LONG2NUM(args.rusage.ru_nsignals), + LONG2NUM(args.rusage.ru_nvcsw), + LONG2NUM(args.rusage.ru_nivcsw), + pst_wifstopped(args.status), + pst_wifsignaled(args.status), + pst_wifexited(args.status), + pst_success_p(args.status), + pst_wcoredump(args.status), + pst_wexitstatus(args.status), + pst_wtermsig(args.status), + pst_wstopsig(args.status) ); - rb_last_status_set(status, pid); + rb_last_status_set(args.status, args.result); OBJ_FREEZE(v_last_status); return v_last_status; @@ -314,20 +371,18 @@ static VALUE proc_wait4(int argc, VALUE *argv, VALUE mod){ */ static VALUE proc_waitid(int argc, VALUE* argv, VALUE mod){ VALUE v_type, v_id, v_options; - siginfo_t infop; - idtype_t idtype; - id_t id = 0; - int options = 0; + struct waitid_args args; rb_scan_args(argc, argv, "12", &v_type, &v_id, &v_options); - idtype = NUM2INT(v_type); + memset(&args, 0, sizeof(args)); + args.idtype = NUM2INT(v_type); if(RTEST(v_id)) - id = NUM2INT(v_id); + args.id = NUM2INT(v_id); if(RTEST(v_options)) - options = NUM2INT(v_options); + args.options = NUM2INT(v_options); /* The Linux man page for waitid() says to zero out the pid field and check * its value after the call to waitid() to detect if there were children in @@ -335,10 +390,12 @@ static VALUE proc_waitid(int argc, VALUE* argv, VALUE mod){ * simply check the infop.si_signo struct member against SI_NOINFO. */ #ifndef SI_NOINFO - infop.si_pid = 0; + args.infop.si_pid = 0; #endif - if(waitid(idtype, id, &infop, options) == -1) + rb_thread_call_without_gvl(waitid_without_gvl, &args, RUBY_UBF_PROCESS, NULL); + + if(args.result == -1) rb_sys_fail("waitid"); /* If the si_code struct member returns SI_NOINFO, or the si_pid member @@ -352,13 +409,13 @@ static VALUE proc_waitid(int argc, VALUE* argv, VALUE mod){ */ #ifdef SI_NOINFO - if(infop.si_code == SI_NOINFO){ + if(args.infop.si_code == SI_NOINFO){ #else - if(infop.si_pid == 0){ + if(args.infop.si_pid == 0){ #endif v_last_status = rb_struct_new(v_siginfo_struct, - INT2FIX(infop.si_signo), - INT2FIX(infop.si_errno), + INT2FIX(args.infop.si_signo), + INT2FIX(args.infop.si_errno), Qnil, Qnil, Qnil, Qnil, Qnil, Qnil, /* code, pid, uid, utime, status, stime */ #ifdef HAVE_ST_SI_TRAPNO Qnil, @@ -424,8 +481,8 @@ static VALUE proc_waitid(int argc, VALUE* argv, VALUE mod){ VALUE v_state = Qnil; #endif VALUE v_band = Qnil, v_entity = Qnil; - int sig = infop.si_signo; - int code = infop.si_code; + int sig = args.infop.si_signo; + int code = args.infop.si_code; #if defined(HAVE_ST_SI_SYSARG) || defined(HAVE_ST_SI_MSTATE) int i = 0; @@ -437,13 +494,13 @@ static VALUE proc_waitid(int argc, VALUE* argv, VALUE mod){ */ if(sig == SIGCHLD){ #ifdef HAVE_ST_SI_UTIME - v_utime = ULL2NUM(infop.si_utime); + v_utime = ULL2NUM(args.infop.si_utime); #endif #ifdef HAVE_ST_SI_STATUS - v_status = ULL2NUM(infop.si_status); + v_status = ULL2NUM(args.infop.si_status); #endif #ifdef HAVE_ST_SI_STIME - v_stime = ULL2NUM(infop.si_stime); + v_stime = ULL2NUM(args.infop.si_stime); #endif } @@ -451,66 +508,66 @@ static VALUE proc_waitid(int argc, VALUE* argv, VALUE mod){ sig == SIGTRAP) { #ifdef HAVE_ST_SI_TRAPNO - v_trapno = INT2FIX(infop.si_trapno); + v_trapno = INT2FIX(args.infop.si_trapno); #endif #ifdef HAVE_ST_SI_PC - v_pc = INT2FIX(infop.si_pc); + v_pc = INT2FIX(args.infop.si_pc); #endif } if(sig == SIGXFSZ){ #ifdef HAVE_ST_SI_FD - v_fd = INT2FIX(infop.si_fd); + v_fd = INT2FIX(args.infop.si_fd); #endif if(code == POLL_IN || code == POLL_OUT || code == POLL_MSG){ - v_band = LONG2FIX(infop.si_band); + v_band = LONG2FIX(args.infop.si_band); } } if(sig == SIGPROF){ #ifdef HAVE_ST_SI_SYSARG - int ssize = sizeof(infop.si_sysarg) / sizeof(infop.si_sysarg[0]); + int ssize = sizeof(args.infop.si_sysarg) / sizeof(args.infop.si_sysarg[0]); v_sysarg = rb_ary_new(); for(i = 0; i < ssize; i++) - rb_ary_push(v_sysarg, LONG2FIX(infop.si_sysarg[i])); + rb_ary_push(v_sysarg, LONG2FIX(args.infop.si_sysarg[i])); #endif #ifdef HAVE_ST_SI_MSTATE - int msize = sizeof(infop.si_mstate) / sizeof(infop.si_mstate[0]); + int msize = sizeof(args.infop.si_mstate) / sizeof(args.infop.si_mstate[0]); v_state = rb_ary_new(); for(i = 0; i < msize; i++) - rb_ary_push(v_state, INT2FIX(infop.si_mstate[i])); + rb_ary_push(v_state, INT2FIX(args.infop.si_mstate[i])); #endif #ifdef HAVE_ST_SI_FADDR - v_addr = INT2FIX(infop.si_faddr); + v_addr = INT2FIX(args.infop.si_faddr); #endif #ifdef HAVE_ST_SI_SYSCALL - v_syscall = INT2FIX(infop.si_syscall); + v_syscall = INT2FIX(args.infop.si_syscall); #endif #ifdef HAVE_ST_SI_NSYSARG - v_nsysarg = INT2FIX(infop.si_nsysarg); + v_nsysarg = INT2FIX(args.infop.si_nsysarg); #endif #ifdef HAVE_ST_SI_FAULT - v_fault = INT2FIX(infop.si_fault); + v_fault = INT2FIX(args.infop.si_fault); #endif #ifdef HAVE_ST_SI_TSTAMP - v_time = rb_time_new(infop.si_tstamp.tv_sec,infop.si_tstamp.tv_nsec); + v_time = rb_time_new(args.infop.si_tstamp.tv_sec,args.infop.si_tstamp.tv_nsec); #endif } #ifdef SIGXRES if(sig == SIGXRES){ - v_entity = INT2FIX(infop.si_entity); + v_entity = INT2FIX(args.infop.si_entity); } #endif v_last_status = rb_struct_new(v_siginfo_struct, - INT2FIX(infop.si_signo), // Probably SIGCHLD - INT2FIX(infop.si_errno), // 0 means no error - INT2FIX(infop.si_code), // Should be anything but SI_NOINFO - INT2FIX(infop.si_pid), // Real PID that sent the signal - INT2FIX(infop.si_uid), // Real UID of process that sent signal + INT2FIX(args.infop.si_signo), // Probably SIGCHLD + INT2FIX(args.infop.si_errno), // 0 means no error + INT2FIX(args.infop.si_code), // Should be anything but SI_NOINFO + INT2FIX(args.infop.si_pid), // Real PID that sent the signal + INT2FIX(args.infop.si_uid), // Real UID of process that sent signal v_utime, v_status, v_stime, @@ -589,8 +646,8 @@ static VALUE proc_pause(int argc, VALUE* argv, VALUE mod){ int signum; struct sigaction act, sa; - bzero(&act, sizeof(act)); - bzero(&sa, sizeof(sa)); + memset(&act, 0, sizeof(act)); + memset(&sa, 0, sizeof(sa)); for(i = 0; i < len; i++){ v_val = rb_ary_shift(v_signals); @@ -619,7 +676,7 @@ static VALUE proc_pause(int argc, VALUE* argv, VALUE mod){ signum = NUM2INT(v_val); } - bzero(&act, sizeof(act)); + memset(&act, 0, sizeof(act)); act.sa_flags = SA_SIGINFO; act.sa_sigaction = sigproc; res = sigaction(signum, &act, &sa); @@ -629,7 +686,11 @@ static VALUE proc_pause(int argc, VALUE* argv, VALUE mod){ } } - return INT2FIX(pause()); /* Should always be -1 */ + { + struct pause_args pargs; + rb_thread_call_without_gvl(pause_without_gvl, &pargs, RUBY_UBF_PROCESS, NULL); + return INT2FIX(pargs.result); /* Should always be -1 */ + } } /* @@ -757,7 +818,7 @@ static VALUE proc_getrusage(int argc, VALUE* argv, VALUE mod){ else if(RTEST(v_children)) who = RUSAGE_CHILDREN; - bzero(&r, sizeof(r)); + memset(&r, 0, sizeof(r)); if(getrusage(who,&r) == -1) rb_sys_fail("getrusage"); From a43befe2d9ae76c0b2259579c332c0acdc2fa0c9 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:52:52 -0500 Subject: [PATCH 02/15] Restore bzero. --- ext/proc/wait3.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ext/proc/wait3.c b/ext/proc/wait3.c index baea0c4..df53a04 100644 --- a/ext/proc/wait3.c +++ b/ext/proc/wait3.c @@ -209,7 +209,7 @@ static VALUE proc_wait3(int argc, VALUE *argv, VALUE mod){ rb_scan_args(argc,argv,"01",&v_flags); - memset(&args, 0, sizeof(args)); + bzero(&args, sizeof(args)); if(Qnil != v_flags){ args.flags = NUM2INT(v_flags); @@ -279,7 +279,7 @@ static VALUE proc_wait4(int argc, VALUE *argv, VALUE mod){ rb_scan_args(argc, argv, "11", &v_pid, &v_flags); - memset(&args, 0, sizeof(args)); + bzero(&args, sizeof(args)); args.pid = NUM2INT(v_pid); if(RTEST(v_flags)) @@ -375,7 +375,7 @@ static VALUE proc_waitid(int argc, VALUE* argv, VALUE mod){ rb_scan_args(argc, argv, "12", &v_type, &v_id, &v_options); - memset(&args, 0, sizeof(args)); + bzero(&args, sizeof(args)); args.idtype = NUM2INT(v_type); if(RTEST(v_id)) @@ -646,8 +646,8 @@ static VALUE proc_pause(int argc, VALUE* argv, VALUE mod){ int signum; struct sigaction act, sa; - memset(&act, 0, sizeof(act)); - memset(&sa, 0, sizeof(sa)); + bzero(&act, sizeof(act)); + bzero(&sa, sizeof(sa)); for(i = 0; i < len; i++){ v_val = rb_ary_shift(v_signals); @@ -676,7 +676,7 @@ static VALUE proc_pause(int argc, VALUE* argv, VALUE mod){ signum = NUM2INT(v_val); } - memset(&act, 0, sizeof(act)); + bzero(&act, sizeof(act)); act.sa_flags = SA_SIGINFO; act.sa_sigaction = sigproc; res = sigaction(signum, &act, &sa); From 49bc12e257ecc7cdb2f3be9c046f30a0b66a1447 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:59:34 -0500 Subject: [PATCH 03/15] Add fiber scheduler support where feasible. --- ext/extconf.rb | 3 ++ ext/proc/wait3.c | 119 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 116 insertions(+), 6 deletions(-) diff --git a/ext/extconf.rb b/ext/extconf.rb index d6b4434..4dd97b4 100644 --- a/ext/extconf.rb +++ b/ext/extconf.rb @@ -70,6 +70,9 @@ # RUSAGE_THREAD is Linux-specific have_const('RUSAGE_THREAD', 'sys/resource.h') +# Fiber scheduler support (Ruby 3.0+) +have_func('rb_fiber_scheduler_current', 'ruby.h') + # BSD have_const('P_JAILID', 'sys/wait.h') diff --git a/ext/proc/wait3.c b/ext/proc/wait3.c index df53a04..2a21d21 100644 --- a/ext/proc/wait3.c +++ b/ext/proc/wait3.c @@ -1,5 +1,8 @@ #include #include +#ifdef HAVE_RB_FIBER_SCHEDULER_CURRENT +#include +#endif #include #include @@ -27,6 +30,11 @@ #define SIG2STR_MAX 32 #endif +/* NUM2PIDT may not be defined on older Ruby versions */ +#ifndef NUM2PIDT +#define NUM2PIDT(v) ((pid_t)NUM2LONG(v)) +#endif + VALUE v_last_status; VALUE v_procstat_struct, v_siginfo_struct, v_usage_struct; @@ -193,6 +201,44 @@ static VALUE pst_wstopsig(int status) return Qnil; } +#ifdef HAVE_RB_FIBER_SCHEDULER_CURRENT +/* + * Helper to build ProcStat struct from pid and status. + * Used by fiber scheduler path where rusage is not available. + * All rusage fields are set to 0. + */ +static VALUE build_procstat_without_rusage(pid_t pid, int status) { + return rb_struct_new(v_procstat_struct, + INT2FIX(pid), + INT2FIX(status), + rb_float_new(0.0), /* utime - not available via fiber scheduler */ + rb_float_new(0.0), /* stime - not available via fiber scheduler */ + LONG2NUM(0), /* maxrss */ + LONG2NUM(0), /* ixrss */ + LONG2NUM(0), /* idrss */ + LONG2NUM(0), /* isrss */ + LONG2NUM(0), /* minflt */ + LONG2NUM(0), /* majflt */ + LONG2NUM(0), /* nswap */ + LONG2NUM(0), /* inblock */ + LONG2NUM(0), /* oublock */ + LONG2NUM(0), /* msgsnd */ + LONG2NUM(0), /* msgrcv */ + LONG2NUM(0), /* nsignals */ + LONG2NUM(0), /* nvcsw */ + LONG2NUM(0), /* nivcsw */ + pst_wifstopped(status), + pst_wifsignaled(status), + pst_wifexited(status), + pst_success_p(status), + pst_wcoredump(status), + pst_wexitstatus(status), + pst_wtermsig(status), + pst_wstopsig(status) + ); +} +#endif + /* * call-seq: * Process.wait3(flags=nil) @@ -202,18 +248,47 @@ static VALUE pst_wstopsig(int status) * * The return value is a ProcStat structure. The special global $? is also * set. Raises a SystemError if there are no child processes. + * + * Note: When a fiber scheduler is active (Ruby 3.0+), this method will + * cooperate with the scheduler. However, rusage information will not be + * available and those fields will be set to 0 in the returned struct. */ static VALUE proc_wait3(int argc, VALUE *argv, VALUE mod){ struct wait3_args args; VALUE v_flags = Qnil; + int flags = 0; rb_scan_args(argc,argv,"01",&v_flags); - bzero(&args, sizeof(args)); - if(Qnil != v_flags){ - args.flags = NUM2INT(v_flags); + flags = NUM2INT(v_flags); + } + +#ifdef HAVE_RB_FIBER_SCHEDULER_CURRENT + VALUE scheduler = rb_fiber_scheduler_current(); + if (!NIL_P(scheduler)) { + /* Use fiber scheduler - wait for any child (pid = -1) */ + VALUE result = rb_fiber_scheduler_process_wait(scheduler, RB_INT2NUM(-1), RB_INT2NUM(flags)); + + if (NIL_P(result)) { + return Qnil; + } + + /* Extract pid and status from Process::Status object */ + pid_t pid = NUM2PIDT(rb_funcall(result, rb_intern("pid"), 0)); + int status = NUM2INT(rb_funcall(result, rb_intern("to_i"), 0)); + + v_last_status = build_procstat_without_rusage(pid, status); + rb_last_status_set(status, pid); + OBJ_FREEZE(v_last_status); + + return v_last_status; } +#endif + + /* No fiber scheduler - use thread-based blocking */ + bzero(&args, sizeof(args)); + args.flags = flags; rb_thread_call_without_gvl(wait3_without_gvl, &args, RUBY_UBF_PROCESS, NULL); @@ -271,19 +346,51 @@ static VALUE proc_wait3(int argc, VALUE *argv, VALUE mod){ * This method is not supported on all platforms. * * Some +flags+ are not supported on all platforms. + * + * Note: When a fiber scheduler is active (Ruby 3.0+), this method will + * cooperate with the scheduler. However, rusage information will not be + * available and those fields will be set to 0 in the returned struct. */ static VALUE proc_wait4(int argc, VALUE *argv, VALUE mod){ struct wait4_args args; VALUE v_pid; VALUE v_flags = Qnil; + pid_t pid; + int flags = 0; rb_scan_args(argc, argv, "11", &v_pid, &v_flags); - bzero(&args, sizeof(args)); - args.pid = NUM2INT(v_pid); + pid = NUM2PIDT(v_pid); if(RTEST(v_flags)) - args.flags = NUM2INT(v_flags); + flags = NUM2INT(v_flags); + +#ifdef HAVE_RB_FIBER_SCHEDULER_CURRENT + VALUE scheduler = rb_fiber_scheduler_current(); + if (!NIL_P(scheduler)) { + /* Use fiber scheduler */ + VALUE result = rb_fiber_scheduler_process_wait(scheduler, RB_INT2NUM(pid), RB_INT2NUM(flags)); + + if (NIL_P(result)) { + return Qnil; + } + + /* Extract pid and status from Process::Status object */ + pid_t rpid = NUM2PIDT(rb_funcall(result, rb_intern("pid"), 0)); + int status = NUM2INT(rb_funcall(result, rb_intern("to_i"), 0)); + + v_last_status = build_procstat_without_rusage(rpid, status); + rb_last_status_set(status, rpid); + OBJ_FREEZE(v_last_status); + + return v_last_status; + } +#endif + + /* No fiber scheduler - use thread-based blocking */ + bzero(&args, sizeof(args)); + args.pid = pid; + args.flags = flags; rb_thread_call_without_gvl(wait4_without_gvl, &args, RUBY_UBF_PROCESS, NULL); From ee47935f6ba2c6fb5cd946479c23ede1d083fa2b Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:10:23 -0500 Subject: [PATCH 04/15] Assume Ruby 3.0 or later. --- ext/extconf.rb | 3 --- ext/proc/wait3.c | 13 ------------- 2 files changed, 16 deletions(-) diff --git a/ext/extconf.rb b/ext/extconf.rb index 4dd97b4..d6b4434 100644 --- a/ext/extconf.rb +++ b/ext/extconf.rb @@ -70,9 +70,6 @@ # RUSAGE_THREAD is Linux-specific have_const('RUSAGE_THREAD', 'sys/resource.h') -# Fiber scheduler support (Ruby 3.0+) -have_func('rb_fiber_scheduler_current', 'ruby.h') - # BSD have_const('P_JAILID', 'sys/wait.h') diff --git a/ext/proc/wait3.c b/ext/proc/wait3.c index 2a21d21..d3adcef 100644 --- a/ext/proc/wait3.c +++ b/ext/proc/wait3.c @@ -1,8 +1,6 @@ #include #include -#ifdef HAVE_RB_FIBER_SCHEDULER_CURRENT #include -#endif #include #include @@ -30,11 +28,6 @@ #define SIG2STR_MAX 32 #endif -/* NUM2PIDT may not be defined on older Ruby versions */ -#ifndef NUM2PIDT -#define NUM2PIDT(v) ((pid_t)NUM2LONG(v)) -#endif - VALUE v_last_status; VALUE v_procstat_struct, v_siginfo_struct, v_usage_struct; @@ -201,7 +194,6 @@ static VALUE pst_wstopsig(int status) return Qnil; } -#ifdef HAVE_RB_FIBER_SCHEDULER_CURRENT /* * Helper to build ProcStat struct from pid and status. * Used by fiber scheduler path where rusage is not available. @@ -237,7 +229,6 @@ static VALUE build_procstat_without_rusage(pid_t pid, int status) { pst_wstopsig(status) ); } -#endif /* * call-seq: @@ -264,7 +255,6 @@ static VALUE proc_wait3(int argc, VALUE *argv, VALUE mod){ flags = NUM2INT(v_flags); } -#ifdef HAVE_RB_FIBER_SCHEDULER_CURRENT VALUE scheduler = rb_fiber_scheduler_current(); if (!NIL_P(scheduler)) { /* Use fiber scheduler - wait for any child (pid = -1) */ @@ -284,7 +274,6 @@ static VALUE proc_wait3(int argc, VALUE *argv, VALUE mod){ return v_last_status; } -#endif /* No fiber scheduler - use thread-based blocking */ bzero(&args, sizeof(args)); @@ -365,7 +354,6 @@ static VALUE proc_wait4(int argc, VALUE *argv, VALUE mod){ if(RTEST(v_flags)) flags = NUM2INT(v_flags); -#ifdef HAVE_RB_FIBER_SCHEDULER_CURRENT VALUE scheduler = rb_fiber_scheduler_current(); if (!NIL_P(scheduler)) { /* Use fiber scheduler */ @@ -385,7 +373,6 @@ static VALUE proc_wait4(int argc, VALUE *argv, VALUE mod){ return v_last_status; } -#endif /* No fiber scheduler - use thread-based blocking */ bzero(&args, sizeof(args)); From ff9b474f018d0c614b521d870242d84ed6ef52e1 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:17:32 -0500 Subject: [PATCH 05/15] Remove 2.7, add 3.4 to workflow. --- .github/workflows/ruby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index db7658e..24c59fa 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -15,7 +15,7 @@ jobs: test: strategy: matrix: - ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3'] + ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4'] platform: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: From 39abb6b3187b304caf5a95946e02f4ea8458832f Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:30:58 -0500 Subject: [PATCH 06/15] Tweak clean task for MacOS. --- Rakefile | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Rakefile b/Rakefile index 9a682be..7fb29f9 100644 --- a/Rakefile +++ b/Rakefile @@ -7,15 +7,15 @@ require 'rbconfig' include RbConfig CLEAN.include( - '**/*.gem', # Gem files - '**/*.rbc', # Rubinius - '**/*.o', # C object file - '**/*.log', # Ruby extension build log - '**/*.lock', # Gemfile.lock - '**/Makefile', # C Makefile - '**/conftest.dSYM', # OS X build directory - "**/*.#{CONFIG['DLEXT']}", # C shared object - '**/*.lock' # Bundler + '**/*.gem', # Gem files + '**/*.rbc', # Rubinius + '**/*.o', # C object file + '**/*.log', # Ruby extension build log + '**/*.lock', # Gemfile.lock + '**/Makefile', # C Makefile + '**/*.dSYM', # OS X build directory + "**/*.#{CONFIG['DLEXT']}", # C shared object + '**/*.lock' # Bundler ) desc "Build the source (but don't install it)" From a102c5c71bfc9f8a462003a5852d185f7849e76b Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:31:28 -0500 Subject: [PATCH 07/15] Remove 3.0, add 4.0. --- .github/workflows/ruby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 24c59fa..374a7ff 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -15,7 +15,7 @@ jobs: test: strategy: matrix: - ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4'] + ruby-version: ['3.1', '3.2', '3.3', '3.4', '4.0'] platform: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: From 8783fec20e838cbbebbd7e25331259c8e498d42f Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:44:28 -0500 Subject: [PATCH 08/15] Restore bzero. --- ext/proc/wait3.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/proc/wait3.c b/ext/proc/wait3.c index d3adcef..a206190 100644 --- a/ext/proc/wait3.c +++ b/ext/proc/wait3.c @@ -912,7 +912,7 @@ static VALUE proc_getrusage(int argc, VALUE* argv, VALUE mod){ else if(RTEST(v_children)) who = RUSAGE_CHILDREN; - memset(&r, 0, sizeof(r)); + bzero(&r, sizeof(r)); if(getrusage(who,&r) == -1) rb_sys_fail("getrusage"); From 8df5ba3f1c02ce950cb24012210f98fccc7e5bfb Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:41:03 -0500 Subject: [PATCH 09/15] Fix rb_pid_t arg. --- ext/proc/wait3.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/proc/wait3.c b/ext/proc/wait3.c index a206190..86c42b0 100644 --- a/ext/proc/wait3.c +++ b/ext/proc/wait3.c @@ -258,7 +258,7 @@ static VALUE proc_wait3(int argc, VALUE *argv, VALUE mod){ VALUE scheduler = rb_fiber_scheduler_current(); if (!NIL_P(scheduler)) { /* Use fiber scheduler - wait for any child (pid = -1) */ - VALUE result = rb_fiber_scheduler_process_wait(scheduler, RB_INT2NUM(-1), RB_INT2NUM(flags)); + VALUE result = rb_fiber_scheduler_process_wait(scheduler, (rb_pid_t)-1, flags); if (NIL_P(result)) { return Qnil; @@ -357,7 +357,7 @@ static VALUE proc_wait4(int argc, VALUE *argv, VALUE mod){ VALUE scheduler = rb_fiber_scheduler_current(); if (!NIL_P(scheduler)) { /* Use fiber scheduler */ - VALUE result = rb_fiber_scheduler_process_wait(scheduler, RB_INT2NUM(pid), RB_INT2NUM(flags)); + VALUE result = rb_fiber_scheduler_process_wait(scheduler, pid, flags); if (NIL_P(result)) { return Qnil; From 0ba0587b2615edabab0e2ea866f7607fca8e360c Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:01:04 -0500 Subject: [PATCH 10/15] Add RB_GC_GUARD in spots with potential long running operations. --- ext/proc/wait3.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ext/proc/wait3.c b/ext/proc/wait3.c index 86c42b0..bc4f9fa 100644 --- a/ext/proc/wait3.c +++ b/ext/proc/wait3.c @@ -268,6 +268,8 @@ static VALUE proc_wait3(int argc, VALUE *argv, VALUE mod){ pid_t pid = NUM2PIDT(rb_funcall(result, rb_intern("pid"), 0)); int status = NUM2INT(rb_funcall(result, rb_intern("to_i"), 0)); + RB_GC_GUARD(result); + v_last_status = build_procstat_without_rusage(pid, status); rb_last_status_set(status, pid); OBJ_FREEZE(v_last_status); @@ -367,6 +369,8 @@ static VALUE proc_wait4(int argc, VALUE *argv, VALUE mod){ pid_t rpid = NUM2PIDT(rb_funcall(result, rb_intern("pid"), 0)); int status = NUM2INT(rb_funcall(result, rb_intern("to_i"), 0)); + RB_GC_GUARD(result); + v_last_status = build_procstat_without_rusage(rpid, status); rb_last_status_set(status, rpid); OBJ_FREEZE(v_last_status); @@ -783,6 +787,7 @@ static VALUE proc_pause(int argc, VALUE* argv, VALUE mod){ { struct pause_args pargs; rb_thread_call_without_gvl(pause_without_gvl, &pargs, RUBY_UBF_PROCESS, NULL); + RB_GC_GUARD(v_signals); return INT2FIX(pargs.result); /* Should always be -1 */ } } From f664a6b125e9af2d9c316a6646ca9672ecb675e4 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:47:11 -0500 Subject: [PATCH 11/15] Update string len and ptr macros. --- ext/proc/wait3.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ext/proc/wait3.c b/ext/proc/wait3.c index bc4f9fa..4de439d 100644 --- a/ext/proc/wait3.c +++ b/ext/proc/wait3.c @@ -755,10 +755,12 @@ static VALUE proc_pause(int argc, VALUE* argv, VALUE mod){ if(strlcpy(signame, StringValuePtr(v_val), max) >= max) rb_raise(rb_eArgError, "string too large"); #else - if(RSTRING(v_val)->len > max) + if(RSTRING_LEN(v_val) >= max) rb_raise(rb_eArgError, "string too large"); - else - strncpy(signame, RSTRING(v_val)->ptr, max); + else{ + strncpy(signame, RSTRING_PTR(v_val), max); + signame[max-1] = '\0'; + } #endif #ifdef HAVE_STR2SIG From 5f04cb2aaad6a48124ef0b05bddbfadf0f96a10c Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Fri, 9 Jan 2026 00:00:04 -0500 Subject: [PATCH 12/15] Add another check for strlcpy. --- ext/proc/wait3.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ext/proc/wait3.c b/ext/proc/wait3.c index 4de439d..74d9ce6 100644 --- a/ext/proc/wait3.c +++ b/ext/proc/wait3.c @@ -858,8 +858,17 @@ static VALUE proc_sigsend(int argc, VALUE* argv, VALUE mod){ char signame[SIG2STR_MAX]; unsigned int max = SIG2STR_MAX; +#ifdef HAVE_STRLCPY if(strlcpy(signame, StringValuePtr(v_signal), max) >= max) rb_raise(rb_eArgError, "string too large"); +#else + if(RSTRING_LEN(v_signal) >= max) + rb_raise(rb_eArgError, "string too large"); + else{ + strncpy(signame, RSTRING_PTR(v_signal), max); + signame[max-1] = '\0'; + } +#endif if(str2sig(signame,&sig) != 0) rb_sys_fail("str2sig"); From 19660a2a63487fa4d18077a151f94f6d45219a1c Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Fri, 9 Jan 2026 00:02:23 -0500 Subject: [PATCH 13/15] Comment fix. --- ext/proc/wait3.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/proc/wait3.c b/ext/proc/wait3.c index 74d9ce6..8e12cdc 100644 --- a/ext/proc/wait3.c +++ b/ext/proc/wait3.c @@ -721,7 +721,7 @@ static VALUE proc_waitid(int argc, VALUE* argv, VALUE mod){ * be sure to leave off the leading 'SIG' substring, e.g. use 'INT' instead * of 'SIGINT'. * - * Note that not all platforms (notably Linux) do not support automatically + * Note that not all platforms (notably Linux) support automatically * converting strings to their corresponding signal values, so it is * recommended that you always use an array of numeric values. * From 4fc1b51e9e71d33c0a9bb7999dfce02be78bb9ab Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Fri, 9 Jan 2026 00:04:19 -0500 Subject: [PATCH 14/15] Fixed some whitespace issues. --- ext/proc/wait3.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ext/proc/wait3.c b/ext/proc/wait3.c index 8e12cdc..aa5dd98 100644 --- a/ext/proc/wait3.c +++ b/ext/proc/wait3.c @@ -101,10 +101,10 @@ static void* pause_without_gvl(void* data) { */ static VALUE pst_wifstopped(int status) { - if(WIFSTOPPED(status)) - return Qtrue; - else - return Qfalse; + if(WIFSTOPPED(status)) + return Qtrue; + else + return Qfalse; } /* @@ -113,7 +113,7 @@ static VALUE pst_wifstopped(int status) static VALUE pst_wifsignaled(int status) { if (WIFSIGNALED(status)) - return Qtrue; + return Qtrue; else return Qfalse; } @@ -782,7 +782,7 @@ static VALUE proc_pause(int argc, VALUE* argv, VALUE mod){ res = sigaction(signum, &act, &sa); if(res) - rb_sys_fail("sigaction"); + rb_sys_fail("sigaction"); } } From 79f7ee64c63e914aca10385b1b4536a4bb7439c2 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:18:29 -0500 Subject: [PATCH 15/15] Add changes for 2.0.0. --- CHANGES.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a1e2f98..e61ca4d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,16 @@ +## 2.0.0 - 9-Jan-2026 +* Added fiber scheduler support for wait3 and wait4 methods, allowing them + to cooperate with Ruby's fiber scheduler for non-blocking async operations. + Note that rusage fields will be zero when using the fiber scheduler path. +* Added `rb_thread_call_without_gvl` for blocking calls so that other Ruby + threads can run while waiting. +* Added `RB_GC_GUARD` to protect Ruby objects during blocking operations. +* Replaced deprecated `RSTRING()->len` and `RSTRING()->ptr` with `RSTRING_LEN()` + and `RSTRING_PTR()` macros. +* Added missing `HAVE_STRLCPY` guard in sigsend with strncpy fallback. +* Fixed some minor whitespace inconsistencies. +* Now requires Ruby 3.1 or later. + ## 1.9.3 - 4-May-2024 * Some internal refactoring where I bzero C structs before using them. @@ -157,7 +170,7 @@ * Updated tests and documentation. ## 1.2.0 - 7-Feb-2005 -* Added the Proc.waitid method (for those platforms that support it). +* Added the Proc.waitid method (for those platforms that support it). * Made the wait3.c file more rdoc friendly. * Added a test_waitid.rb file in the examples directory.