четверг, 16 декабря 2010 г.

Изгоняющий демонов

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

Одна из таких идей у меня касалась ошибок. Не простых, а трудно воспроизводимых. У нас вообще складывается ситуация на данный момент, что очень часто мы ничего не можем сказать, глядя на багу. Хорошо, если воспроизвел на стенде и причина ясна. Часто бывает что она проявляется не всегда, а только при полной луне...

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

Вообще сам факт наличия таких ошибок, как то трудновоспроизводимых или просто непонятных, характеризует общее состояние проекта. Да, есть над чем работать...

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

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

Прошел день...

Удалось выяснить, что падает она при попытке вызова виртуальной функции. Где-то портится память? Память вряд ли может портится в самой виртуальной таблице, скорее всего поехал указатель на виртуальную таблицу. Обложили это все ассертами - не клюет...

В тщетных попытках понять смысл проблемы прошел еще один день...

И вот ведь зараза, вообще перестала обрабатывать соединения. Ошибка происходит в 100% случаев. Это конечно хорошо, только ясности не добавляет. В порыве отчаяния накрутил сильно отладочных опций, как-то -fstack-check, -fbounds-check (Написано что в си не работает, ну до кучи). После чего удалось выяснить, что вызов виртуальной функции всетаки происходит, отладочное сообщение вывелось, а падение происходит где-то дальше.

Но долго искать уже не пришлось. В начале функции стоит буфер на 65 килобайт. Раньше уже приходилось сталкиваться с ограничением на размер стека нити. Помню что оно не велико... Очень невелико... Что-то типа нескольких килобайт...

Определить размер стека по умолчанию - просто.
#include <stdio.h>
#include <pthread.h>

int main(int argc, char **argv)
{
pthread_attr_t thread_attr;
pthread_attr_init(&thread_attr);

size_t stacksize = 0;
pthread_attr_getstacksize(&thread_attr, &stacksize);

printf("Размер стека по умолчанию: %lu\n", stacksize);

return 0;
}
Для Linux (x86-64) он достаточно комфортный - 8 мегабайт... но на FreeBSD (i386) - всего 64 килобайта. Учитывая тот факт, что стек нити располагается в адресном пространстве процесса, выделяясь в куче, выход за его пределы может нарушить работу приложения в самый непонятный момент. И эта проблема может долго оставаться незамеченной. Использование стека в релизной и отладочной версии разное. Расположение блоков памяти в куче тоже может варьироваться, предсказать результат достаточно сложно.

Я в принципе люблю пользоваться стеком, это ограничивает область видимости и избавляет от возни с динамической памятью. Наверное я все таки правильно сделал, что в MDF создал для каждой нити свой, полностью изолированный стек.

А что делать с проектом - где-то видел, что в gcc можно инструментовать каждую функцию. Надо туда вставить функцию, контролирующую размер стека. И погонять...