|
|
|||
|
wm-help.net -> Электронная библиотека -> C++/C#/C -> Джеффри РИХТЕР "Windows для профессионалов" -> Глава 5. ЗаданияГлава 5. ЗаданияГpynny процессов зачастую нужно рассматривать как единую сущность. Например, когда Вы командуете Microsoft Developer Studio собрать проект, он порождает про цесс Ct.exe, а тот в свою очередь может создать другие процессы (скажем, для допол нительных проходов компилятора) Но, если Вы пожелаете прервать сборку, Developer Studio должен каким-то образом завершить C1.exe и все его дочерние процессы. Ре шение этой простой (и распространенной) проблемы в Windows было весьма затруд нительно, поскольку она не отслеживает родственные связи между процессами. В ча стности, выполнение дочерних процессов продолжается даже после завершения ро дительского При разработке сервера тоже бывает полезно группировать процессы. Допустим, клиентская программа просит сервер выполнить приложение (которое создает ряд дочерних процессов) и сообщить результаты Поскольку к серверу может обратиться сразу несколько клиентов, было бы неплохо, если бы он умел как-то ограничивать ресурсы, выделяемые каждому клиенту, и тем самым не давал бы одному клиенту мо нопольно использовать все серверные ресурсы. Под ограничения могли бы подпадать такие ресурсы, как процессорное время, выделяемое на обработку клиентского зап роса, и размеры рабочего набира (working set). Кроме того, у клиентской программы не должно быть возможности завершить работу сервера и т д. В Wmdows 2000 введен новый объект ядра — задание job). Он позволяет группи ровать процессы и помещать их в нечто вроде песочницы, которая определенным образом ограничивает их действия. Относитесь к этому объекту как к контейнеру процессов Кстати, очень полезно создавать задание и с одним процессом — это по зволяет налагать на процесс- ограничения, которые иначе указать нельзя Взгляните на мою функцию StartRestrictedProcess (рис. 5-1). Она включает процесс в задание, которое ограничивает возможность выполнения определенных операций
// проводим
очистку } Рис. 5-1. Функция StartRestrictedProcess А теперь я объясню, как работает StartRestrictedProcess. Сначала я создаю новый объект ядра «задание», вызывая:
Как и любая функция, создающая объекты ядра, CreateJobObject принимает в пер вом параметре информацию о защите и сообщает системе, должна ли она вернуть наследуемый описатель. Параметр pszName позволяет присвоить заданию имя, что бы к нему могли обращаться другие процессы через функцию OpenJobObject.
Закончив работу с объектом-заданием, закройте сго описатель, вызвав, как всегда, CloseHandle Именно так я и делаю в конце своей функции StartRestrictedProcess Имейте в виду, что закрытие объекта-задания не приводит к автоматическому завершению всех его процессов. На самом деле этот объект просто помечается как подлежащий разрушению, и система уничтожает его только после завершения всех включенных в него процессов Заметьте, что после закрытия описателя объект-задание становится недоступным для процессов, даже несмотря на то что объект все еще существует Этот факт иллю стрирует следующий код:
Определение ограничений, налагаемых на процессы в заданииСоздав задание, Вы обычно строите "песочницу" (набор ограничений) для включае мых в него процессов. Ограничения бывают нескольких видов:
Ограничения на задание вводятся вызовом:
Первый параметр определяет нужное Вам задание, второй параметр (перечисли мого типа) — вид ограничений, третий — адрес структуры данных, содержащей под робную информацию о задаваемых ограничениях, а четвертый — размер этой струк туры (используется для указания версии). Следующая таблица показывает, как уста навливаются ограничения.
В функции StartRestrictedProcess я устанавливаю для задания лишь несколько базовых ограничений. Для этого я создаю структуру JOB_OBJECT_BASIC_LIMIT_INFOR MATION, инициализирую ее и вызываю функцию SetInformationJobObject. Данная струк тура выглядит так:
Все элементы этой структуры кратко описаны в таблице 5-1.
Таблица 5-1. Элементы структуры JOBOBJECT_BASIC_LIMIT_INFORMATION Хочу пояснить некоторые вещи, связанные с этой структурой, которые, по-моему довольно туманно изложены в документации Platform SDK, Указывая ограничения для задания, Вы устанавливаете те или иные биты в элементе LimitFlags. Например, в StartRestrictedProcess я использовал флаги JOB_OBJECT_LIMIT_PRIORITY_CLASS и JOB_ OBJECT_LIMIT_JOB_TIME, т. e. определил всего два ограничения. При выполнении задание ведет учет по нескольким показателям — например, сколько процессорного времени уже использовали его процессы. Всякий раз, когда Вы устанавливаете базовые ограничения с помощью флага JOB_OBJECT_LIMIT_JOB_ TIME, из общего процессорного времени, израсходованного всеми процессами, вы читается то, которое использовали завершившиеся процессы. Этот показатель сооб щает, сколько процессорного времени израсходовали активные на данный момент процессы, А что если Вам понадобится изменить ограничения на доступ к подмно жеству процессоров, не сбрасывая при этом учетную информацию по процессорно му времени? Для этого Вы должны ввести новое базовое ограничение флагом JOB_OB JECT_LIMIT_AFFINITY и отказаться от флага JOB_OBJECT_LIMIT_JOB_TIME. Но тогда получится, что Вы снимаете ограничения на процессорное время. Вы хотели другого: ограничить доступ к подмножеству процессоров, сохранив существующее ограничение на процессорное время, и не вычитать время, израсхо дованное завершенными процессами, из общего времени. Чтобы решить эту пробле му, используйте специальный флаг JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME. Этот флaг и JOB_OBJECT_LIMIT_JOB_TIME являются взаимоисключающими. Флаг JOB_OB JECT_LIMIT_PRESERVE_JOB_TIME указывает системе изменить ограничения, не вычи тая процессорное время, использованное уже завершенными процессами. Обсудим также элемент SchedulingCtoss структуры JOBOBJECT_BASIC_LIMIT_INFOR MATION. Представьте, что для двух заданий определен класс приоритета NORMAL_ PRIORITY_CLASS, а Вы хотите, чтобы процессы одного задания получали больше про цессорного времени, чем процессы другого. Так вот, элемент SchedulingClass позволя ет изменять распределение процессорного времени между заданиями с одинаковым классом приоритета. Вы можете присвоить ему любое значение в пределах 0-9 (по умолчанию он равен 5). Увеличивая сго значение, Вы заставляете Windows 2000 вы делять потокам в процессах конкретного задания более длительный квант времени, а снижая — напротив, уменьшаете этот квант, Допустим, у меня есть два задания с обычным (normal) классом приоритета: в каждом задании — по одному процессу, а в каждом процессе — по одному потоку (тоже с обычным приоритетом). В нормальной ситуации эти два потока обрабатыва лись бы процессором по принципу каруссли и получали бы равные кванты процес сорного времени. Но если я запишу в элемент SchedulingClass для первого задания значение 3, система будет выделять его потокам более короткий квант процессорно го времени, чсм потокам второго задания. Используя SchedulingClass, избегайте слишком больших его значений, иначе Вы замедлите общую реакцию других заданий, процессов и потоков на ка-кие-либо со бытия в системе. Кроме того, учтите, что все сказанное здесь относится только к Windows 2000. В будущих версиях Windows планировщик потоков предполагается существенно изменить, чтобы операционная система могла более гибко планировать потоки в заданиях и процессах. И последнее ограничение, которое заслуживает отдельного упоминания, связано с флагом JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION. Он отключает для всех процессов в задании вывод диалогового окна с сообщением о необработанном исключении. Система реагирует на этот флаг вызовом SetErrorMode с флагом SEM_NOG PFAULTERRORBOX для каждого из процессов в задании Процесс, в котором возник нет необрабатываемое им исключение, немедленно завершается бсз уведомления пользователя. Этот флаг полезен в сервисных и других пакетных заданиях. В его от сутствие один из процессов в задании мог бы вызвать исключение и не завершиться, впустую расходуя системные ресурсы. Помимо базовых ограничений, Вы можстс устанавливать расширенные, для чего применяется структура JOBOBJECT_EXTENDED_LIMIT_INFORMATION:
Как видите, она включает структуру JOBOBJECT_BASIC_LIMIT_INFORMATION, яв ляясь фактически ее надстройкой, Это несколько странная структура, потому что в ней есть элементы, не имеющие никакого отношения к определению ограничений для задания. Во-первых, элемент IoInfo зарезервирован, и Вы ни в коем случае не дол жны обращаться к нему. О том, как узнать значение счетчика ввода-вывода, я расска жу позже Кроме того, элементы PeakProcessMemoryUsed и PeakJobMemoryUsed пред назначены только для чтения и сообщают о максимальном объеме памяти, передан ной соответственно одному из процессов или всем процессам в задании. Остальные два элемента, ProcessMemoryLimit и JobMemoryLimit, ограничивают со ответственно объем переданной памяти, который может быть использован одним из процессов или всеми процессами в задании Чтобы задать любое из этих ограниче ний, укажите в элементе LimitFlags флаг JOB_OBJECT_LIMIT_JOB_MEMORY или JOB_OB JECT_LIMIT_PROCESS_MEMORY. А теперь вернемся к прочим ограничениям, которые можно налагять на задания. Структура JOBOBJECT_BASIC_UI_RESTRICTIONS выглядит так:
В этой структуре всего один элемент, UIRestrictionsClass, который содержит набор битовых флагов, кратко описанных н таблице 5-2.
Таблица 5-2. Битовые флаги базовых ограничений по пользовательскому интерфейсу дпя объекта-задания Последний флaг, JOB_OBJECT_UILIMIT_HANDLES, представляет особый интерес: он запрещает процессам в задании обращаться к USER-объектам, созданным внешними по отношению к этому заданию процессами. Так, запустив утилиту Microsoft Spy++ из задания, Вы не обнаружите никаких окон, кроме тех, которые создаст сама Spy++. Ha рис. 5-2 показано окно Microsoft Spy++ с двумя открытыми дочерними MDI-окнами. Заметьте, что в левой секции (Threads 1) содержится список потоков в системе. Ка жется, что лишь у одного из них, 000006АС SPYXX, есть дочерние окна. А все дело в том, что я запустил Microsoft Spy++ из задания и ограничил ему права па использова ние описателей USER-объектов. В том же окне сообщается о потоках MSDEV и EXPLO RER, но никаких упоминаний о созданных ими окнах нет. Уверяю Вас, эти потоки наверняка создали какие-нибудь окна — просто Spy++ лишена возможности их ви деть. В правой секции (Windows 3) утилита Spy++ должна показывать иерархию окон на рабочем столе, но там нет ничего, кроме одного элемента — 00000000. (Это не настоящий элемент, но Spy++ была обязана поместить сюда хоть что-нибудь.) Обратите внимание, что такие oграничения односторонни, т e. внешние процес сы все равно видят USER-объекты, которые созданы процессами, включенными в за дание. Например, если запустить Notepad в задании, a Spy++ — внс сго, последняя увидит окно Notepad, даже если для задания указан флаг JOB_OBJECT_UILIMIT_HAND LES Кроме того, Spy++, запущенная в отдельном задании, все равно увидинт окно Notepad, если только для ее задания не установлен флаг JOB_OBJECT_UILIMIT_HAN DLES.
Рис. 5-2. Microsoft Spy++ работает в задании, которому ограничен доступ к описателям USER-объектов Ограничение доступа к описателям USER-объектов — вещь изумительная, если Вы хотите создать по-настоящему безопасную песочницу, в которой будут «копаться" процессы Вашего задания. Однако часто бывает нужно, чтобы процесс в задании взаимодействовал с внешними процессами. Одно из самых простых решений здесь — использовать оконные сообщения, но, ссли процессам в задании доступ к описате лям пользовательского интерфейса запрещен, ни один из них не сможет послать со общение (синхронно или асинхронно) окну, созданному внешним процессом К счастью,теперь есть функция, которая поможет решить эту проблему:
Параметр hUserObj идентифицирует конкретный USER-объект, доступ к которому Вы хотите предоставить или запретить процессам в задании. Это почти всегда опи сатель окна, но USER объектом может быть, например, рабочий стол, программная ловушка, ярлык или меню Последние два параметра, hjob и fGrant, указывают на зада ние и вид ограничения. Обратите внимание, что функция не сработает, если ее выз вать из процесса в том задании, на которое указывает hjob, — процесс нс имеет права сам себе предоставлять доступ к объекту. И последний вид ограничений, устанавливаемых для задания, относится к защи те. (Введя в действие такие ограничения, Вы не сможете их отменить) Структура JOBOBJECT_SECURITY_LIMIT_INFORMATION выглядит так.
Ее элементы описаны в следующей таблице
Естественно, если Вы налагаете ограничения, то потом Вам, наверное, понадобится информация о них. Для этого вызовите:
В эту функцию, как и в SetInformationJobObject, передается описатель задания, пе ременная перечислимого типа JOBOJECTINFOCLASS. Она сообщает информацию об ограничениях, адрес и размер структуры данных, инициализируемой функцией. Пос ледний параметр, pdwReturnLength, заполняется самой функцией и указывает, сколь ко байтов помещено в буфер Если эти сведения Вас не интересуют (что обычно и бывает), передавайте в этом параметре NULL.
Включение процесса в заданиеО'кэй, с ограничениями па этом закончим Вернемся к StartRestrictedProcess. Устано вив ограничения для задания, я вызываю CreateProcess и создаю процесс, который помещаю в это задание. Я использую здесь флаг CREATE_SUSPENDED, и он приводит к тому, что процесс порождается, но код пока не выполняет. Поскольку StartRestricted Process вызывается из процесса, внешнего по отношению к заданию, его дочерний процесс тоже не входит в это задание. Если бы я разрешил дочернему процессу не медленно начать выполнение кода, он проигнорировал бы мою песочницу со всеми ее ограничениями. Поэгому сразу после создания дочернего процесса и перед нача лом его работы я должен явно включить этот процесс в только что сформированное задание, вызвав:
Эта функция заставляет систему рассматривать процесс, идентифицируемый па раметром hProcess, как часть существующего задания, на которое указывает hJob. Об ратите внимание, что AssignProcessToJobObject позволяет включить в задание только тот процесс, который еще не относится ни к одному заданию. Как только процесс стал частью какого-нибудь задания, его нельзя переместить в другое задание или отпус тить на волю. Кроме того, когда процесс, включенный в задание, порождает новый процесс, последний автоматически помещается в то же задание. Однако этот поря док можно изменить.
Что касается StartRestrictedProcess, то после вызова AssignProcessToJobObject новый процесс становится частью задания. Далее я вызываю ResumeThread, чтобы поток нового процесса начал выполняться в рамках ограничений, установлепных для зада ния. В этот момент я также закрываю описатель потока, поскольку он мне больше не нужен. Завершение всех процессов в заданииУверен, именно это Вы и будете делать чаще всего. В начале главы я упомянул о том, как непросто остановить сборку в Developer Studio, потому что для этого ему должны быть известны все процессы, которые успел создать его самый первый процесс. (Это очень каверзная задача. Как Developer Studio справляется с ней, я объяснял в своей колонке «Вопросы и ответы по Win32» в июньском выпуске Microsoft Systems Journal за 1998 год.) Подозреваю, что следующие версии Developer Studio будут использовать механизм заданий, и решать задачу, о которой мы с Вами говорили, станет гораздо легче. Чтобы уничтожить все процессы в задании, Вы просто вызываете
Вызов этой функции похож на вызов TerminateProcessw для каждого процесса в за дании и присвоение всем кодам завершения одного значения — uExitCode. Получение статистической информации о заданииМы уже обсудили, как с помощью QueryInformationJobObject получить информацию о текущих ограничениях, установленных для задания. Этой функцией можно пользо ваться и для получения статистической информации. Например, чтобы выяснить ба зовые учетные сведения, вызовите ее, передав JobObjeсtBasicAccountingInformation во втором параметре и адрес структуры JOBOBJECT_BASIC_ACCOUNTING_INFORMATION:
Элементы этой структуры кратко описаны в таблице 5-3
Таблица 5-3. Элементы структуры JOBOBJECT_BASIC_ACCOUNTING_INFORMATION Вы можете извлечь те же сведения вместе с учетной информацией по вводу-выво ду, передав JobObjectBasicAndIoAccountingInformation во втором параметре и адрес структуры JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION:
Как видите, она просто возвращает JOBOBJECT_BASIC_ACCOUNTlNG_INFORMA TION и IO_COUNTERS. Последняя структура показана на следующей странице
Она сообщает о числе операций чтения, записи и перемещения (а также о коли честве байтов, переданных при выполнении этих операций) Данные относятся ко всем процессам в задании Кстати, новая функция GetProcessIoCounters позволяет по лучить ту же информацию о процессах, не входящих ни в какие задания
QueryInformationJobObject такжe возвращает набор идентификаторов текущих про цессов в задании Но перед этим Вы должны прикинуть, сколько их там может быть, и выделить соответствующий блок памяти, где поместятся массив идентификаторов и структура JOBOBJECT_BASIC_PROCESS_ID_LIST
В итоге, чтобы получить набор идентификаторов текущих процессов в задании, нужно написать примерно такой код
Вот и все, что Вам удастся получить через эти функции, хотя на самом деле опе рационная система знает о заданиях гораздо больше. Эту информацию, которая хра нится в специальных счетчиках, можно извлечь с помощью функций из библиотеки Performance Data Helper (PDH dIl) или через модуль Performance Monitor, подключае мый к Microsoft Management Console (MMC) Рис 5-3 иллюстрирует некоторые из доступных в системе счетчиков заданий (job object counters), а рис. 5-4 — счетчики, относящиеся к отдельным параметрам заданий (job object details counters) Заметьте, что в чадании Jeff содержится четыре процесса calc, cmd, notepad и wordpad.
Рис. 5-3. MMC Performance Monitor счетчики задания
Рис. 5-4. MMC Performance Monitor счетчики, относящиеся к отдельным параметрам задания Извлечь сведения из этих счетчиков Вы сможете только для тех заданий, которым были присвоены имена при вызове CreateJobObject. По этой причине, наверное, луч ше всегда именовать задания, даже если Вы и не собираетесь ссылаться на них по именам из других процессов. Уведомления заданийИтак, базовые сведения об объектах-заданиях я изложил. Единственное, что осталось рассмотреть, — уведомления Допустим, Вам нужно знать, когда завершаются все про цессы в задании или заканчивается все отпущенное им процессорное время. Либо выяснить, когда в задании порождается или уничтожается очередной процесс Если такие уведомления Вас не интересуют (а во многих приложениях они и не нужны), работать с заданиями будет очень легко — не сложнее, чем я уже рассказывал. Но если они все же понадобятся, Вам придется копнуть чуть поглубже Информацию о том, все ли выделенное процессорное время исчерпано, получить нетрудно. Объекты-задания не переходят в свободное состояние до тех пор, пока их процессы нс израсходуют отведенное процессорное время Как только оно заканчи вается, система уничтожает всс процессы в задании и переводит его объект в свобод ное состояние (signaled scate). Это событие легко перехватить с помощью WaitFor SingleObject (или похожей функции). Кстати, потом Вы можете вернуть объект-зада ние в состояние «занято" (nonsignaled state), вызвав SetInformationJobObject и выделив емудополншельное процессорное время. Когда я только начинал разбираться с заданиями, мне казалось, что объект-зада ние должен переходить в свободное состояние после завершения всех его процес сов. В конце концов, прекращая свою работу, объекты процессов и потоков освобож даются, то же самое вроде бы должно происходить и с заданиями. Нo Microsoft пред почла сделать по-другому объект-задание переходит в свободное состояние после того, как исчерпает выделенное ему время Поскольку большинство заданий начина ет свою работу с одним процессом, который существует, пока не завершатся все eго дочерние процессы, Вам нужно просто следить за описателем родительского процес са — он освободится, как только завершится все задание. Моя функция StartRestricted Зrocess как раз и демонстрирует данный прием Но это были лишь простейшие уведомления — более «продвинутые", например о создании или разрушении процесса, получать гораздо сложнее. В частности, Вам придется создать объект ядра «порт завершения ввода-вывода" и связать с ним объект или объекты «задание". После этого нужно будет перевести один или больше пото ков в режим ожидания порта завершения. Создав порт завершения ввода-вывода. Вы сопоставляете с ним задание, вызывая SetInformationJobObject следующим образом:
После выполнения этого кода система начнет отслеживать задание и при возник новении событий передавать их порту завершения. (Кстати, Вы можете вызывать QueryInformationJobQbjectw получать ключ завершения и описатель порта, по врядли это Вам когда-нибудь понадобится ) Потоки следят за портом завершения ввода-вы вода, вызывая GetQueuedCompletionStatus.
Когда эта функция возвращает уведомление о событии задания, *pCompletionKey содержит значение ключа завершения, заданное при вызове SetInformationJobObjett для связывания задания с портом завершения По нему Вы узнаете, в каком из заданий возникло событие Значение в *pNumBytesTransferred указывет какое именно собы тие произошло (таблица 5-4). В зависимости от конкретного события в *pOverlapped может возвращаться идентификатор процесса.
Таблица 5-4. Уведомления о событиях задания, посылаемые системой связанному с этим заданием порту завершения И последнее замечание: по умолчанию объект-задание настраивается системой на автоматическое завершение всех его процессов по истечении выделенного ему про цессорного времени, а уведомление JOB_OBJECT_MSG_END_OF_JOB_TIME не посы лается. Если Вы хотите, чтобы объект-задание не уничтожал свои процессы, а просто сообщал о превышении лимита на процессорное время, Вам придется написать при мерно такой код:
Вы можете указать и другое значение, JOB__OBJECT_TERMINATE_AT_END_OF_JOB, но оно задается по умолчанию, еще при создании задания Программа-пример JobLabЭтапрограмма, "05КJobLab.ехе»(см листингнарис 5-6),позволяет легко эксперимен тировать с заданиями Ее файлы исходного кода и ресурсов находятся в каталоге 05-JobLab на компакт-диске, прилагаемом к книге После запуска JobLab открывается окно, показанное на рис 5-5
Рис. 5-5. Программа-пример JobLab Когда процесс инициализируется, он создает объект «задание» Я присваиваю ему имя JobLab, чтобы Вы могли наблюдать за ним с помощью MMC Performance Monitor Моя программа также создает порт завершения ввода-вывода и связывает с ним объ ект-задание Это позволяет отслеживать уведомления от задания и отображать их в списке в нижней части окна Изначально в задании нет процессов, и никаких ограничений для него не уста новлено. Поля в верхней части окна позволяют задавать базовые и расширенные ог раничения Все, что от Вас требуется, — ввести в них допустимые значения и щелк нуть кнопкуАрр1у Limits Если Вы оставляете поле пустым, соответствующие ограни чения не вводятся Кроме базовых и расширенных, Вы можете задавать ограничения по пользовательскому интерфейсу Обратите внимание помечая флажок PreserveJob Time When Applymg Limits, Вы не устанавливаете ограничение, а просто получаете возможность изменять ограничения, не сбрасывая значения элементов ThisPeriod- TotalUserTime и ThisPeriodTotalKemelTime при запросе базовой учетной информации. Этот флажок становится недоступен при наложении ограничений на процессорное время для отдельных заданий. Остальные кнопки позволяют управлять заданием по-другому. Кнопка Terminate Processes уничтожает все процессы в задании. Кнопка Spawn CMD In Job запускает командный процессор, сопоставляемый с заданием Из этого процесса можно запус кать дочерние процессы и наблюдать, как они ведут себя, став частью задания И пос ледняя кнопка, Put PID In Job, позволяет связать существующий свободный процесс с заданием (т. e. включить его в задание). Список в нижней части окна отображает обновляемую каждые 10 секунд инфор мацию о статусе задания, базовые и расширенные сведения, статистику ввода-выво да, а также пиковые объемы памяти, занимаемые процессом и заданием. Кроме этой информации, в списке показываются уведомления, поступающие от задания в порт завершения ввода-вывода. (Кстати, вся информация обновляется и при приеме уведомления.) И еще одно: если Вы измените исходный код и будете создавать безымянный объект ядра «задание», то сможете запускать несколько копий этой программы, со здавая тем самым два и более объектов-заданий на одной машине. Это расширит Ваши возможности в экспериментах с заданиями. Что касается исходного кода, то специально обсуждать его нет смысла — в нем и так достаточно комментариев. Замечу лишь, что в файле Job.h я определил С++-класс CJob, инкапсулирующий объект "задание» операционной системы. Эти избавило меня от необходимости передавать туда-сюда описатель задания и позволило уменьшить число операций приведения типов, которые обычно приходится выполнять при вы зове функций QuerylnformationJobObject и SetInformationJobObject. |
|
| бодибилдинг | Строим Домик | RU-домены за 170 рублей | Ноутбуки, мониторы, комлектующие и другие полезные вещи
Copyright © "В помощь Веб-Мастеру" (Alexander D. Belyaev) 2005-2008. При перепечатке любого материала видимая ссылка на источник "В помощь Веб-Мастеру" и все имена, ссылки авторов обязательны! Время генерации страницы: 0.154 |