]> git.ipfire.org Git - thirdparty/binutils-gdb.git/blobdiff - gdb/testsuite/gdb.threads/slow-waitpid.c
gdb: Don't drop SIGSTOP during stop_all_threads
[thirdparty/binutils-gdb.git] / gdb / testsuite / gdb.threads / slow-waitpid.c
diff --git a/gdb/testsuite/gdb.threads/slow-waitpid.c b/gdb/testsuite/gdb.threads/slow-waitpid.c
new file mode 100644 (file)
index 0000000..93304ef
--- /dev/null
@@ -0,0 +1,342 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2018 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+/* This file contains a library that can be preloaded into GDB on Linux
+   using the LD_PRELOAD technique.
+
+   The library intercepts calls to WAITPID and SIGSUSPEND in order to
+   simulate the behaviour of a heavily loaded kernel.
+
+   When GDB wants to stop all threads in an inferior each thread is sent a
+   SIGSTOP, GDB will then wait for the signal to be received by the thread
+   with a waitpid call.
+
+   If the kernel is slow in either delivering the signal, or making the
+   result available to the waitpid call then GDB will enter a sigsuspend
+   call in order to wait for the inferior threads to change state, this is
+   signalled to GDB with a SIGCHLD.
+
+   A bug in GDB meant that in some cases we would deadlock during this
+   process.  This was rarely seen as the kernel is usually quick at
+   delivering signals and making the results available to waitpid, so quick
+   that GDB would gather the statuses from all inferior threads in the
+   original pass.
+
+   The idea in this library is to rate limit calls to waitpid (where pid is
+   -1 and the WNOHANG option is set) so that only 1 per second can return
+   an answer.  Any additional calls will report that no threads are
+   currently ready.  This should match the behaviour we see on a slow
+   kernel.
+
+   However, given that usually when using this library, the kernel does
+   have the waitpid result ready this means that the kernel will never send
+   GDB a SIGCHLD.  This means that when GDB enters sigsuspend it will block
+   forever.  Alternatively, if GDB enters its polling loop the lack of
+   SIGCHLD means that we will never see an event on the child threads.  To
+   resolve these problems the library intercepts calls to sigsuspend and
+   forces the call to exit if there is a pending waitpid result.  Also,
+   when we know that there's a waitpid result that we've ignored, we create
+   a new thread which, after a short delay, will send GDB a SIGCHLD.  */
+
+#define _GNU_SOURCE
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/time.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <dlfcn.h>
+#include <string.h>
+#include <stdarg.h>
+#include <signal.h>
+#include <errno.h>
+#include <pthread.h>
+#include <unistd.h>
+
+/* Logging.  */
+
+static void
+log_msg (const char *fmt, ...)
+{
+#ifdef LOGGING
+  va_list ap;
+
+  va_start (ap, fmt);
+  vfprintf (stderr, fmt, ap);
+  va_end (ap);
+#endif /* LOGGING */
+}
+
+/* Error handling, message and exit.  */
+
+static void
+error (const char *fmt, ...)
+{
+  va_list ap;
+
+  va_start (ap, fmt);
+  vfprintf (stderr, fmt, ap);
+  va_end (ap);
+
+  exit (EXIT_FAILURE);
+}
+
+/* Cache the result of a waitpid call that has not been reported back to
+   GDB yet.  We only ever cache a single result.  Once we have a result
+   cached then later calls to waitpid with the WNOHANG option will return a
+   result of 0.  */
+
+static struct
+{
+  /* Flag to indicate when we have a result cached.  */
+  int cached_p;
+
+  /* The cached result fields from a waitpid call.  */
+  pid_t pid;
+  int wstatus;
+} cached_wait_status;
+
+/* Lock to hold when modifying SIGNAL_THREAD_ACTIVE_P.  */
+
+static pthread_mutex_t thread_creation_lock_obj = PTHREAD_MUTEX_INITIALIZER;
+#define thread_creation_lock (&thread_creation_lock_obj)
+
+/* This flag is only modified while holding the THREAD_CREATION_LOCK mutex.
+   When this flag is true then there is a signal thread alive that will be
+   sending a SIGCHLD at some point in the future.  */
+
+static int signal_thread_active_p;
+
+/* When we last allowed a waitpid to complete.  */
+
+static struct timeval last_waitpid_time = { 0, 0 };
+
+/* The number of seconds that must elapse between calls to waitpid where
+   the pid is -1 and the WNOHANG option is set.  If calls occur faster than
+   this then we force a result of 0 to be returned from waitpid.  */
+
+#define WAITPID_MIN_TIME (1)
+
+/* Return true (non-zero) if we should skip this call to waitpid, or false
+   (zero) if this waitpid call should be handled with a call to the "real"
+   waitpid function.  Allows 1 waitpid call per second.  */
+
+static int
+should_skip_waitpid (void)
+{
+  struct timeval *tv = &last_waitpid_time;
+  if (tv->tv_sec == 0)
+    {
+      if (gettimeofday (tv, NULL) < 0)
+       error ("error: gettimeofday failed\n");
+      return 0; /* Don't skip.  */
+    }
+  else
+    {
+      struct timeval new_tv;
+
+      if (gettimeofday (&new_tv, NULL) < 0)
+       error ("error: gettimeofday failed\n");
+
+      if ((new_tv.tv_sec - tv->tv_sec) < WAITPID_MIN_TIME)
+       return 1; /* Skip.  */
+
+      *tv = new_tv;
+    }
+
+  /* Don't skip.  */
+  return 0;
+}
+
+/* Perform a real waitpid call.  */
+
+static pid_t
+real_waitpid (pid_t pid, int *wstatus, int options)
+{
+  typedef pid_t (*fptr_t) (pid_t, int *, int);
+  static fptr_t real_func = NULL;
+
+  if (real_func == NULL)
+    {
+      real_func = dlsym (RTLD_NEXT, "waitpid");
+      if (real_func == NULL)
+       error ("error: failed to find real waitpid\n");
+    }
+
+  return (*real_func) (pid, wstatus, options);
+}
+
+/* Thread worker created when we cache a waitpid result.  Delays for a
+   short period of time and then sends SIGCHLD to the GDB process.  This
+   should trigger GDB to call waitpid again, at which point we will make
+   the cached waitpid result available.  */
+
+static void*
+send_sigchld_thread (void *arg)
+{
+  /* Delay one second longer than WAITPID_MIN_TIME so that there can be no
+     chance that a call to SHOULD_SKIP_WAITPID will return true once the
+     SIGCHLD is delivered and handled.  */
+  sleep (WAITPID_MIN_TIME + 1);
+
+  pthread_mutex_lock (thread_creation_lock);
+  signal_thread_active_p = 0;
+
+  if (cached_wait_status.cached_p)
+    {
+      log_msg ("signal-thread: sending SIGCHLD\n");
+      kill (getpid (), SIGCHLD);
+    }
+
+  pthread_mutex_unlock (thread_creation_lock);
+  return NULL;
+}
+
+/* The waitpid entry point function.  */
+
+pid_t
+waitpid (pid_t pid, int *wstatus, int options)
+{
+  log_msg ("waitpid: waitpid (%d, %p, 0x%x)\n", pid, wstatus, options);
+
+  if ((options & WNOHANG) != 0
+      && pid == -1
+      && should_skip_waitpid ())
+    {
+      if (!cached_wait_status.cached_p)
+       {
+         /* Do the waitpid call, but hold the result back.  */
+         pid_t tmp_pid;
+         int tmp_wstatus;
+
+         tmp_pid = real_waitpid (-1, &tmp_wstatus, options);
+         if (tmp_pid > 0)
+           {
+             log_msg ("waitpid: delaying waitpid result (pid = %d)\n",
+                      tmp_pid);
+
+             /* Cache the result.  */
+             cached_wait_status.pid = tmp_pid;
+             cached_wait_status.wstatus = tmp_wstatus;
+             cached_wait_status.cached_p = 1;
+
+             /* Is there a thread around that will be sending a signal in
+                the near future?  The prevents us from creating one
+                thread per call to waitpid when the calls occur in a
+                sequence.  */
+             pthread_mutex_lock (thread_creation_lock);
+             if (!signal_thread_active_p)
+               {
+                 sigset_t old_ss, new_ss;
+                 pthread_t thread_id;
+                 pthread_attr_t attr;
+
+                 /* Create the new signal sending thread in detached
+                    state.  This means that the thread doesn't need to be
+                    pthread_join'ed.  Which is fine as there's no result
+                    we care about.  */
+                 pthread_attr_init (&attr);
+                 pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
+
+                 /* Ensure the signal sending thread has all signals
+                    blocked.  We don't want any signals to GDB to be
+                    handled in that thread.  */
+                 sigfillset (&new_ss);
+                 sigprocmask (SIG_BLOCK, &new_ss, &old_ss);
+
+                 log_msg ("waitpid: spawn thread to signal us\n");
+                 if (pthread_create (&thread_id, &attr,
+                                     send_sigchld_thread, NULL) != 0)
+                   error ("error: pthread_create failed\n");
+
+                 signal_thread_active_p = 1;
+                 sigprocmask (SIG_SETMASK, &old_ss, NULL);
+                 pthread_attr_destroy (&attr);
+               }
+
+             pthread_mutex_unlock (thread_creation_lock);
+           }
+       }
+
+      log_msg ("waitpid: skipping\n");
+      return 0;
+    }
+
+  /* If we have a cached result that is a suitable reply for this call to
+     waitpid then send that cached result back now.  */
+  if (cached_wait_status.cached_p
+      && (pid == -1 || pid == cached_wait_status.pid))
+    {
+      pid_t pid;
+
+      pid = cached_wait_status.pid;
+      log_msg ("waitpid: return cached result (%d)\n", pid);
+      *wstatus = cached_wait_status.wstatus;
+      cached_wait_status.cached_p = 0;
+      return pid;
+    }
+
+  log_msg ("waitpid: real waitpid call\n");
+  return real_waitpid (pid, wstatus, options);
+}
+
+/* Perform a real sigsuspend call.  */
+
+static int
+real_sigsuspend (const sigset_t *mask)
+{
+  typedef int (*fptr_t) (const sigset_t *);
+  static fptr_t real_func = NULL;
+
+  if (real_func == NULL)
+    {
+      real_func = dlsym (RTLD_NEXT, "sigsuspend");
+      if (real_func == NULL)
+       error ("error: failed to find real sigsuspend\n");
+    }
+
+  return (*real_func) (mask);
+}
+
+/* The sigsuspend entry point function.  */
+
+int
+sigsuspend (const sigset_t *mask)
+{
+  log_msg ("sigsuspend: sigsuspend (0x%p)\n", ((void *) mask));
+
+  /* If SIGCHLD is _not_ in MASK, and is therefore deliverable, then if we
+     have a pending wait status pretend that a signal arrived.  We will
+     have a thread alive that is going to deliver a signal but doing this
+     will boost the speed as we don't have to wait for a signal.  If the
+     signal ends up being delivered then it should be harmless, we'll just
+     perform an additional waitpid call.   */
+  if (!sigismember (mask, SIGCHLD))
+    {
+      if (cached_wait_status.cached_p)
+       {
+         log_msg ("sigsuspend: interrupt for cached waitstatus\n");
+         last_waitpid_time.tv_sec = 0;
+         last_waitpid_time.tv_usec = 0;
+         errno = EINTR;
+         return -1;
+       }
+    }
+
+  log_msg ("sigsuspend: real sigsuspend call\n");
+  return real_sigsuspend (mask);
+}