Как безопасно обновить работающую динамическую библиотеку c? - PullRequest
1 голос
/ 21 марта 2020

Я реализую механизм перезагрузки живого кода для программы в C, и у меня есть такая функция:

#include <sys/types.h>
#include <sys/stat.h>
#include <dlfcn.h>

void module_load(mod_t *mod) {
    struct stat statbuf;
    if (stat(mod->path, &statbuf) != 0) {
        // ...
    }
    if (statbuf.st_mtime != mod->time) {
        if (mod->code != NULL) {  // THIS here seems unsafe
            dlclose(mod->code);
        }
        mod->code = dlopen(mod->path, RTLD_GLOBAL | RTLD_LAZY);
        if (mod->code != NULL) {
            mod->foo = dlsym(mod->code, "foo");
            mod->bar = dlsym(mod->code, "bar");
            // ...
            mod->time = statbuf.st_mtime;
        }
    }
}

И мои функции называются так:

mod->foo();
mod->bar();

Система работает нормально, функции корректно обновляются, но меня что-то беспокоит. Функция module_load выполняется в al oop в отдельном потоке, поэтому обновление может произойти в любое время, и, хотя оно работает нормально, мне интересно, какие странные вещи произойдут, если библиотека будет обновлена, а функция из нее будучи вызванным.

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

Я попытался временно получить две «живые» копии библиотеки одновременно, чтобы старые функции могли все еще использоваться, пока загружались новые, но код продолжал использовать старые функции, независимо от того, какие изменения я сделал.

Как можно безопасно перезагрузить библиотеку и ее функции, желательно в отдельном виде нить?


Чтобы сохранить две живые копии, я попытался сделать что-то вроде этого:

...
void *new_code = dlopen(mod->path, RTLD_GLOBAL | RTLD_LAZY);
if (new_code != NULL) {
    mod->foo = dlsym(new_code, "foo");
    mod->bar = dlsym(new_code, "bar");
    // ...
    void *tmp = mod->code;
    mod->code = new_code;
    if (tmp != NULL) {
        dlclose(tmp);
    }
    mod->time = statbuf.st_mtime;

1 Ответ

3 голосов
/ 21 марта 2020

dlclose(), который вы помечаете как возможно небезопасный, на самом деле определенно небезопасен. Любые функции из модуля, который в настоящее время активен в других потоках, будут извлекать слово из-под них: их ссылки на данные stati c и другие функции в модуле станут висячими указателями.

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

  • Скомпилируйте ваши модули в файлы с включенным номером версии, а затем поставьте символическую ссылку на официальное имя файла модуля для последней версии. (Именно так большинство .so файлов генерируются в типичной системе Unix.)

  • Когда вы хотите открыть модуль, сначала используйте readlink(2), чтобы найти текущий версия связанного модуля. Затем откройте этот путь.

(Я на самом деле не пробовал этого, но думаю, что он будет работать, по крайней мере, на Unix -подобных системах.)

I предложил бы попытаться избежать RTLD_GLOBAL, если это возможно. В общем, dlclose модуль, открытый с помощью RTLD_GLOBAL, по крайней мере, рискован; dlclose может потерять разрешенные символы, используемые другими динамически загружаемыми модулями. (И если никакой другой модуль не будет использовать символы, экспортируемые этим модулем, вы могли бы dlclose, тогда RTLD_GLOBAL никогда не требовалось.) Я не уверен, что RTLD_LAZY также хорошая идея.

Наконец, вам нужно найти способ узнать, когда возможно dlclose старых модулей. Вы не можете сделать это, пока не будете уверены, что ни один поток в настоящее время не вызывает функцию из устаревшего модуля. Возможно, вы захотите добавить счетчик ссылок в структуру модуля и использовать макрос или оболочку, чтобы гарантировать, что счетчик ссылок увеличивается и уменьшается до и после вызова. Вы также захотите добавить какой-нибудь мьютекс в структуру модуля, чтобы избежать условий гонки.

...