mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-08-28 07:33:45 -04:00
When `perf record` samples a multi-threaded process and one of the target threads exits during the session, perf itself may start burning 100% CPU (up to 200% across two cores) until the session ends. A single dead fd is sufficient to trigger this; it can be reproduced with 15 pthreads in a compute loop where one thread exits halfway through. The root cause is two independent instances of the same defect: dead perf_event ring-buffer fds are left in a pollfd array. When a monitored thread exits, the kernel closes its ring-buffer fd, which then returns POLLHUP. POSIX specifies that poll() always reports POLLHUP and POLLERR regardless of the events mask, so any dead fd left in the array makes poll() return immediately every time, spinning in a tight loop: 3 seconds: 256,600 poll() calls, 0 context switches, only 21 write() Woken up count goes from ~0 to 1,300,000+ There are two affected poll paths, fixed together here: 1. Record main loop, via fdarray__filter() (tools/lib/api/fd/array.c). Since commit59b4412f27("libperf: Avoid internal moving of fdarray fds") it only zeroes events/revents without setting fd to -1, so poll() keeps reporting POLLHUP for the entry. Setting fd = -1 makes poll() skip it, matching the pattern already used in the control-fd path at tools/perf/builtin-record.c:1673. 2. BPF sideband thread, perf_evlist__poll_thread() (tools/perf/util/sideband_evlist.c). This thread polls for PERF_RECORD_BPF_EVENT but, unlike the main record loop, never calls fdarray__filter() at all, so dead fds accumulate forever and it spins at 100% CPU: Before fix: dJiffies=101, wchan=0 (running) After fix: dJiffies=0, wchan=do_sys_poll (blocking) Fixed by calling the existing evlist__filter_pollfd() helper after evlist__poll(), mirroring the main record loop. <poll.h> is included for the POLLERR/POLLHUP macros (previously unused there). The two fixes compose: fix 1 makes poll() ignore dead fds (fd=-1); fix 2 ensures the sideband thread actually performs the filtering. Both paths are affected in all kernels from v5.1/v5.9 to the current master (7.2-rc1); the source of both functions is byte-identical across them. BPF event recording is preserved: after the fix, perf.data still contains PERF_RECORD_BPF_EVENT records and bpf_prog_info entries. Verified on perf 6.1.76, 6.6.143 and 7.2-rc1 with a minimal reproducer (Woken up 1,300,000 -> 3, CPU 100% -> 0%) and an A/B orthogonal test: keeping the unpatched binary but preventing the target thread from exiting also makes the storm disappear, confirming the trigger. Fixes:59b4412f27("libperf: Avoid internal moving of fdarray fds") Fixes:657ee55319("perf evlist: Introduce side band thread") Signed-off-by: Jiawei Sun <abyssmystery@gmail.com> Reviewed-by: Ian Rogers <irogers@google.com> Signed-off-by: Namhyung Kim <namhyung@kernel.org>
156 lines
3.2 KiB
C
156 lines
3.2 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Copyright (C) 2014, Red Hat Inc, Arnaldo Carvalho de Melo <acme@redhat.com>
|
|
*/
|
|
#include "array.h"
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <poll.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
|
|
void fdarray__init(struct fdarray *fda, int nr_autogrow)
|
|
{
|
|
fda->entries = NULL;
|
|
fda->priv = NULL;
|
|
fda->nr = fda->nr_alloc = 0;
|
|
fda->nr_autogrow = nr_autogrow;
|
|
}
|
|
|
|
int fdarray__grow(struct fdarray *fda, int nr)
|
|
{
|
|
struct priv *priv;
|
|
int nr_alloc = fda->nr_alloc + nr;
|
|
size_t psize = sizeof(fda->priv[0]) * nr_alloc;
|
|
size_t size = sizeof(struct pollfd) * nr_alloc;
|
|
struct pollfd *entries = realloc(fda->entries, size);
|
|
|
|
if (entries == NULL)
|
|
return -ENOMEM;
|
|
|
|
priv = realloc(fda->priv, psize);
|
|
if (priv == NULL) {
|
|
free(entries);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
memset(&entries[fda->nr_alloc], 0, sizeof(struct pollfd) * nr);
|
|
memset(&priv[fda->nr_alloc], 0, sizeof(fda->priv[0]) * nr);
|
|
|
|
fda->nr_alloc = nr_alloc;
|
|
fda->entries = entries;
|
|
fda->priv = priv;
|
|
return 0;
|
|
}
|
|
|
|
struct fdarray *fdarray__new(int nr_alloc, int nr_autogrow)
|
|
{
|
|
struct fdarray *fda = calloc(1, sizeof(*fda));
|
|
|
|
if (fda != NULL) {
|
|
if (fdarray__grow(fda, nr_alloc)) {
|
|
free(fda);
|
|
fda = NULL;
|
|
} else {
|
|
fda->nr_autogrow = nr_autogrow;
|
|
}
|
|
}
|
|
|
|
return fda;
|
|
}
|
|
|
|
void fdarray__exit(struct fdarray *fda)
|
|
{
|
|
free(fda->entries);
|
|
free(fda->priv);
|
|
fdarray__init(fda, 0);
|
|
}
|
|
|
|
void fdarray__delete(struct fdarray *fda)
|
|
{
|
|
fdarray__exit(fda);
|
|
free(fda);
|
|
}
|
|
|
|
int fdarray__add(struct fdarray *fda, int fd, short revents, enum fdarray_flags flags)
|
|
{
|
|
int pos = fda->nr;
|
|
|
|
if (fda->nr == fda->nr_alloc &&
|
|
fdarray__grow(fda, fda->nr_autogrow) < 0)
|
|
return -ENOMEM;
|
|
|
|
fda->entries[fda->nr].fd = fd;
|
|
fda->entries[fda->nr].events = revents;
|
|
fda->priv[fda->nr].flags = flags;
|
|
fda->nr++;
|
|
return pos;
|
|
}
|
|
|
|
int fdarray__dup_entry_from(struct fdarray *fda, int pos, struct fdarray *from)
|
|
{
|
|
struct pollfd *entry;
|
|
int npos;
|
|
|
|
if (pos >= from->nr)
|
|
return -EINVAL;
|
|
|
|
entry = &from->entries[pos];
|
|
|
|
npos = fdarray__add(fda, entry->fd, entry->events, from->priv[pos].flags);
|
|
if (npos >= 0)
|
|
fda->priv[npos] = from->priv[pos];
|
|
|
|
return npos;
|
|
}
|
|
|
|
int fdarray__filter(struct fdarray *fda, short revents,
|
|
void (*entry_destructor)(struct fdarray *fda, int fd, void *arg),
|
|
void *arg)
|
|
{
|
|
int fd, nr = 0;
|
|
|
|
if (fda->nr == 0)
|
|
return 0;
|
|
|
|
for (fd = 0; fd < fda->nr; ++fd) {
|
|
if (!fda->entries[fd].events)
|
|
continue;
|
|
|
|
if (fda->entries[fd].revents & revents) {
|
|
if (entry_destructor)
|
|
entry_destructor(fda, fd, arg);
|
|
|
|
/*
|
|
* Set fd to -1 so poll() ignores this entry; otherwise
|
|
* POLLHUP/POLLERR are still reported for events=0 fds
|
|
* (POSIX: always checked), causing a poll storm.
|
|
*/
|
|
fda->entries[fd].fd = -1;
|
|
fda->entries[fd].revents = fda->entries[fd].events = 0;
|
|
continue;
|
|
}
|
|
|
|
if (!(fda->priv[fd].flags & fdarray_flag__nonfilterable))
|
|
++nr;
|
|
}
|
|
|
|
return nr;
|
|
}
|
|
|
|
int fdarray__poll(struct fdarray *fda, int timeout)
|
|
{
|
|
return poll(fda->entries, fda->nr, timeout);
|
|
}
|
|
|
|
int fdarray__fprintf(struct fdarray *fda, FILE *fp)
|
|
{
|
|
int fd, printed = fprintf(fp, "%d [ ", fda->nr);
|
|
|
|
for (fd = 0; fd < fda->nr; ++fd)
|
|
printed += fprintf(fp, "%s%d", fd ? ", " : "", fda->entries[fd].fd);
|
|
|
|
return printed + fprintf(fp, " ]");
|
|
}
|