Don't resume new threads if scheduler-locking is in effect

If scheduler-locking is in effect, like e.g., with "set
scheduler-locking on", and you step over a function that spawns a new
thread, the new thread is allowed to run free, at least until some
event is hit, at which point, whether the new thread is re-resumed
depends on a number of seemingly random factors.  E.g., if the target
is all-stop, and the parent thread hits a breakpoint, and gdb decides
the breakpoint isn't interesting to report to the user, then the
parent thread is resumed, but the new thread is left stopped.

I think that letting the new threads run with scheduler-locking
enabled is a defect.  This commit fixes that, making use of the new
clone events on Linux, and of target_thread_events() on targets where
new threads have no connection to the thread that spawned them.

Testcase and documentation changes included.

Approved-By: Eli Zaretskii <eliz@gnu.org>
Change-Id: Ie12140138b37534b7fc1d904da34f0f174aa11ce
This commit is contained in:
Pedro Alves
2021-11-30 19:52:11 +00:00
parent 859163afc6
commit 648e6605fb
5 changed files with 164 additions and 9 deletions

View File

@@ -118,6 +118,13 @@ show always-read-ctf
from the current process state. GDB will show this additional information
automatically, or through one of the memory-tag subcommands.
* Scheduler-locking and new threads
When scheduler-locking is in effect, only the current thread may run
when the inferior is resumed. However, previously, new threads
created by the resumed thread would still be able to run free. Now,
they are held stopped.
* "info breakpoints" now displays enabled breakpoint locations of
disabled breakpoints as in the "y-" state. For example:

View File

@@ -7050,7 +7050,9 @@ the following:
There is no locking and any thread may run at any time.
@item on
Only the current thread may run when the inferior is resumed.
Only the current thread may run when the inferior is resumed. New
threads created by the resumed thread are held stopped at their entry
point, before they execute any instruction.
@item step
Behaves like @code{on} when stepping, and @code{off} otherwise.

View File

@@ -104,6 +104,8 @@ static bool start_step_over (void);
static bool step_over_info_valid_p (void);
static bool schedlock_applies (struct thread_info *tp);
/* Asynchronous signal handler registered as event loop source for
when we have pending events ready to be passed to the core. */
static struct async_event_handler *infrun_async_inferior_event_token;
@@ -1892,7 +1894,13 @@ static void
update_thread_events_after_step_over (thread_info *event_thread,
const target_waitstatus &event_status)
{
if (target_supports_set_thread_options (0))
if (schedlock_applies (event_thread))
{
/* If scheduler-locking applies, continue reporting
thread-created/thread-cloned events. */
return;
}
else if (target_supports_set_thread_options (0))
{
/* We can control per-thread options. Disable events for the
event thread, unless the thread is gone. */
@@ -2465,9 +2473,14 @@ do_target_resume (ptid_t resume_ptid, bool step, enum gdb_signal sig)
to start stopped. We need to release the displaced stepping
buffer if the stepped thread exits, so we also enable
thread-exit events.
- If scheduler-locking applies, threads that the current thread
spawns should remain halted. It's not strictly necessary to
enable thread-exit events in this case, but it doesn't hurt.
*/
if (step_over_info_valid_p ()
|| displaced_step_in_progress_thread (tp))
|| displaced_step_in_progress_thread (tp)
|| schedlock_applies (tp))
{
gdb_thread_options options
= GDB_THREAD_OPTION_CLONE | GDB_THREAD_OPTION_EXIT;
@@ -2476,6 +2489,13 @@ do_target_resume (ptid_t resume_ptid, bool step, enum gdb_signal sig)
else
target_thread_events (true);
}
else
{
if (target_supports_set_thread_options (0))
tp->set_thread_options (0);
else if (!displaced_step_in_progress_any_thread ())
target_thread_events (false);
}
/* If we're resuming more than one thread simultaneously, then any
thread other than the leader is being set to run free. Clear any
@@ -6157,16 +6177,21 @@ handle_inferior_event (struct execution_control_state *ecs)
parent->set_running (false);
/* If resuming the child, mark it running. */
if (ecs->ws.kind () == TARGET_WAITKIND_THREAD_CLONED
|| (follow_child || (!detach_fork && (non_stop || sched_multi))))
if ((ecs->ws.kind () == TARGET_WAITKIND_THREAD_CLONED
&& !schedlock_applies (ecs->event_thread))
|| (ecs->ws.kind () != TARGET_WAITKIND_THREAD_CLONED
&& (follow_child
|| (!detach_fork && (non_stop || sched_multi)))))
child->set_running (true);
/* In non-stop mode, also resume the other branch. */
if ((ecs->ws.kind () == TARGET_WAITKIND_THREAD_CLONED
&& target_is_non_stop_p ())
|| (!detach_fork && (non_stop
|| (sched_multi
&& target_is_non_stop_p ()))))
&& target_is_non_stop_p ()
&& !schedlock_applies (ecs->event_thread))
|| (ecs->ws.kind () != TARGET_WAITKIND_THREAD_CLONED
&& (!detach_fork && (non_stop
|| (sched_multi
&& target_is_non_stop_p ())))))
{
if (follow_child)
switch_to_thread (parent);

View File

@@ -0,0 +1,54 @@
/* This testcase is part of GDB, the GNU debugger.
Copyright 2021-2022 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/>. */
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
static void *
thread_func (void *arg)
{
#if !SCHEDLOCK
while (1)
sleep (1);
#endif
return NULL;
}
int
main (void)
{
pthread_t thread;
int ret;
ret = pthread_create (&thread, NULL, thread_func, NULL); /* set break 1 here */
assert (ret == 0);
#if SCHEDLOCK
/* When testing with schedlock enabled, the new thread won't run, so
we can't join it, as that would hang forever. Instead, sleep for
a bit, enough that if the spawned thread is scheduled, it hits
the thread_func breakpoint before the main thread reaches the
"return 0" line below. */
sleep (3);
#else
pthread_join (thread, NULL);
#endif
return 0; /* set break 2 here */
}

View File

@@ -0,0 +1,67 @@
# Copyright 2021-2022 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/>.
# Test continuing over a thread spawn with scheduler-locking on.
standard_testfile .c
foreach_with_prefix schedlock {off on} {
set sl [expr $schedlock == "on" ? 1 : 0]
if { [build_executable "failed to prepare" $testfile-$sl \
$srcfile \
[list debug pthreads additional_flags=-DSCHEDLOCK=$sl]] \
== -1 } {
return
}
}
proc test {non-stop schedlock} {
save_vars ::GDBFLAGS {
append ::GDBFLAGS " -ex \"set non-stop ${non-stop}\""
set sl [expr $schedlock == "on" ? 1 : 0]
clean_restart $::binfile-$sl
}
set linenum1 [gdb_get_line_number "set break 1 here"]
if { ![runto $::srcfile:$linenum1] } {
return
}
delete_breakpoints
set linenum2 [gdb_get_line_number "set break 2 here"]
gdb_breakpoint $linenum2
gdb_breakpoint "thread_func"
gdb_test_no_output "set scheduler-locking $schedlock"
if {$schedlock} {
gdb_test "continue" \
"return 0.*set break 2 here .*" \
"continue does not stop in new thread"
} else {
gdb_test "continue" \
"thread_func .*" \
"continue stops in new thread"
}
}
foreach_with_prefix non-stop {off on} {
foreach_with_prefix schedlock {off on} {
test ${non-stop} ${schedlock}
}
}