Издание четвертое windows ® для профессионалов создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows



Pdf просмотр
страница19/68
Дата28.11.2016
Размер3.57 Mb.
Просмотров12696
Скачиваний0
1   ...   15   16   17   18   19   20   21   22   ...   68
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
Таков вкратце механизм планирования работы множества потоков. Детали мы обсудим позже, но главное я уже показал. Все очень просто, да Windows потому и называется системой с вытесняющей многозадачностью, что в любой момент может приостановить любой потоки вместо него запустить другой. Как Вы еще увидите, этим механизмом можно управлять, правда, крайне ограниченно. Всегда помните Вы не в состоянии гарантировать, что Ваш поток будет выполняться непрерывно, что никакой другой поток не получит доступ к процессору и т. д.
Меня часто спрашивают, как сделать так, чтобы поток гарантированно запускался в течение определенного времени после какого нибудь события — например, не позднее чем через миллисекунду после приема данных с последовательного порта Ответ прост никак. Такие требования можно предъявлять к операционным системам реального времени, но Windows к ним не относится. Лишь операционная система реального времени имеет полное представление о характеристиках аппаратных средств, на которых она работает (об интервалах запаздывания контроллеров жестких дисков, клавиатуры и т. д. А
создавая Windows, Microsoft ставила другую цель обеспечить поддержку максимально широкого спектра оборудования — различных процессоров, дисковых устройств, сетей и др. Короче говоря, Windows не является операционной системой реального времени.
Хочу особо подчеркнуть, что система планирует выполнение только тех потоков,
которые могут получать процессорное время, но большинство потоков в системе к таковым не относится. Так, у некоторых объектов потоков значение счетчика простоев) больше 0, а значит, соответствующие потоки приостановлены и не получают процессорное время. Вы можете создать приостановленный поток вызовом
CreateProcess или CreateThread с флагом CREATE_SUSPENDED. (В следующем разделе я расскажу и о таких функциях, как
SuspendThread и ResumeThread.)
Кроме приостановленных, существуют и другие потоки, не участвующие в распределении процессорного времени, — они ожидают каких либо событий. Например,
если Вы запускаете Notepad и не работаете в нем с текстом, его поток бездействует, а система не выделяет процессорное время тем, кому нечего делать. Но стоит лишь сместить его окно, прокрутить в нем текст или что то ввести, как система автоматически включит поток Notepad в число планируемых. Это вовсе не означает, что поток тут же начнет выполняться. Просто система учтет его при планировании потоков и когда нибудь выделит ему время — по возможности в ближайшем будущем.
Приостановка и возобновление потоков
В объекте ядра поток имеется переменная — счетчик числа простоев данного потока. При вызове
CreateProcess или CreateThread он инициализируется значением, равным, которое запрещает системе выделять новому потоку процессорное время. Такая схема весьма разумна сразу после создания поток неготов к выполнению, ему нужно время для инициализации.
После того как поток полностью инициализирован,
CreateProcess или CreateThread
проверяет, не передан ли ей флаги, если да, возвращает управление, оставив поток в приостановленном состоянии. Вином случае счетчик простоев обнуляется, и поток включается в число планируемых — если только он не ждет какого то события (например, ввода с клавиатуры).

157
Г ЛАВА 7
Планирование потоков, приоритет и привязка к процессорам
Создав поток в приостановленном состоянии, Вы можете настроить некоторые его свойства (например, приоритет, о котором мы поговорим позже. Закончив настройку,
Вы должны разрешить выполнение потока. Для этого вызовите
ResumeThread и передайте описатель потока, возвращенный функцией
CreateThread (описатель можно взять и из структуры, на которую указывает параметр
ppiProcInfo, передаваемый в
CreateProcess).
DWORD ResumeThread(HANDLE Если вызов
ResumeThread прошел успешно, она возвращает предыдущее значение счетчика простоев данного потока вином случае — Выполнение отдельного потока можно приостанавливать несколько раз. Если поток приостановлен 3 раза, то и возобновлен он должен быть тоже 3 раза — лишь тогда система выделит ему процессорное время. Выполнение потока можно приостановить не только при его создании с флагом CREATE_SUSPENDED, но и вызовом
SuspendThread:
DWORD SuspendThread(HANDLE Любой поток может вызвать эту функцию и приостановить выполнение другого потока (конечно, если его описатель известен. Хоть об этом нигде и не говорится
(но я все равно скажу, приостановить свое выполнение поток способен сама возобновить себя без посторонней помощи — нет. Как и
ResumeThread, функция Sus
pendThread возвращает предыдущее значение счетчика простоев данного потока.
Поток можно приостанавливать не более чем MAXIMUM_SUSPEND_COUNT разв файле WinNT.h это значение определено как 127). Обратите внимание, что
Suspend
Thread в режиме ядра работает асинхронно, нов пользовательском режиме не выполняется, пока поток остается в приостановленном состоянии.
Создавая реальное приложение, будьте осторожны с вызовами
SuspendThread, так как нельзя заранее сказать, чем будет заниматься его поток в момент приостановки.
Например, он пытается выделить память из кучи и поэтому заблокировал к ней доступ. Тогда другим потокам, которым тоже нужна динамическая память, придется ждать его возобновления.
SuspendThread безопасна только в том случае, когда Вы точно знаете, что делает (или может делать) потоки предусматриваете все меры для исключения вероятных проблем и взаимной блокировки потоков. (О взаимной блокировке и других проблемах синхронизации потоков я расскажу в главах 8, 9 и 10.)
Приостановка и возобновление процессов
В Windows понятия приостановка и возобновление неприменимы к процессам, так как они не участвуют в распределении процессорного времени. Однако меня не раз спрашивали, как одним махом приостановить все потоки определенного процесса.
Это можно сделать из другого процесса, причем он должен быть отладчиком ив частности, вызывать функции вроде
WaitForDebugEvent и ContinueDebugEvent.
Других способов приостановки всех потоков процесса в Windows нет программа, выполняющая такую операцию, может потерять новые потоки. Система должна как то приостанавливать в этот период не только все существующие, но и вновь создаваемые потоки. Microsoft предпочла встроить эту функциональность в системный механизм отладки.
Вам, конечно, не удастся написать идеальную функцию
SuspendProcess, но вполне по силам добиться ее удовлетворительной работы во многих ситуациях. Вот мойва риант функции
SuspendProcess.

158
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ SuspendProcess(DWORD dwProcessID, BOOL fSuspend) {
// получаем список потоков в системе hSnapshot = CreateToolhelp32Snapshot(
TH32CS_SNAPTHREAD, dwProcessID);
if (hSnapshot != INVALID_HANDLE_VALUE) {
// просматриваем список потоков te = { sizeof(te) };
BOOL fOk = Thread32First(hSnapshot, &te);
for (; fOk; fOk = Thread32Next(hSnapshot, &te)) {
// относится ли данный поток к нужному процессу (te.th32OwnerProcessID == dwProcessID) {
// пытаемся получить описатель потока по его идентификатору hThread = OpenThread(THREAD_SUSPEND_RESUME,
FALSE, te.th32ThreadID);
if (hThread != NULL) {
// приостанавливаем или возобновляем поток if (Для перечисления списка потоков я использую ToolHelp функции (они рассматривались в главе 4). Определив потоки нужного процесса, я вызываю
OpenThread:
HANDLE OpenThread(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD Это новая функция, которая появилась в Windows 2000. Она находит объект ядра
«поток» по идентификатору, указанному в
dwThreadID, увеличивает его счетчик пользователей на 1 и возвращает описатель объекта. Получив описатель, я могу передать его вили имеется только в Windows поэтому моя функция
SuspendProcess не будет работать нив, нив Вероятно, Вы уже догадались, почему
SuspendProcess будет срабатывать не во всех случаях при перечислении могут создаваться новые и уничтожаться существующие потоки. После вызова
CreateToolhelp32Snapshot в процессе может появиться новый поток, который моя функция уже не увидит, а значит, и не приостановит. Впоследствии, когда я попытаюсь возобновить потоки, вновь вызвав
SuspendProcess, она во

159
Г ЛАВА 7
Планирование потоков, приоритет и привязка к процессорам зобновит поток, который собственно и не приостанавливался. Но может быть еще хуже при перечислении текущий поток уничтожается и создается новый стем же идентификатором. Тогда моя функция приостановит неизвестно какой потоки даже непонятно в каком процессе).
Конечно, все эти ситуации крайне маловероятны, и, если Вы точно представляете, что делает интересующий Вас процесс, никаких проблем не будет. В общем, используйте мою функцию на свой страхи риск.
Функция Sleep
Поток может сообщить системе не выделять ему процессорное время на определенный период, вызвав Sleep(DWORD Эта функция приостанавливает поток на
dwMilliseconds миллисекунд. Отметим несколько важных моментов, связанных с функцией
Sleep.
í
Вызывая
Sleep, поток добровольно отказывается от остатка выделенного ему кванта времени.
í
Система прекращает выделять потоку процессорное время на период,
пример
но равный заданному. Все верно если Вы укажете остановить поток на 100 мс,
приблизительно настолько они заснет, хотя не исключено, что его сон продлится на несколько секунд или даже минут больше. Вспомните, Windows не является системой реального времени. Ваш поток может возобновиться в заданный момент, но это зависит оттого, какая ситуация сложится в системе к тому времени.
í
Вы можете вызвать
Sleep и передать в dwMilliseconds значение INFINITE, вообще запретив планировать поток. Но это не очень практично — куда лучше корректно завершить поток, освободив его стеки объект ядра.
í
Вы можете вызвать
Sleep и передать в dwMilliseconds нулевое значение. Тогда
Вы откажетесь от остатка своего кванта времени и заставите систему подключить к процессору другой поток. Однако система может снова запустить Ваш поток, если других планируемых потоков стем же приоритетом нет.
Переключение потоков
Функция
SwitchToThread позволяет подключить к процессору другой поток (если он есть Когда Вы вызываете эту функцию, система проверяет, есть ли поток, которому не хватает процессорного времени. Если нет,
SwitchToThread немедленно возвращает управление, а если да, планировщик отдает ему дополнительный квант времени (приоритет этого потока может быть ниже, чему вызывающего. По истечении этого кванта планировщик возвращается в обычный режим работы.
SwitchToThread позволяет потоку, которому не хватает процессорного времени,
отнять этот ресурсу потока с более низким приоритетом. Она возвращает FALSE, если на момент ее вызова в системе нет ни одного потока, готового к исполнению вином случае — ненулевое значение.

160
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
Вызов
SwitchToThread аналогичен вызову Sleep с передачей в dwMilliseconds нулевого значения. Разница лишь в том, что
SwitchToThread дает возможность выполнять потоки с более низким приоритетом, которым не хватает процессорного времени, а
Sleep действует без оглядки на голодающие потоки.
В Windows 98 функция
SwitchToThread лишь определена, ноне реализована.
Определение периодов выполнения потока
Иногда нужно знать, сколько времени затрачивает поток на выполнение той или иной операции. Многие в таких случаях пишут что то вроде этого получаем стартовое время dwStartTime = GetTickCount();
// здесь выполняем какой нибудь сложный алгоритм вычитаем стартовое время из текущего dwElapsedTime = GetTickCount() Этот код основан на простом допущении, что он не будет прерван. Нов операционной системе с вытесняющей многозадачностью никто не знает, когда поток получит процессорное время, и результат будет сильно искажен. Что нам здесь нужно, так это функция, которая сообщает время, затраченное процессором на обработку данного потока. К счастью, весть такая функция GetThreadTimes(
HANDLE hThread,
PFILETIME pftCreationTime,
PFILETIME pftExitTime,
PFILETIME pftKernelTime,
PFILETIME pftUserTime);
GetThreadTimes возвращает четыре временных параметра:
Показатель времени
Описание
Время создания (creation Абсолютная величина, выраженная в интервалах по 100 нс. Отсчитывается с полуночи 1 января года по Гринвичу до момента создания потока.
Время завершения (exit Абсолютная величина, выраженная в интервалах по 100 нс. Отсчитывается с полуночи 1 января года по Гринвичу до момента завершения потока. Если поток все еще выполняется, этот показатель имеет неопределенное значение.
Время выполнения ядра (kernel Относительная величина, выраженная в интервалах по 100 нс. Сообщает время, затраченное этим потоком на выполнение кода операционной системы.
Время выполнения User (User Относительная величина, выраженная в интервалах по 100 нс. Сообщает время, затраченное потоком на выполнение кода приложения.

161
Г ЛАВА 7
Планирование потоков, приоритет и привязка к процессорам
С помощью этой функции можно определить время, необходимое для выполнения сложного алгоритма FileTimeToQuadWord(PFILETIME pft) {
return(Int64ShllMod32(pft >dwHighDateTime, 32) | pft >dwLowDateTime);
}
void PerformLongOperation () {
FILETIME ftKernelTimeStart, ftKernelTimeEnd;
FILETIME ftUserTimeStart, ftUserTimeEnd;
FILETIME ftDummy;
__int64 qwKernelTimeElapsed, qwUserTimeElapsed, qwTotalTimeElapsed;
// получаем начальные показатели времени, &ftDummy, &ftDummy,
&ftKernelTimeStart, &ftUserTimeStart);
// здесь выполняем сложный алгоритм получаем конечные показатели времени, &ftDummy, &ftDummy,
&ftKernelTimeEnd, &ftUserTimeEnd);
// получаем значения времени, затраченного на выполнение ядра и User,
// преобразуя начальные и конечные показатели времени изв учетверенные слова, а затем вычитая начальные показатели из конечных qwKernelTimeElapsed = FileTimeToQuadWord(&ftKernelTimeEnd)
FileTimeToQuadWord(&ftKernelTimeStart);
qwUserTimeElapsed = FileTimeToQuadWord(&ftUserTimeEnd)
FileTimeToQuadWord(&ftUserTimeStart);
// получаем общее время, складывая время выполнения ядра и User qwTotalTimeElapsed = qwKernelTimeElapsed + qwUserTimeElapsed;
// общее время хранится в Заметим, что существует еще одна функция, аналогичная
GetThreadTimes и применимая ко всем потокам в процессе GetProcessTimes(
HANDLE hProcess,
PFILETIME pftCreationTime,
PFILETIME pftExitTime,
PFILETIME pftKernelTime,
PFILETIME pftUserTime);
GetProcessTimes возвращает временные параметры, суммированные по всем потокам (даже уже завершенным) в указанном процессе. Так, время выполнения ядра будет суммой периодов времени, затраченного всеми потоками процесса на выполнение кода операционной системы.

162
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
К сожалению, в Windows 98 функции
GetThreadTimes и GetProcessTimes определены, ноне реализованы. Так что в Windows 98 нет надежного механизма, с помощью которого можно было бы определить, сколько процессорного времени выделяется потоку или процессу.
GetThreadTimes не годится для высокоточного измерения временных интервалов для этого в Windows предусмотрено две специальные функции QueryPerformanceFrequency(LARGE_INTEGER* pliFrequency);
BOOL QueryPerformanceCounter(LARGE_INTEGER* Они построены на том допущении, что поток не вытесняется, поскольку высокоточные измерения проводятся, как правило, в очень быстро выполняемых блоках кода.
Чтобы слегка упростить работу с этими функциями, я создал следующий C++ класс CStopwatch {
public:
CStopwatch() { QueryPerformanceFrequency(&m_liPerfFreq); Start(); }
void Start() { QueryPerformanceCounter(&m_liPerfStart); }
__int64 Now() const {
// возвращает число миллисекунд после вызова Start
LARGE_INTEGER liPerfNow;
QueryPerformanceCounter(&liPerfNow);
return(((liPerfNow.QuadPart m_liPerfStart.QuadPart) * 1000)
/ m_liPerfFreq.QuadPart);
}
private:
LARGE_INTEGER m_liPerfFreq;
// количество отсчетов в секунду m_liPerfStart;
// начальный отсчет
};
Я применяю этот класс так создаю секундомер (начинающий отсчет с текущего момента времени stopwatch;
// здесь я помещаю код, время выполнения которого нужно измерить определяю, сколько времени прошло qwElapsedTime = stopwatch.Now();
// qwElapsedTime сообщает длительность выполнения в миллисекундах
Структура CONTEXT
К этому моменту Вы должны понимать, какую важную роль играет структура в планировании потоков. Система сохраняет в ней состояние потока перед самым отключением его от процессора, благодаря чему его выполнение возобновляется с того места, где было прервано.
Вы, наверное, удивитесь, нов документации Platform SDK структуре отведен буквально один абзац:

163
Г ЛАВА 7
Планирование потоков, приоритет и привязка к процессорам
«В структуре CONTEXT хранятся данные о состоянии регистров с учетом специфики конкретного процессора. Она используется системой для выполнения различных внутренних операций. В настоящее время такие структуры определены для процессоров и PowerPC. Соответствующие определения см. в заголовочном файле В документации нет ни слова об элементах этой структуры, набор которых зависит от типа процессора. Фактически CONTEXT — единственная из всех структур, специфичная для конкретного процессора.
Так из чего же состоит структура CONTEXT? Давайте посмотрим. Ее элементы четко соответствуют регистрам процессора. Например, для процессоров
x86 в число элементов входят
Eax, Ebx, Ecx, Edx и т. да для процессоров Alpha — IntV0, IntT0, IntT1,
IntS0, IntRa, IntZero и др. Структура CONTEXT для процессоров x86 выглядит так struct _CONTEXT {
//
// Флаги, управляющие содержимым записи CONTEXT.
//
// Если запись контекста используется как входной параметр, тогда раздел управляемый флагом (когда он установлен, считается содержащим действительные значения. Если запись контекста используется для модификации контекста потока, то изменяются только те разделы, для которых флаг установлен Если запись контекста используется как входной и выходной параметр для захвата контекста потока, возвращаются только те разделы контекста для которых установлены соответствующие флаги. Запись контекста никогда не используется только как выходной параметр ContextFlags;
//
// Этот раздел определяется/возвращается, когда в ContextFlags установлен флаг CONTEXT_DEBUG_REGISTERS. Заметьте, что CONTEXT_DEBUG_REGISTERS
// не включаются в CONTEXT_FULL.
//
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
//
// Этот раздел определяется/возвращается, когда в ContextFlags
// установлен флаг CONTEXT_FLOATING_POINT.
//
FLOATING_SAVE_AREA FloatSave;
см. след. стр.

164
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ Этот раздел определяется/возвращается, когда в ContextFlags
// установлен флаг CONTEXT_SEGMENTS.
//
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
//
// Этот раздел определяется/возвращается, когда в ContextFlags
// установлен флаг CONTEXT_INTEGER.
//
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
//
// Этот раздел определяется/возвращается, когда в ContextFlags
// установлен флаг CONTEXT_CONTROL.
//
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
// следует очистить EFlags;
// следует очистить Esp;
DWORD SegSs;
//
// Этот раздел определяется/возвращается, когда в ContextFlags
// установлен флаг CONTEXT_EXTENDED_REGISTERS.
// Формат и смысл значений зависят от типа процессора ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} Эта структура разбита на несколько разделов. Раздел CONTEXT_CONTROL содержит управляющие регистры процессора указатель команд, указатель стека, флаги и адрес возврата функции. (В отличие от
x86, который при вызове функции помещает адрес возврата в стек, процессор Alpha сохраняет адрес возврата водном из регистров) Раздел CONTEXT_INTEGER соответствует целочисленным регистрам процессора регистрам с плавающей точкой, CONTEXT_SEG
MENTS — сегментным регистрам (только для
x86), CONTEXT_DEBUG_REGISTERS регистрам, предназначенным для отладки (только для
x86), а CONTEXT_EXTEN
DED_REGISTERS — дополнительным регистрам (только для
x86).

165
Г ЛАВА 7
Планирование потоков, приоритет и привязка к процессорам фактически позволяет заглянуть внутрь объекта ядра потоки получить сведения о текущем состоянии регистров процессора. Для этого предназначена функция Создайте экземпляр структуры CONTEXT, инициализируйте нужные флаги (в элементе) и передайте функции GetThreadContext адрес этой структуры.
Функция поместит значения в элементы, сведения о которых Вы запросили.
Прежде чем обращаться к
GetThreadContext, приостановите поток вызовом Sus
pendThread, иначе поток может быть подключен к процессору, и значения регистров существенно изменятся. На самом деле у потока есть два контекста пользовательского режима и режима ядра.
GetThreadContext возвращает лишь первый из них. Если Вы вызываете
SuspendThread, когда поток выполняет код операционной системы, пользовательский контекст можно считать достоверным, даже несмотря на то что поток еще не остановлен (он все равно не выполнит ни одной команды пользовательского кода до последующего возобновления).
Единственный элемент структуры CONTEXT, которому не соответствует какой либо регистр процессора, —
ContextFlags. Присутствуя во всех вариантах этой структуры независимо от типа процессора, он подсказывает функции
GetThreadContext,
значения каких регистров Вы хотите узнать. Например, чтобы получить значения управляющих регистров для потока, напишите что то вроде создаем экземпляр структуры CONTEXT
CONTEXT Context;
// сообщаем системе, что нас интересуют сведения только об управляющих регистрах = CONTEXT_CONTROL;
// требуем от системы информацию о состоянии регистров процессора для данного потока, &Context);
// действительные значения содержат элементы структуры CONTEXT,
// соответствующие управляющим регистрам, остальные значения не определены
Перед вызовом
GetThreadContext надо инициализировать элемент ContextFlags.
Чтобы получить значения как управляющих, таки целочисленных регистров, инициализируйте его так сообщаем системе, что нас интересуют управляющие и целочисленные регистры = CONTEXT_CONTROL | Есть еще один идентификатор, позволяющий узнать значения важнейших регистров (те. используемых, по мнению Microsoft, чаще всего сообщаем системе, что нас интересуют все значимые регистры = CONTEXT_FULL;
CONTEXT_FULL определен в файле WinNT.h, как показано в таблице.

166
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
Тип процессора
Определение CONTEXT_FULL
x86
CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS
Alpha
CONTEXT_CONTROL | CONTEXT_FLOATING_POINT | После возврата из
GetThreadContext Вы легко проверите значения любых регистров для потока, но помните, что такой код зависит от типа процессора. В следующей таблице перечислены элементы структуры CONTEXT, соответствующие указателям команд и стека для разных типов процессоров.
Тип процессора
Указатель команд
Указатель стека
x86
CONTEXT.Eip
CONTEXT.Esp
Alpha
CONTEXT.Fir
CONTEXT.IntSp
Даже удивительно, какой мощный инструмент дает Windows в руки разработчика Но есть вещь, от которой Вы придете в полный восторг значения элементов можно изменять и передавать объекту ядра поток с помощью функции
SetThreadContext.
BOOL SetThreadContext(
HANDLE hThread,
CONST CONTEXT Перед этой операцией поток тоже нужно приостановить, иначе результаты могут быть непредсказуемыми.
Прежде чем обращаться к
SetThreadContext, инициализируйте элемент ContextFlags,
как показано ниже Context;
// приостанавливаем поток получаем регистры для контекста потока = CONTEXT_CONTROL;
GetThreadContext(hThread, &Context);
// устанавливаем указатель команд по своему выбору в нашем примере присваиваем значение 0x00010000
#if defined(_ALPHA_)
Context.Fir = 0x00010000;
#elif defined(_X86_)
Context.Eip = 0x00010000;
#else
#error Module contains CPU specific code; modify and recompile.
#endif
// вносим изменения в регистры потока ContextFlags
// можно и не инициализировать, так как это уже сделано = CONTEXT_CONTROL;
SetThreadContext(hThread, &Context);
// возобновляем выполнение потока оно начнется с адреса 0x00010000
ResumeThread(hThread);
1   ...   15   16   17   18   19   20   21   22   ...   68


База данных защищена авторским правом ©nethash.ru 2019
обратиться к администрации

войти | регистрация
    Главная страница


загрузить материал