Вступление
Добрый день, читатели. Искренне надеюсь, что вас больше одного. Рад приветствовать вас в начале нового цикла статей, посвящённых многопоточности и примитивам синхронизации.
Возможно, вам будет интересно, что сподвигло на написание этого материала. Что же, немного предыстории. Некоторые из вас знают, что реверс-инженерией я не занимаюсь уже почти год. Я перешёл на сторону software development, и сейчас занимаюсь разработкой высокопроизводительных клиент-серверных приложений на С++.
В ходе своего карьерного роста мне пришлось столкнуться с такой темой, как многопоточное программирование. Так уж устроены современные системы, что высокой производительности можно достичь лишь с помощью параллелизма.
В процессе поиска работы мне приходилось побывать на большом количестве собеседований (да и проводить их тоже приходилось). Поэтому в этом цикле статей я постараюсь рассмотреть все самые нужные и востребованные темы, охватить все популярные и часто задаваемые вопросы, которые могут вас ожидать как на собеседованиях, так и на практике.
Здесь я постараюсь доступно (насколько это возможно) и последовательно изложить всё, что вам требуется знать для написания эффективных многопоточных приложений. И сегодня мы начнём разговор с такого примитива синхронизации, как критическая секция.
Зачем нужны критические секции?
Очевидно, что критическая секция – это примитив синхронизации. Про неё (критическую секцию) написано немало, и нам предстоит разобраться, что из этого всего правда, а что – нет.
Используется она для защиты участка кода от одновременного выполнения несколькими потоками. То есть, захватив критическую секцию, поток начинает выполнять участок кода, в который другой поток уже не может попасть, потому что захватить ранее захваченную критическую секцию нельзя.
Наглядно работу критической секции демонстрирует вот эта иллюстрация.

Как работать с критическими секциями.
В идеале всё сводится к нескольким простым вызовам API – проинициализировать критическую секцию, захватить, освободить и удалить. Конечно же, с некоторыми ньюансами и оговорками.
Для иллюстрации есть пример на MSDN:
Рассмотрим часть этого примера:
PHP Code:
DWORD WINAPI ThreadProc( LPVOID lpParameter )
{
...
// Request ownership of the critical section.
EnterCriticalSection(&CriticalSection);
// Access the shared resource.
// Release ownership of the critical section.
LeaveCriticalSection(&CriticalSection);
...
return 1;
}
Пример хороший, простой, понятный. Вот только в общем случае делать так нельзя!
Рассмотрим такой код:
PHP Code:
int main(int argc, char** argv)
{
CRITICAL_SECTION CriticalSection;
auto x = 10;
InitializeCriticalSection(&CriticalSection);
EnterCriticalSection(&CriticalSection);
EnterCriticalSection(&CriticalSection);
x += 55;
LeaveCriticalSection(&CriticalSection);
return x;
}
Здесь переменная x защищена критической секцией. Да что там - мы ДВАЖДЫ попытались захватить секцию. И что в итоге?
PHP Code:
00402760 | 8D 4C 24 04 | lea ecx,dword ptr ss:[esp+4] |
00402764 | 83 E4 F0 | and esp,FFFFFFF0 |
00402767 | FF 71 FC | push dword ptr ds:[ecx-4] |
0040276A | 55 | push ebp |
0040276B | 89 E5 | mov ebp,esp |
0040276D | 56 | push esi |
0040276E | 53 | push ebx |
0040276F | 51 | push ecx |
00402770 | 8D 5D D0 | lea ebx,dword ptr ss:[ebp-30] |
00402773 | 83 EC 3C | sub esp,3C |
00402776 | E8 C5 EF FF FF | call testcriticalsections.401740 |
0040277B | 89 1C 24 | mov dword ptr ss:[esp],ebx |
0040277E | FF 15 3C 71 40 00 | call dword ptr ds:[<&RtlInitializeCriticalSection>] |
00402784 | 83 EC 04 | sub esp,4 |
00402787 | 8B 35 10 71 40 00 | mov esi,dword ptr ds:[<&RtlEnterCriticalSection>] |
0040278D | 89 1C 24 | mov dword ptr ss:[esp],ebx |
00402790 | FF D6 | call esi |
00402792 | 83 EC 04 | sub esp,4 |
00402795 | 89 1C 24 | mov dword ptr ss:[esp],ebx |
00402798 | FF D6 | call esi |
0040279A | 83 EC 04 | sub esp,4 |
0040279D | 89 1C 24 | mov dword ptr ss:[esp],ebx |
004027A0 | FF 15 40 71 40 00 | call dword ptr ds:[<&RtlLeaveCriticalSection>] |
004027A6 | 83 EC 04 | sub esp,4 |
004027A9 | 8D 65 F4 | lea esp,dword ptr ss:[ebp-C] |
004027AC | B8 41 00 00 00 | mov eax,41 | //наш х в самом конце
Между захватом и освобождением критической секции нет работы с этой переменной вообще. Критическая секция её никак не защищает.
Это иногда называют ошибками "видимости памяти". Суть в том, что оптимизатор переставляет инструкции местами. В данном случае он не нашёл никакой связи между вызовом API и модификацией переменной. Для борьбы с такими ошибками следует использовать идиому RAII. Но об этом чуть позже.
Как работает EnterCriticalSection
Задача EnterCriticalSection - захватить критическую секцию. Перед захватом
для критической секции должна быть выделена память, плюс должна быть выполнена инициализация – InitializeCriticalSection или аналогичная. Об инициализации мы поговорим чуть позже, сейчас же вернёмся к захвату секции.
Сам объект CRITICAL_SECTION (или правильнее сказать структура) выглядит так:
PHP Code:
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread;
HANDLE LockSemaphore;
ULONG_PTR SpinCount;
} RTL_CRITICAL_SECTION,*PRTL_CRITICAL_SECTION;
Аналогичное определение в winnt.h.
Теперь рассмотрим случай, когда в системе Windows 7 x86 выполняется захват критической секции. Эта секция была проинициализирована и никем не захвачена пока что.
PHP Code:
00402786 | 89 1C 24 | mov dword ptr ss:[esp],ebx | //указатель на структуру передаётся через стек
00402789 | FF 15 10 71 40 00 | call dword ptr ds:[<&RtlEnterCriticalSection>] | //EnterCriticalSection имеет forward в ntdll
Просматривая состояние структуры RTL_CRITICAL_SECTION в памяти, видим следующее:
PHP Code:
0022FE98 E0355200 FFFFFFFF 00000000 00000000 à5R.ÿÿÿÿ........
0022FEA8 00000000 00000000 D0FE2200 01000000 ........Ðþ".....
Первый шесть двойных слов является искомой структурой. Как видим, DebugInfo уже присутствует. RecursionCount равен -1 (0xFFFFFFFF). Остальное по нулям.
Теперь рассмотрим самую важную часть захвата:
PHP Code:
770E731D | 8D 77 04 | lea esi,dword ptr ds:[edi+4] |
770E7320 | 8B C6 | mov eax,esi | // получаем адрес LockCount
PHP Code:
770E7322 | F0 0F BA 30 00 | lock btr dword ptr ds:[eax],0
Вот эта конструкция крайне важна. Префикс lock делает эту операцию атомарной, т.е. непрерываемой.
btr dword ptr ds:[eax],0 считывает младший бит из LockCount в флаг CF. А на место этого бита записывается нулевой бит. Значение CF станет равным 1, потому что LockCount был 0xFFFFFFFF, а значит все его биты были установлены в 1. Новое значение LockCount - 0xFFFFFFFE.
PHP Code:
770E7327 | 0F 83 EB E5 00 00 | jae ntdll.770F5918 |
Сравнивает значение CF с нулём. Если там оказался 0, критическая секция уже была захвачена кем-то ранее. Сейчас же там 1, что говорит о её полной свободе. Значит мы можем спокойно её захватывать. В таком случае переход не выполняется.
PHP Code:
770E732D | 64 A1 18 00 00 00 | mov eax,dword ptr fs:[18] |
NtGetCurrentTeb - ищется TEB текущего потока.
770E7333 | 8B 48 24 | mov ecx,dword ptr ds:[eax+24] |
Получаем Id потока из TEB.
770E7336 | 89 4F 0C | mov dword ptr ds:[edi+C],ecx |
Сохраняем Id потока в OwningThread.
770E7339 | C7 47 08 01 00 00 00 | mov dword ptr ds:[edi+8],1 |
Ставим 1 в RecursionCount.
Как видите, захват основывается на атомарной инструкции lock btr dword ptr ds:[eax],0.
Как работает LeaveCriticalSection
PHP Code:
770E72D6 | 8B 75 08 | mov esi,dword ptr ss:[ebp+8] |
Получаем указатель на RTL_CRITICAL_SECTION.
PHP Code:
770E72D9 | 83 46 08 FF | add dword ptr ds:[esi+8],FFFFFFFF |
К значению RecursionCount прибавляем -1. Помните, после захвата это значение было равным 1? После такого прибавления оно будет равно 0, что будет сигнализировать об успешном выходе из рекурсии. Какой такой рекурсии? Сейчас узнаем.
PHP Code:
770E72DD | 75 23 | jne ntdll.770E7302 |
У нас в нормальной ситуации этот переход не выполнится, и мы пойдём дальше.
PHP Code:
770E72DF | 53 | push ebx |
770E72E0 | 57 | push edi |
770E72E1 | 8D 7E 04 | lea edi,dword ptr ds:[esi+4] |
Получаем адрес поля LockCount.
PHP Code:
770E72E4 | C7 46 0C 00 00 00 00 | mov dword ptr ds:[esi+C],0 |
Стираем Id потока, который владел критической секцией. (Обнуляем OwningThread)
PHP Code:
770E72EB | BB 01 00 00 00 | mov ebx,1 |
770E72F0 | 8B C7 | mov eax,edi |
770E72F2 | F0 0F C1 18 | lock xadd dword ptr ds:[eax],ebx |
В этих трёх интруцкиях к полю LockCount атомарно добавляется 1. Помните, после захвата там было значение 0xFFFFFFFE. После прибавления единицы будет 0xFFFFFFFF. Т.е. исходное значение, как до захвата. В ebx же запишется старое значение - 0xFFFFFFFE.
PHP Code:
770E72F6 | 43 | inc ebx |
770E72F7 | 83 FB FF | cmp ebx,FFFFFFFF |
770E72FA | 0F 85 9F AB FE FF | jne ntdll.770D1E9F |
Критическая секция может быть захвачена больше одного раза. Об этом говорит параметр LockCount.
Собственно, он и говорит, сколько раз она была захвачена. Этот код проверяет, освободили ли мы её столько раз, сколько захватили. Если это так, то переход не выполняется, и мы выходим из функции LeaveCriticalSection.
PHP Code:
770E7300 | 5F | pop edi |
770E7301 | 5B | pop ebx |
770E7302 | 33 C0 | xor eax,eax |
770E7304 | 5E | pop esi |
770E7305 | 5D | pop ebp |
770E7306 | C2 04 00 | ret 4 |
Таким образом, мы выяснили, что пока для нас важными является 3 поля:
LONG LockCount; // сколько раз секция была захвачена
LONG RecursionCount; // сколько раз была захвачена рекурсивно
HANDLE OwningThread; // какому потоку принадлежит секция (кто первый захватил)
Рекурсивный захват критической секции
EnterCriticalSection может вызываться дважды из одного и того же потока для захвата одной и той же (уже захваченной в первый раз) критической секции. Повторный захват из одного и того же потока называется рекурсивным.
PHP Code:
770E7320 | 8B C6 | mov eax,esi |
770E7322 | F0 0F BA 30 00 | lock btr dword ptr ds:[eax],0 |
770E7327 | 0F 83 EB E5 00 00 | jae ntdll.770F5918 |
CF в таком случае будет равен 0, и переход выполнится.
PHP Code:
770F5918 | 64 8B 0D 18 00 00 00 | mov ecx,dword ptr fs:[18] |
770F591F | 8B 57 0C | mov edx,dword ptr ds:[edi+C] |
770F5922 | 3B 51 24 | cmp edx,dword ptr ds:[ecx+24] |
770F5925 | 0F 85 E1 C3 FD FF | jne ntdll.770D1D0C |
Здесь текущий Id потока сравнивается с Id владельца. Так как это - один и тот же поток, то они равны. Переход не выполняется.
PHP Code:
770F592B | FF 47 08 | inc dword ptr ds:[edi+8] |
++RecursionCount
PHP Code:
770F592E | 5F | pop edi |
770F592F | 33 C0 | xor eax,eax |
770F5931 | 5E | pop esi |
770F5932 | 8B E5 | mov esp,ebp |
770F5934 | 5D | pop ebp |
770F5935 | C2 04 00 | ret 4
Инициализация критической секции.
Есть несколько API для инициализации. Самыми старыми являются:
PHP Code:
InitializeCriticalSection
InitializeCriticalSectionAndSpinCount
Эти две функции появились в XP. Есть более новая Функция:
PHP Code:
InitializeCriticalSectionEx
Теперь две старые функции будут работать через неё. Давайте для начала рассмотрим самый простой случай - как работает InitializeCriticalSection.
PHP Code:
0041FC66 | C7 04 24 34 10 43 00 | mov dword ptr ss:[esp],testcriticalsections.431034 |
0041FC6D | FF 15 28 22 43 00 | call dword ptr ds:[<&RtlInitializeCriticalSection>] |
Вызывается её форвард RtlInitializeCriticalSection. Он принимает указатель на структуру CRITICAL_SECTION.
PHP Code:
778B9FB6 | 8B FF | mov edi,edi |
778B9FB8 | 55 | push ebp |
778B9FB9 | 8B EC | mov ebp,esp |
778B9FBB | 8B 45 08 | mov eax,dword ptr ss:[ebp+8] |
778B9FBE | 6A 00 | push 0 |
778B9FC0 | 6A 00 | push 0 |
778B9FC2 | 50 | push eax |
778B9FC3 | E8 61 CC FF FF | call <ntdll.RtlInitializeCriticalSectionEx> |
778B9FC8 | 5D | pop ebp |
778B9FC9 | C2 04 00 | ret 4 |
RtlInitializeCriticalSectionEx принимает 3 параметра, 2 последних (SpinCount и Flags) равны нулю, потому что мы не имеем возможности их задать, вызывая InitializeCriticalSection.
PHP Code:
778B6C29 | 8B FF | mov edi,edi |
778B6C2B | 55 | push ebp |
778B6C2C | 8B EC | mov ebp,esp |
778B6C2E | 8B 45 10 | mov eax,dword ptr ss:[ebp+10] |
778B6C31 | 83 EC 28 | sub esp,28 |
778B6C34 | A9 00 00 00 F8 | test eax,F8000000 |
778B6C39 | 0F 85 25 B0 02 00 | jne ntdll.778E1C64 |
778B6C3F | 8B 4D 0C | mov ecx,dword ptr ss:[ebp+C] |
778B6C42 | F7 C1 00 00 00 FF | test ecx,FF000000 |
778B6C48 | 0F 85 21 B0 02 00 | jne ntdll.778E1C6F |
778B6C4E | A9 00 00 00 04 | test eax,4000000 |
778B6C53 | 0F 85 BE 00 00 00 | jne ntdll.778B6D17 |
Так мы заходим внутрь RtlInitializeCriticalSectionEx. [ebp+10] содержит значение Flags, а [ebp+C] - SpinCount. Как вы помните - они нулевые, потому ни один переход не выполнится.
PHP Code:
778B6C59 | 64 8B 15 18 00 00 00 | mov edx,dword ptr fs:[18] |
778B6C60 | 53 | push ebx |
778B6C61 | 56 | push esi |
778B6C62 | 33 DB | xor ebx,ebx |
778B6C64 | 57 | push edi |
778B6C65 | 8B 7D 08 | mov edi,dword ptr ss:[ebp+8] |
778B6C68 | C7 47 04 FF FF FF FF | mov dword ptr ds:[edi+4],FFFFFFFF |
778B6C6F | 89 5F 08 | mov dword ptr ds:[edi+8],ebx |
778B6C72 | 89 5F 0C | mov dword ptr ds:[edi+C],ebx |
778B6C75 | 89 5F 10 | mov dword ptr ds:[edi+10],ebx |
778B6C78 | 8B 52 30 | mov edx,dword ptr ds:[edx+30] |
778B6C7B | 83 7A 64 01 | cmp dword ptr ds:[edx+64],1 |
778B6C7F | 0F 86 9A 00 00 00 | jbe ntdll.778B6D1F |
Все поля структуры CRITICAL_SECTION, кроме LockCount, обнуляются. В LockCount записывается 0xFFFFFFFF. Через PEB достаётся количество процессоров в системе и сравнивается с 1. Дело в том, что спинлок имеет смысл применять только на системах, которые аппаратно поддерживают реальное параллельное выполнение. На однопроцессорных/одноядерных системах это бессмысленно. Потому совершенно неважно, какой SpinCount вы зададите на такой системе - он принудительно будет выставлен в 1.
PHP Code:
778B6C85 | A9 00 00 00 02 | test eax,2000000 |
778B6C8A | 0F 85 EA AF 02 00 | jne ntdll.778E1C7A |
Чтобы понять смысл работы с переменной Flags, следует изучить немного теории. В документации на MSDN описывается только одно значение этой переменной - CRITICAL_SECTION_NO_DEBUG_INFO. Если зададим его, то не будет создано поле DebugInfo в структуре CRITICAL_SECTION. Но на самом деле (недокументированных) значений больше.
PHP Code:
#define RTL_CRITICAL_SECTION_FLAG_NO_DEBUG_INFO 0x01000000
#define RTL_CRITICAL_SECTION_FLAG_DYNAMIC_SPIN 0x02000000
#define RTL_CRITICAL_SECTION_FLAG_STATIC_INIT 0x04000000
#define RTL_CRITICAL_SECTION_FLAG_RESOURCE_TYPE 0x08000000
#define RTL_CRITICAL_SECTION_FLAG_FORCE_DEBUG_INFO 0x10000000
#define RTL_CRITICAL_SECTION_ALL_FLAG_BITS 0xff000000
Так вот инструкция test eax,2000000 как раз проверяет, а не задали ли мы значение
RTL_CRITICAL_SECTION_FLAG_DYNAMIC_SPIN ? Если это так, то SpinCount будет задан независимо от нашего желания. И будет иметь некоторое предопределённое значение:
PHP Code:
778E1C7A | C7 47 14 D0 07 00 02 | mov dword ptr ds:[edi+14],20007D0
Почему 536872912 - не могу сказать.
Что интересно, RtlInitializeCriticalSectionEx может ничего и не проинициализировать. Такое может произойти, если:
1. Используется RTL_CRITICAL_SECTION_ALL_FLAG_BITS.
2. Используется очень большое значение SpinLock (более 0xFF000000).
Напомню, что оба эти сценария невозможны при вызове InitializeCriticalSection. При вызове этой API критическая секция будет инициализирована в любом случае. Потому у неё и соответствующая сигнатура:
PHP Code:
void WINAPI InitializeCriticalSection(
_Out_ LPCRITICAL_SECTION lpCriticalSection
);
Против
PHP Code:
BOOL WINAPI InitializeCriticalSectionAndSpinCount(
_Out_ LPCRITICAL_SECTION lpCriticalSection,
_In_ DWORD dwSpinCount
);
или
PHP Code:
BOOL WINAPI InitializeCriticalSectionEx(
_Out_ LPCRITICAL_SECTION lpCriticalSection,
_In_ DWORD dwSpinCount,
_In_ DWORD Flags
);
PHP Code:
778B6C90 | 81 E1 FF FF FF 00 | and ecx,FFFFFF |
778B6C96 | 89 4F 14 | mov dword ptr ds:[edi+14],ecx |
778B6C99 | 25 00 00 00 01 | and eax,1000000 |
778B6C9E | 89 45 10 | mov dword ptr ss:[ebp+10],eax |
778B6CA1 | 0F 85 E2 AF 02 00 | jne ntdll.778E1C89 |
778B6CA7 | E8 80 00 00 00 | call <ntdll.RtlpAllocateDebugInfo> |
Значение SpinCount уменьшается максимум до 0xFFFFFF, и если не использовался Flags RTL_CRITICAL_SECTION_FLAG_NO_DEBUG_INFO, то выделяется буфер под DebugInfo. Память выделяется в Heap процесса, и если это по каким-то причинам невозможно, то в зависимости от параметров запуска процесса мы можем даже увидеть отладочные сообщения об этом. Точнее это могут быть сообщения
Трассировки
Но к нашей тематике это не относится. Потому идём дальше.
PHP Code:
778B6CB6 | 89 37 | mov dword ptr ds:[edi],esi |
778B6CB8 | 33 C0 | xor eax,eax |
778B6CBA | 6A 01 | push 1 |
778B6CBC | 66 89 06 | mov word ptr ds:[esi],ax |
778B6CBF | 89 5E 14 | mov dword ptr ds:[esi+14],ebx |
778B6CC2 | 89 5E 10 | mov dword ptr ds:[esi+10],ebx |
778B6CC5 | 89 7E 04 | mov dword ptr ds:[esi+4],edi |
778B6CC8 | 89 5E 18 | mov dword ptr ds:[esi+18],ebx |
778B6CCB | E8 8A 00 00 00 | call <ntdll.RtlLogStackBackTraceEx> |
Здесь может производится логгирование. Это может быть полезно, если критические секции вдруг почему-то не будут работать, как надо. По дефолту оно выключено.
PHP Code:
778B6CD7 | 68 18 81 93 77 | push <ntdll.RtlCriticalSectionLock> |
778B6CDC | 66 89 46 1C | mov word ptr ds:[esi+1C],ax |
778B6CE0 | E8 2B 06 FF FF | call <ntdll.RtlEnterCriticalSection> |
778B6CE5 | 8B 0D 64 85 93 77 | mov ecx,dword ptr ds:[77938564] |
778B6CEB | 8D 46 08 | lea eax,dword ptr ds:[esi+8] |
778B6CEE | C7 00 60 85 93 77 | mov dword ptr ds:[eax],<ntdll.RtlCriticalSectionList> |
778B6CF4 | 89 48 04 | mov dword ptr ds:[eax+4],ecx |
778B6CF7 | 89 01 | mov dword ptr ds:[ecx],eax |
778B6CF9 | 68 18 81 93 77 | push <ntdll.RtlCriticalSectionLock> |
778B6CFE | A3 64 85 93 77 | mov dword ptr ds:[77938564],eax |
778B6D03 | E8 C8 05 FF FF | call <ntdll.RtlLeaveCriticalSection> |
Каждый процесс содержит список критических секций. Если вы инициализировали ещё одну, то она
добавляется в список.
Итак, подытожим.
1. Для начала работы с критической секцией требуется инициализация.
2. Инициализация может быть выполнена тремя Win API функциями:
- InitializeCriticalSection
- InitializeCriticalSectionAndSpinCount
- InitializeCriticalSectionEx (Windows Vista и старше)
3. Для InitializeCriticalSectionAndSpinCount и InitializeCriticalSectionEx требуется проверять
возвращаемое значение, т.к. эти функции могут не выполниться успешно.
4. Основная задача инициализации - записать дефолтные значения в структуру CRITICAL_SECTION
и добавить информацию о критической секции в RtlCriticalSectionList.
5. InitializeCriticalSectionEx может принимать недокументированные значения параметра Flags.
6. Возможно логировать события инициализации.
7. До инициализации использовать её не выйдет.
Захват критической секции, ранее захваченной другим потоком.
На текущий момент у нас есть захваченная секция, Id потока владельца отличается от Id текущего потока. Мы находимся здесь:
PHP Code:
770B5918 | 64 8B 0D 18 00 00 00 | mov ecx,dword ptr fs:[18] |
770B591F | 8B 57 0C | mov edx,dword ptr ds:[edi+C] |
770B5922 | 3B 51 24 | cmp edx,dword ptr ds:[ecx+24] |
770B5925 | 0F 85 E1 C3 FD FF | jne ntdll.77091D0C |
Переход выполняется.
PHP Code:
77091D0C | 8B 47 14 | mov eax,dword ptr ds:[edi+14] |
77091D0F | A9 00 00 00 04 | test eax,4000000 |
77091D14 | 0F 85 50 D2 FD FF | jne ntdll.7706EF6A |
Значение SpinCount меньше (в нашем случае), чем 0х4000000. Переход не выполняется.
PHP Code:
77091D1A | C7 45 FC 01 00 00 00 | mov dword ptr ss:[ebp-4],1 |
77091D21 | C7 45 F4 04 00 00 00 | mov dword ptr ss:[ebp-C],4 |
Инициализируются некие локальные переменные.
PHP Code:
77091CED | 8B 47 14 | mov eax,dword ptr ds:[edi+14] |
77091CF0 | 8B D0 | mov edx,eax |
77091CF2 | 81 E2 FF FF FF 00 | and edx,FFFFFF |
77091CF8 | 25 00 00 00 FF | and eax,FF000000 |
77091CFD | 89 45 F8 | mov dword ptr ss:[ebp-8],eax |
Значение SpinCount записывается в два регистра: eax и edx. В edx остаются 3 младших байта, а в eax кладётся старший. Значение старшего сохраняется рядом с другими локальными переменными - стек выглядит так:
PHP Code:
0022FE7C 00000004
0022FE80 00000000 //SpinCount == 0
0022FE84 00000001
PHP Code:
77091D00 | 85 D2 | test edx,edx |
77091D02 | 76 B3 | jbe ntdll.77091CB7 |
SpinCount сравнивается с нулём, и т.к. они равны - переход выполняется.
PHP Code:
77091CB7 | 8B 0E | mov ecx,dword ptr ds:[esi] |
77091CB9 | F6 C1 01 | test cl,1 |
77091CBC | 0F 85 A2 01 00 00 | jne ntdll.77091E64 |
Обратите внимание - здесь снова проверяется значение LockCount. Но теперь чтение происходит не атомарно. Эта проверка нужна на тот случай, если за время работы со SpinCount другой поток успел освободить блокировку. Но если не успел, то переход не выполнится.
PHP Code:
77091CC2 | F7 45 F8 00 00 00 02 | test dword ptr ss:[ebp-8],2000000 |
77091CC9 | 0F 85 FD 07 FE FF | jne ntdll.770724CC |
Снова сравнение SpinCount c некоей константой. Они не равны, потому идём дальше.
PHP Code:
77091CD2 | 52 | push edx |
77091CD3 | 57 | push edi |
77091CD4 | E8 58 00 00 00 | call <ntdll.RtlpWaitOnCriticalSection> |
Ожидание освобождения критической секции происходит внутри функции RtlpWaitOnCriticalSection. Ей передаются (первым параметром) адрес критической секции, и переменная
PHP Code:
77091D21 | C7 45 F4 04 00 00 00 | mov dword ptr ss:[ebp-C],4
вот отсюда (равна 4)
Рассмотрим весь кусок в целом:
PHP Code:
77091CB7 | 8B 0E | mov ecx,dword ptr ds:[esi] |
77091CB9 | F6 C1 01 | test cl,1 |
77091CBC | 0F 85 A2 01 00 00 | jne ntdll.77091E64 |
77091CC2 | F7 45 F8 00 00 00 02 | test dword ptr ss:[ebp-8],2000000 |
77091CC9 | 0F 85 FD 07 FE FF | jne ntdll.770724CC |
77091CCF | 8B 55 F4 | mov edx,dword ptr ss:[ebp-C] |
77091CD2 | 52 | push edx |
77091CD3 | 57 | push edi |
77091CD4 | E8 58 00 00 00 | call <ntdll.RtlpWaitOnCriticalSection> |
77091CD9 | 83 F8 01 | cmp eax,1 |
77091CDC | 74 28 | je ntdll.77091D06 |
77091CDE | 83 F8 02 | cmp eax,2 |
77091CE1 | 75 0A | jne ntdll.77091CED |
77091CE3 | C7 45 FC 03 00 00 00 | mov dword ptr ss:[ebp-4],3 |
77091CEA | 89 45 F4 | mov dword ptr ss:[ebp-C],eax |
77091CED | 8B 47 14 | mov eax,dword ptr ds:[edi+14] |
77091CF0 | 8B D0 | mov edx,eax |
77091CF2 | 81 E2 FF FF FF 00 | and edx,FFFFFF |
77091CF8 | 25 00 00 00 FF | and eax,FF000000 |
77091CFD | 89 45 F8 | mov dword ptr ss:[ebp-8],eax |
77091D00 | 85 D2 | test edx,edx |
77091D02 | 76 B3 | jbe ntdll.77091CB7 |
Если RtlpWaitOnCriticalSection возвращает не 1 и не 2, то две локальные переменные изменяются (одна равна 3, другая возвращённому значению RtlpWaitOnCriticalSection), а дальше опять то же самое. Более того, выйти отсюда нам поможет только возврат 1, т.к. возврат 2 просто позволит нам перепрыгнуть две инструкции:
PHP Code:
77091CE3 | C7 45 FC 03 00 00 00 | mov dword ptr ss:[ebp-4],3 |
77091CEA | 89 45 F4 | mov dword ptr ss:[ebp-C],eax |
Что же происходит внутри RtlpWaitOnCriticalSection?
PHP Code:
77091D31 | 8B FF | mov edi,edi |
77091D33 | 55 | push ebp |
77091D34 | 8B EC | mov ebp,esp |
77091D36 | 83 EC 44 | sub esp,44 |
77091D39 | 64 8B 0D 18 00 00 00 | mov ecx,dword ptr fs:[18] |
77091D40 | 53 | push ebx |
77091D41 | 56 | push esi |
77091D42 | 8B 75 08 | mov esi,dword ptr ss:[ebp+8] |
77091D45 | 33 C0 | xor eax,eax |
77091D47 | 81 FE 40 83 13 77 | cmp esi,<ntdll.LdrpLoaderLock> |
77091D4D | 0F 94 C0 | sete al |
77091D50 | 57 | push edi |
77091D51 | C7 45 F4 00 00 00 00 | mov dword ptr ss:[ebp-C],0 |
77091D58 | 89 4D F8 | mov dword ptr ss:[ebp-8],ecx |
77091D5B | 89 45 EC | mov dword ptr ss:[ebp-14],eax |
77091D5E | 85 C0 | test eax,eax |
77091D60 | 0F 85 DC 5D FF FF | jne ntdll.77087B42 |
77091D66 | 80 3D A8 88 13 77 00 | cmp byte ptr ds:[771388A8],0 |
77091D6D | 0F 85 CB DC FF FF | jne ntdll.7708FA3E |
77091D73 | 33 C0 | xor eax,eax |
77091D75 | 38 05 68 80 13 77 | cmp byte ptr ds:[<RtlpTimoutDisable>],al |
77091D7B | 0F 95 C0 | setne al |
77091D7E | 48 | dec eax |
77091D7F | 25 18 83 13 77 | and eax,<ntdll.RtlpTimeout> |
77091D84 | 89 45 F0 | mov dword ptr ss:[ebp-10],eax |
77091D87 | 8B 46 10 | mov eax,dword ptr ds:[esi+10] |
77091D8A | 89 45 FC | mov dword ptr ss:[ebp-4],eax |
77091D8D | 85 C0 | test eax,eax |
77091D8F | 0F 84 97 B5 FF FF | je ntdll.7708D32C |
Это достаточно большой кусок кода. Кратко прокомментирую происходящее. Вначале проверяется, не работаем ли мы со специальной критической секцией LdrpLoaderLock. Проверяется также, включена ли опция мониторинга времени захвата. Про EnterCriticalSection:
This function can raise EXCEPTION_POSSIBLE_DEADLOCK if a wait operation on the critical
section times out. The timeout interval is specified by the following registry value:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Contro l\Session Manager\CriticalSectionTimeout.
Do not handle a possible deadlock exception; instead, debug the application.
Если специально не создавать этот параметр, то RtlpTimoutDisable будет равен 1 (т.е. true).
PHP Code:
7708D32C | 56 | push esi |
7708D32D | E8 10 00 00 00 | call <ntdll.RtlpCreateCriticalSectionSem> |
Далее создаётся некий семафор. Внутри этой функции видим следующее:
PHP Code:
7708D342 | 8B FF | mov edi,edi |
7708D344 | 55 | push ebp |
7708D345 | 8B EC | mov ebp,esp |
7708D347 | 51 | push ecx |
7708D348 | 6A 00 | push 0 |
7708D34A | 6A 01 | push 1 |
7708D34C | 6A 00 | push 0 |
7708D34E | 68 03 00 10 00 | push 100003 |
7708D353 | 8D 45 FC | lea eax,dword ptr ss:[ebp-4] |
7708D356 | 50 | push eax |
7708D357 | E8 14 7E 01 00 | call <ntdll.ZwCreateEvent> |
7708D35C | 8B 55 08 | mov edx,dword ptr ss:[ebp+8] |
7708D35F | 83 C2 10 | add edx,10 |
7708D362 | 85 C0 | test eax,eax |
7708D364 | 0F 8C D2 D5 04 00 | jl ntdll.770DA93C |
7708D36A | 8B 4D FC | mov ecx,dword ptr ss:[ebp-4] |
7708D36D | 33 C0 | xor eax,eax |
7708D36F | F0 0F B1 0A | lock cmpxchg dword ptr ds:[edx],ecx |
7708D373 | 85 C0 | test eax,eax |
7708D375 | 0F 85 B0 D5 04 00 | jne ntdll.770DA92B |
7708D37B | B0 01 | mov al,1 |
7708D37D | 8B E5 | mov esp,ebp |
7708D37F | 5D | pop ebp |
7708D380 | C2 04 00 | ret 4 |
Делается вызов ZwCreateEvent, дескриптор события атомарно через lock cmpxchg записывается в поле LockSemaphore. Если ранее какое-то событие уже было создано (объект-событие), то оно будет уничтожено вызовом NtClose:
PHP Code:
770DA92B | 8B 45 FC | mov eax,dword ptr ss:[ebp-4] |
770DA92E | 50 | push eax |
770DA92F | E8 5C A7 FC FF | call <ntdll.NtClose> |
770DA934 | B0 01 | mov al,1 |
770DA936 | 8B E5 | mov esp,ebp |
770DA938 | 5D | pop ebp |
770DA939 | C2 04 00 | ret 4 |
Движемся дальше:
PHP Code:
77091D95 | 8B 4E 04 | mov ecx,dword ptr ds:[esi+4] |
77091D98 | 8D 7E 04 | lea edi,dword ptr ds:[esi+4] |
77091D9B | F6 C1 01 | test cl,1 |
77091D9E | 0F 85 AA DD FF FF | jne ntdll.7708FB4E |
Снова делается попытка проверить, не освободилась ли секция - проверяется параметр LockCount. Реализация пытается максимально всё проверить перед использованием объекта ядра (события). Событие будет применено только в случае крайней необходимости.
Далее пропущены не очень интересные моменты, и вот мы, наконец, ждём события:
PHP Code:
77091DE3 | 57 | push edi |
77091DE4 | 6A 00 | push 0 |
77091DE6 | 83 F8 FF | cmp eax,FFFFFFFF |
77091DE9 | 0F 84 A9 FD 04 00 | je ntdll.770E1B98 |
77091DEF | 50 | push eax |
77091DF0 | E8 EB 47 01 00 | call <ntdll.NtWaitForSingleObject> |
Если нам удалось дождаться, то NtWaitForSingleObject вернёт 0, и вся функция вернёт 2:
PHP Code:
77091DF5 | 3D 02 01 00 00 | cmp eax,102 |
77091DFA | 0F 84 AA FD 04 00 | je ntdll.770E1BAA |
77091E00 | 85 C0 | test eax,eax |
77091E02 | 0F 8C 3E FE 04 00 | jl ntdll.770E1C46 |
77091E08 | 83 7D EC 00 | cmp dword ptr ss:[ebp-14],0 |
77091E0C | 0F 85 1E 5D FF FF | jne ntdll.77087B30 |
77091E12 | 5F | pop edi |
77091E13 | 5E | pop esi |
77091E14 | B8 02 00 00 00 | mov eax,2 |
77091E19 | 5B | pop ebx |
77091E1A | 8B E5 | mov esp,ebp |
77091E1C | 5D | pop ebp |
77091E1D | C2 08 00 | ret 8 |
Ещё немного ненужного пропущено, и мы здесь:
PHP Code:
77091CB7 | 8B 0E | mov ecx,dword ptr ds:[esi] |
77091CB9 | F6 C1 01 | test cl,1 |
77091CBC | 0F 85 A2 01 00 00 | jne ntdll.77091E64 |
Опять читается LockCount, сейчас он равен 0xFFFFFFFD. Напомню, что после захвата критической секции его значение меняется с 0xFFFFFFFF на 0xFFFFFFFE, а захват "семафора" уменьшает его ещё на 1. Переход выполняется, и через пару инструкций оказываемся здесь:
PHP Code:
77091E22 | 33 45 FC | xor eax,dword ptr ss:[ebp-4] |
В eax 0xFFFFFFFD ксорится на 3. Таким образом, eax снова равен 0xFFFFFFFE. Далее это значение атомарно записывается в LockCount, и текущий поток становится владельцем критической секции.
Популярные вопросы и ответы на них
- Считаете ли вы критическую секцию самым простым синхронизационным примитивом?
- Нет, критическая секция является симбиозом сразу трёх механизмов синхронизации: атомарных операций, спинлока и объекта-события.
- Это правда, что критическая секция работает в режиме пользователя, а к ядру не обращается. Поэтому она и быстрее тех же мьютексов?
- Нет, критическая секция устроена так, что старается делать вызовы функций ядра только в том случае, когда без них не обойтись. Но в случае захвата разными потоками одной и той же секции ZwCreateEvent всё же будет вызван.
- Как критическая секция обрабатывает параметр SpinCount (счётчик спинлока) в однопроцессороной/одноядерной системе?
- Игнорирует его. Для таких систем спинлок не имеет смысла.
- Могут ли критические секции использоваться для синхронизации разных потоков, принадлежащих разным процессам?
- Нет, не могут. Критическая секция не видна за пределами адресного пространства процесса, в котором создана.
- А как же событие, которое создаётся в критической секции? Это же объект ядра – его должно быть “видно” из другого процесса.
- Событие создаётся с нулевым указателем POBJECT_ATTRIBUTES, потому не попадает в глобальную область видимости – его видно только локально в процессе, его создавшем.
- Следует ли использовать идиому RAII при работе с критическими секциями и зачем, если следует?
- Следует обязательно, она позволит освободить критическую секцию в случае генерации исключения и защитит от ошибок “видимости памяти”.
- Критические секции рекурсивны?
- Да, и других (нерекурсивных) не может быть.
В завершение
Надеюсь, материал был вам полезен. Пишите свои вопросы и пожелания в комментариях. До новых стреч – до следующей главы.