Скачать скомпилированную версию. Совместимо только с SA-MP 0.3.7-R2 Linux сервером.
Этот плагин исправляет потокобезопасность функций RakNet::GetTime и RakNet::GetTimeNS, которые используют общую глобальную переменную для хранения результата вызова gettimeofday. Вот код этих функций:
static bool initialized=false;
#ifdef _WIN32
static LARGE_INTEGER yo;
#else
static timeval tp, initialTime; // <=== глобальная переменная tp, используемая в функциях GetTime и GetTimeNS
#endif
RakNetTime RakNet::GetTime( void )
{
if ( initialized == false )
{
#ifdef _WIN32
QueryPerformanceFrequency( &yo );
// The original code shifted right 10 bits
//counts = yo.QuadPart >> 10;
// It gives the wrong value since 2^10 is not 1000
// counts = yo.QuadPart;// / 1000;
#else
gettimeofday( &initialTime, 0 );
#endif
initialized = true;
}
#ifdef _WIN32
LARGE_INTEGER PerfVal;
QueryPerformanceCounter( &PerfVal );
return (RakNetTime)(PerfVal.QuadPart*1000 / yo.QuadPart);
#else
gettimeofday( &tp, 0 );
// Seconds to ms and microseconds to ms
return ( tp.tv_sec - initialTime.tv_sec ) * 1000 + ( tp.tv_usec - initialTime.tv_usec ) / 1000;
#endif
}
RakNetTimeNS RakNet::GetTimeNS( void )
{
if ( initialized == false )
{
#ifdef _WIN32
QueryPerformanceFrequency( &yo );
// The original code shifted right 10 bits
//counts = yo.QuadPart >> 10;
// It gives the wrong value since 2^10 is not 1000
// counts = yo.QuadPart;// / 1000;
#else
gettimeofday( &initialTime, 0 );
#endif
initialized = true;
}
#ifdef _WIN32
LARGE_INTEGER PerfVal;
QueryPerformanceCounter( &PerfVal );
__int64 quotient, remainder;
quotient=((PerfVal.QuadPart*1000) / yo.QuadPart);
remainder=((PerfVal.QuadPart*1000) % yo.QuadPart);
//return (PerfVal.QuadPart*1000 / (yo.QuadPart/1000));
return quotient*1000 + (remainder*1000 / yo.QuadPart);
#else
gettimeofday( &tp, 0 );
return ( tp.tv_sec - initialTime.tv_sec ) * (RakNetTimeNS) 1000000 + ( tp.tv_usec - initialTime.tv_usec );
#endif
}
Изначально эти функции вызывались в основном из сетевого потока RakNet (хотя есть места, где это происходит в потоке пользовательского приложения), но на последних версиях SA-MP использует эти функции из своего главного потока в огромных количествах.
Одновременное выполнение любой из этих функций из разных потоков приводит к повреждению глобальной переменной tp, что в итоге сказывается на возвращаемом результате функции. И чем больше вызовов совершается из разных потоков, тем больше вероятность воспроизведения проблемы.
Примечательно, что Win32 реализация этих функций не имеет такого недостатка, результат вызова QueryPerformanceCounter записывается в локальную переменную PerfVal.
Последствий от этой ошибки может быть множество, но самая заметная проблема заключается в отправке подтверждений полученных пакетов:
unsigned ReliabilityLayer::GenerateDatagram(...)
{
// ...
if (time > nextAckTime)
{
if (acknowlegements.Size()>0)
{
output->Write(true);
messagesSent++;
statistics.acknowlegementBitsSent +=acknowlegements.Serialize(output, (MTUSize-UDP_HEADER_SIZE)*8-1, true);
if (acknowlegements.Size()==0)
nextAckTime=time+(RakNetTimeNS)(ping*(RakNetTime)(PING_MULTIPLIER_TO_RESEND/4.0f));
else
{
// printf("Ack full\n");
}
writeFalseToHeader=false;
}
else
{
writeFalseToHeader=true;
nextAckTime=time+(RakNetTimeNS)(ping*(RakNetTime)(PING_MULTIPLIER_TO_RESEND/4.0f));
}
}
else
writeFalseToHeader=true;
// ...
}
RakNet использует полученное функцией RakNet::GetTimeNS время, чтобы запланировать следующую отправку подтверждений клиенту, как это видно на приведенном сверху коде. В ситуациях, когда RakNet::GetTimeNS возвращает некорректный результат (чаще всего он намного больше корректного времени), следующая отправка подтверждений будет запланирована на время в далеком будущем (в рамках секунд). Сервер перестает отправлять подтверждения, что в конечном итоге приводит к решению клиента разорвать соединение.
Плагин перехватывает функции RakNet::GetTime и RakNet::GetTimeNS, чтобы использовать свой вариант реализации, который использует локальную переменную для хранения результата gettimeofday:
RakNetTime HOOK_RakNet_GetTime()
{
static bool& initialized = *reinterpret_cast<bool*>(0x81A19C4);
static timeval& initialTime = *reinterpret_cast<timeval*>(0x81A19BC);
if (!initialized)
{
gettimeofday(&initialTime, NULL);
initialized = true;
}
struct timeval tv; // <=== локальная переменная
gettimeofday(&tv, NULL);
return 1000 * (tv.tv_sec - initialTime.tv_sec) + (tv.tv_usec - initialTime.tv_usec) / 1000;
}
Это исправляет проблему и делает эти функции безопасными для вызова из нескольких потоков.
Очевидно, что разработчики или мейнтейнеры той версии RakNet, которая по сей день используется в SA-MP. Хотя Kalcor тоже внес свой вклад в появление этой проблемы и возможно без него она не была бы такой заметной. Последняя версия RakNet не имеет этой проблемы, как и более стабильный и функциональный форк SLikeNet.
Долгое время эта проблема оставалась незамеченной из-за того, что на актуальных несколько лет назад дистрибутивах Linux реализация gettimeofday каким-то образом избегала проблемы с повреждением глобальной переменной tp. Проблема воспроизводится лишь в современных дистрибутивах (как пример Debian 9, 10 и более поздние версии), что делает невозможным их использование для размещения SA-MP сервера.
Проблема имеет настолько плавающий характер, что может воспроизводиться только несколько раз в сутки, при этом из-за реализации использования RakTime::GetTimeNS (значение времени сохраняется один раз и используется для всех вызовов ReliabilityLayer::Update) одновременно вылетают абсолютно все игроки. Я смог поймать стабильное воспроизведение проблемы с помощью sleep 0 в server.cfg, при котором TPS (кол-во итераций циклов в секунду) было в районе 400 тысяч тиков в секунду, что резко повысило шансы на проявление проблемы (раз в 10-60 секунд) и поэтому стало возможно её исследовать и исправить.
Можно предположить, что до какой-то версии ядра Linux системный вызов gettimeofday имел блокировку, которая обеспечивала защиту от повреждения глобальной переменной tp, но в какой-то момент это было изменено для повышения производительности (хотя я не смог найти подтверждение этому в истории коммитов репозитория Linux).
Дополнительно стоит сказать, что я не смог воспроизвести эту проблему на нескольких виртуальных машинах, только на своих физических серверах. Поэтому, возможно, виртуальные сервера полностью избегают эту проблему, либо она каким-то образом зависит от конфигураций системы или других факторов.
На текущий момент уже завершена долгосрочная поддержка многих дистрибутивов Linux, на которой эта проблема не воспроизводится. Это значит, что эти дистрибутивы постепенно перестают получать обновления программ и утилит, в том числе самые главные - обновления безопасности. Этот плагин позволяет начать использовать новые современные версии дистрибутивов, чтобы размещать на них свои SA-MP сервера.
И хотя я общался с Kalcor насчет этой проблемы, думаю его уже не так сильно будет интересовать её решение и вряд ли у него будет желание выпустить патч сервера с исправлением: