Задний план

У меня есть многокомпонентная кодовая база на C ++. Есть один центральный компонент, который включает основной исполняемый файл, и есть несколько компонентов, которые компилируются в динамические модули (файлы .so). Центральный исполняемый файл может загружать и выгружать их во время выполнения (горячая замена, если хотите).

Есть один файл с именем Scheduler.h, который объявляет класс Scheduler, который предоставляет синхронные события в определенное время или через определенные промежутки времени, и несколько вспомогательных классов, которые используются для выполнения запросов к планировщику. Существует класс Event, который содержит данные о времени, и абстрактный класс action с единственной чистой виртуальной функцией DoEvent. Существует также Scheduler.cpp, который содержит определения для большинства функций Scheduler.h (за исключением классов шаблонов, которые объявлены и определены в файле заголовка).

Event владеет указателем на подкласс action, которым управляют функциональные возможности планировщика. Scheduler.h сам предоставляет некоторые из этих подклассов.

action объявляется так:

class action
{
    action();
    virtual ~action();
    virtual DoEvent() = 0;
};

FunctionCallAction, подкласс action объявляется и определяется следующим образом:

template <class R, class T>
class FunctionCallAction : public action
{
public:
    FunctionCallAction(R (*f)(T), T arg) : argument(arg), callback(f) {}
    ~FunctionCallAction() {}
    void DoEvent() { function(argument); }
private:
    R (*callback)(T);
    T argument;
};

HelloAction, другой подкласс, объявляется следующим образом:

// In Scheduler.h
class HelloAction : public action
{
    ~HelloAction();
    void DoEvent();
};

// in Scheduler.cpp
HelloAction::~HelloAction() {}
void HelloAction::DoEvent() { cout << "Hello world" << endl; }

Одна из моих динамических библиотек, CloneWatch, объявленная в CloneWatch.h и определенная в CloneWatch.cpp, использует эту службу планировщика. В своем конструкторе он создает постоянное событие, запускаемое каждые 300 секунд. В своем деструкторе он удаляет это событие. Когда этот модуль загружен, он получает ссылку на существующий объект планировщика. Процесс "загрузки" модуля влечет за собой использование dlopen() для открытия библиотеки, dlsym() для поиска фабричного метода (метко названного Factory) и использования этого фабричного метода для создания экземпляр некоторого объекта (семантика не имеет значения). Чтобы закрыть библиотеку, объект, созданный фабричным методом, удаляется, и вызывается dlclose() для удаления библиотеки из адресного пространства процесса.

Загрузка и выгрузка библиотек во время выполнения контролируется командой.

// relevant declarations
const float DB_CLEAN_FREQ = 300;
event_t cleanerevent; // event_t is a typedef to an integral type
void * RunDBCleaner(void *); // static function of CloneWatch
Scheduler& scheduler;

// in constructor:
Event e(DB_CLEAN_FREQ, -1, new FunctionCallAction<void *, void *>(CloneWatch::RunDBCleaner, (void *) this));
cleanerevent = scheduler.AddEvent(e);

// in destructor:
scheduler.RemoveEvent(cleanerevent);

Scheduler::RemoveEvent ленив. Вместо того, чтобы обходить всю очередь приоритетов событий, он поддерживает набор «отмененных событий». Если в ходе обработки события он извлекает из своей очереди событие с идентификатором, который соответствует идентификатору в его наборе отмененных событий, событие не запускается и не переносится в расписание и немедленно удаляется. Процессы очистки события влекут за собой удаление принадлежащего ему объекта action.

Проблема

Проблема, с которой я столкнулся, заключается в том, что программный сегмент неисправен. Ошибка возникает внутри цикла событий планировщика, который выглядит примерно так:

while (!eventqueue.empty() && e.Due())
{
    Event e = eventqueue.top();
    eventqueue.pop();
    if (cancelled.find(e.GetID()) != cancelled.end())
    {
        cancelled.erase(e.GetID());
        e.Cancel();
        continue;
    }

    QueueUnlock();
    e.DoEvent();
    QueueLock();

    e.Next();

    if (e.ShouldReschedule()) eventqueue.push(e);
}

Вызов e.Cancel удаляет действие события. Вызов e.Next может удалить действие события (только если событие само по себе истекло. В этом случае e.ShouldReschedule вернет false, и событие будет отброшено). В целях тестирования я добавил несколько операторов печати к деструкторам класса действий и подклассов, чтобы увидеть, что происходит.

Кикер

Если событие удалено из e.Next по истечении срока действия, все будет происходить как обычно. Однако, когда я выгружаю модуль, в результате чего событие удаляется из списка отмененных, в программе возникает ошибка сегментации, как только вызывается деструктор действия. Это происходит через некоторое время после выгрузки модуля из-за ленивого удаления событий планировщиком.

Он не входит ни в один из деструкторов, а сразу дает сбой. Я пробовал различные комбинации управляемого и неуправляемого удаления действия события, а также делал это в разных местах и ​​разными способами. Я пропустил его через valgrind и gdb, но они оба просто вежливо сообщают мне, что произошла ошибка сегментации, и, хоть убей, я не могу понять, почему (хотя я не знаю, как использовать любой из них очень хорошо) .

Если я также вызываю e.Cancel в основном теле цикла, вызывая удаление, и закомментирую строку, которая изменяет график события, тем самым заставляя событие быть отменено сразу после его выполнения, ошибка не возникает. .

Я также заменил действие на HelloAction, но в этом нет ничего плохого. Что-то очень специфическое в деструкторе FunctionCallAction, по-видимому, и заключается проблема. Я более или менее устранил семантическую ошибку, и подозреваю, что это результат неясного поведения компилятора или динамического компоновщика. Кто-нибудь видит проблему?

4
Wug 22 Авг 2012 в 19:22
Я решил, что поделюсь этим, потому что мне потребовалось слишком много времени, чтобы отследить, что было не так. Если у кого-то есть предложения по добавлению Google-Fu в заголовок, не стесняйтесь предлагать / редактировать.
 – 
Wug
22 Авг 2012 в 19:26

1 ответ

Лучший ответ

Это поведение компилятора.

Проблема в том, что FunctionCallAction определен (а не просто объявлен) в его заголовочном файле. Это необходимый побочный эффект того, что он является классом-шаблоном, однако объявление обычного класса с функциональностью FunctionCallAction<void *, void *> дает те же результаты ЕСЛИ класс определен в файле заголовка.

Это было банальным ограничением для классов шаблонов, вызывающих непредвиденные побочные эффекты в необычных обстоятельствах.

Причина в том, что если определение класса находится в файле заголовка, оно компилируется в каждый файл, который его использует. Поскольку я использую его из кода моей динамической библиотеки, именно здесь он компилируется. Таким образом, когда библиотека выгружается, код деструкторов и всего остального класса больше не существует.

Я решил эту проблему, создав класс FunctionCallAction без шаблона, оставив только его объявление в Scheduler.h и переместив его определение в Scheduler.cpp. Таким образом, функции предоставляются постоянно загруженным исполняемым файлом ядра, а не индивидуально динамическими модулями.

Вызов деструктора действия был ошибкой сегмента, потому что сам деструктор больше не был частью адресного пространства процесса.

5
Community 20 Июн 2020 в 12:12
Я был бы готов принять другой ответ, если кто-то опубликует тот, который сформулирован лучше, чем мой.
 – 
Wug
22 Авг 2012 в 19:35