Я пишу программу, которая использует Linux perf API для измерения количества инструкций, выполняемых программой, и хочу получить уведомление, как только дочерний процесс достигнет заданного количества инструкций. Perf предоставляет способ сделать это с помощью уведомлений о переполнении, и согласно его официальной странице руководства уведомления о переполнении могут быть получены двумя способами:
Overflow handling
Events can be set to notify when a threshold is crossed,
indicating an overflow. Overflow conditions can be captured by
monitoring the event file descriptor with poll(2), select(2), or
epoll(7). Alternatively, the overflow events can be captured via
sa signal handler, by enabling I/O signaling on the file
descriptor; see the discussion of the F_SETOWN and F_SETSIG
operations in fcntl(2).
Включение сигнализации ввода-вывода в дескрипторе файла perf и установка обработчика сигнала для меня не вариант из-за особенностей моей программы, поэтому я хотел бы использовать epoll
в дескрипторе файла. Однако epoll_wait
продолжает возвращать EPOLLHUP
(даже если уведомление о переполнении не должно отправляться) и никогда не возвращает EPOLLIN
, который должен. Я прочитал практически всю доступную в Интернете информацию об этом варианте использования perf_event_open
, но до сих пор не смог понять, почему это происходит.
Вот упрощенная версия исходного кода, с которым я работаю:
#include <bits/stdc++.h>
#include <linux/perf_event.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
char* stringToCStr(const string &str) {
char* cStr = new char[str.size() + 1];
copy(str.begin(), str.end(), cStr);
cStr[str.size()] = '\0';
return cStr;
}
static long perf_event_open(struct perf_event_attr *hw_event, pid_t pid, int cpu, int group_fd, unsigned long flags) {
return syscall(SYS_perf_event_open, hw_event, pid, cpu, group_fd, flags);
}
long long get_instructions_used(int perf_fd) {
long long int instructionsUsed;
int size = read(perf_fd, &instructionsUsed, sizeof(long long));
if (size != sizeof(instructionsUsed)) {
cout << "read failed";
exit(0);
}
if (instructionsUsed < 0) {
cout << "read negative instructions count";
exit(0);
}
return static_cast<uint64_t>(instructionsUsed);
}
int main() {
pthread_barrier_t *barrier_ = (pthread_barrier_t*) mmap(nullptr, sizeof(pthread_barrier_t), PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, 0, 0);
pthread_barrierattr_t attr;
pthread_barrierattr_init(&attr);
pthread_barrierattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_barrier_init(barrier_, &attr, 2);
pthread_barrierattr_destroy(&attr);
int pid = fork();
if (pid == 0) {
pthread_barrier_wait(barrier_);
munmap(barrier_, sizeof(pthread_barrier_t));
auto programName = stringToCStr("test");
char** programArgv = new char*[2];
programArgv[0] = programName;
programArgv[1] = nullptr;
execv(programName, programArgv);
};
struct perf_event_attr attrs {};
memset(&attrs, 0, sizeof(attrs));
attrs.type = PERF_TYPE_HARDWARE;
attrs.size = sizeof(attrs);
attrs.config = PERF_COUNT_HW_INSTRUCTIONS;
attrs.exclude_user = 0;
attrs.exclude_kernel = 1;
attrs.exclude_hv = 1;
attrs.disabled = 1;
attrs.enable_on_exec = 1;
attrs.inherit = 1;
attrs.sample_period = 500000000LL;
attrs.wakeup_events = 1;
int perf_fd = perf_event_open(&attrs, pid, -1, -1, PERF_FLAG_FD_NO_GROUP | PERF_FLAG_FD_CLOEXEC);
pthread_barrier_wait(barrier_);
pthread_barrier_destroy(barrier_);
munmap(barrier_, sizeof(pthread_barrier_t));
// Uncomment to enable capturing events via signal handler
/*int my_pid = getpid();
fcntl(perf_fd, F_SETOWN, my_pid);
int old_flags = fcntl(perf_fd, F_GETFL, 0);
fcntl(perf_fd, F_SETFL, old_flags | O_ASYNC);*/
int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
struct epoll_event event;
event.events = EPOLLIN;
event.data.u64 = 1;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, perf_fd, &event);
while(true) {
struct epoll_event events[1];
epoll_wait(epoll_fd, events, 1, -1);
cout << "event: " << events[0].events << ", u64: " << event.data.u64 << ", instruction count: " << get_instructions_used(perf_fd) << "\n";
if (events[0].events == 1)
break;
}
return 0;
}
Тестовый исполняемый файл, запускаемый дочерним элементом и заданный в переменной programName
, может быть любой простой программой, использующей множество инструкций, например скомпилированный исполняемый файл следующей программы:
#include "bits/stdc++.h"
using namespace std;
int main() {
int n = 1e8, primeCount = 0;
vector<bool> sieve(n + 1, true);
for(int i = 2; i <= n; i++) {
if (sieve[i]) {
for(int j = i * 2; j <= n; j += i)
sieve[j] = false;
primeCount++;
}
}
cout << primeCount << "\n";
return 0;
}
Этот код продолжает распечатывать event: 16, u64: 1, instruction count: <instruction count>
, что означает, что, как объяснялось выше, epoll продолжает получать EPOLLHUP
от perf_fd
. Я посчитал, что такое поведение происходит потому, что мое событие perf сначала отключено и включается только тогда, когда запускается дочерний процесс execv
(на что указывает enable_on_exec
), а это означает, что в течение короткого периода времени epoll
может опрашивать perf_fd
, пока он отключен, но это все еще не объясняет, почему дескриптор файла продолжает возвращать EPOLLHUP
даже после запуска execv
, и поведение не исчезает, если я добавляю короткий usleep
перед epoll_ctl
, заставляя его запускаться после включения события perf.
Кроме того, что любопытно, если вы раскомментируете закомментированные строки над epoll_create1
, которые вызывают уведомления о переполнении с использованием сигнала SIGIO (другой метод, упомянутый в разделе обработки переполнения), сигнал отправляется в правильное время, и процесс завершается, однако Я сказал, что не могу использовать это в своем окончательном коде. Однако это означает, что проблема каким-то образом связана с epoll
, поскольку уведомления о переполнении правильно отправляются в файловом дескрипторе, epoll
просто не видит их и вместо этого спамит epoll_wait
с помощью EPOLLHUP
.
🤔 А знаете ли вы, что...
Язык C++ поддерживает метапрограммирование с использованием шаблонов.
Мне удалось решить проблему. Как оказалось, это сочетание множества разных проблем. Прежде всего, кажется, что если вы хотите epoll
perf_event_open
fd, вам нужно сначала выделить для него буфер mmap (в отличие от того, когда вы устанавливаете флаг O_ASYNC
, который не требует этого). Мне кажется, это противоречит этому разделу справочной страницы perf_event_open
:
Before Linux 2.6.39, there is a bug that means you must allocate
an mmap ring buffer when sampling even if you do not plan to
access it.
Однако, возможно, я неправильно понимаю этот раздел, или, альтернативно, на странице руководства есть ошибка, или это ранее не обнаруженная ошибка в ядре. Я постараюсь в ближайшее время сообщить об этом поведении и посмотреть, что из этого выйдет.
Вторая проблема, которую я обнаружил, заключается в том, что вы не можете выделить буфер mmap (что не позволяет вам использовать epoll
на fd), если у вас установлен оба флага inherit
в perf_event_attr
и cpu = -1
при вызове perf_event_open
. Кажется, это ранее недокументированное поведение, вызванное следующими строками из функции perf_mmap
в файле kernel/events/core.c
ядра Linux:
/*
* Don't allow mmap() of inherited per-task counters. This would
* create a performance issue due to all children writing to the
* same rb.
*/
if (event->cpu == -1 && event->attr.inherit)
return -EINVAL;