Многоядерные процессоры и проблемы ими порождаемые

       

Пути решения проблем


Самое простое (и самое радикальное) решение— указать ключ /NUMPROC=1 (или /ONECPU) в файле boot.ini, одним росчерком пера превратив многопроцессорную систему в однопроцессорную. Правда, о производительности после этого можно забыть, поэтому прибегать к такому "варварскому" методу стоит только в самых крайних случаях, когда система регулярно сбоит, а времени на поиски неисправности и капитальный ремонт у нас нет.

Кстати, поиск неисправностей – самое сложное дело. Некорректная синхронизация потоков приводит к порче данных и критические ошибки возникают (если они вообще возникают, хуже когда программа делает из обрабатываемых данных "винегрет") довольно далеко от места "аварии". То же самое относится и к голубым экранам смерти. Изучение дампов памяти дает довольно скудную информацию, особенно если разрушены структуры данных, хранящиеся в динамической памяти, которая каждый раз выделяется по разным адресам, что затрудняет воспроизведение ошибки.

При наличии исходных текстов в первую очередь проверьте: не используется ли во многопоточной программе однопоточные версии библиотек? В частности, компилятор Microsoft Visual C++ поставляется с двумя версиями статических Си-библиотек: LIBC.LIB – для однопоточных и LIBCMT.LIB – для многопоточных программ. Динамически компонуемая библиотека MSVCRT.LIB используется как в одно- так и во многопоточных проектах. Так же поищите прямые вызовы CreateThread(). Со стандартной Си-библиотекой они _не_ совместимы и потому должны быть в обязательном порядке заменены на _beginthread() или _beginthreadex().

Все глобальные переменные (кроме тех, что используются для обмена данных между потоками) поместите в TLS (Thread Local Storage – Локальная Память Потока). На уровне исходных текстов это делается так: "__declspec (thread) int my_var;", при этом компилятор создает в PE-файле специальную секцию .tls, куда и помещает my_var, автоматически создавая отдельный экземпляр для каждого из потоков.


В отсутствии исходных текстов эту затею осуществить труднее, но все-таки возможно. Сначала необходимо найти переменные, к которым идет обращение из нескольких потоков. Это делается так: ищутся все вызовы CreateThread()/_beginthread(), определяется стартовый адрес функции потока и создается дерево функций, вызываемых этим потоков (для этого удобно использовать скрипт func_tree.idc от mammon'а, который можно скачать с www.idapro.com). Перечисляем глобальные переменные, упомянутые в этих функциях и если одна и та же переменная встречается в деревьях двух разных потоков — смотрим на нее пристальным взглядом, пытаясь ответить на вопрос: может ли она быть источником проблем или нет? Если переменная не используется для обмена данными между потоками — замещаем все обращения к ней на переходник к нашему обработчику, размещенному в свободном месте файла, который, используя вызовы TslSetValue()/TslGetValue(), записывает/считывает ее содержимое. Если же переменная используется для обмена данными между потоками — окружаем ее критическими секциями или другими механизмами синхронизации.

Естественно, все это требует правки исполняемого файла (и при том довольно значительной). Без соответствующих знаний и навыков за такую задачу не стоит и браться! Правда, есть шанс, что проблему удастся разрешить и без правки — поменяв приоритеты потоков. Если один из двух (или более) потоков, использующий разделяемые данные без синхронизации, получит больший приоритет, чем остальные, "расстановка сил" немедленно изменится и, возможно, она изменится так, что порча данных станет происходить не так часто, как прежде. Нужные значения приоритетов подбираются экспериментально, а задаются API-функций SetThreadPriority(), принимающий дескриптор потока. Вот тут-то и начинаются проблемы. Мы можем легко узнать идентификатор потока через функции TOOLHELP32: CreateToolhelp32Snapshot(),Thread32First()/Thread32Next(), остается "всего лишь" преобразовать его в дескриптор.


Долгое время это приходилось делать весьма извращенным путем через недокументированные функции типа NtOpenThread (см. http://hi-tech.nsys.by/11/), но в Windows 2000 наконец-то появилась легальная API-функция OpenThread(), принимающая идентификатор потока и возвращающая его дескриптор (разумеется, при условии, что все необходимые права у нас есть). Виват, Microsoft!

Разобравшись с прикладными приложениями, перейдем к драйверам. При наличии исходных текстов достаточно использовать спин-блокировку во всех отложенных процедурах, однако, в большинстве случаев исходных текстов у нас нет, а править драйвер в hiew'е удовольствие не из приятных. К счастью, существует и другой путь — достаточно исправить таблицу диспетчеризации прерываний (IDT – Interrupt Dispatch Table), разрешив каждому процессору обрабатывать прерывания только от "своих" устройств. Это практически не снижает производительности (особенно если быстрые и медленные устройства между процессорами распределяются по честному, то есть равномерно) и ликвидирует ошибки синхронизации вместе с голубыми экранами смерти.


Содержание раздела