/ Language: Русский / Genre:computers

1.Внутреннее устройство MS Windows (гл. 1-4)

М.Руссинович

Книга посвящена внутреннему устройству и алгоритмам работы основных компонентов операционной системы Microsoft Windows - Windows Server 2003, Windows XP и Windows 2000 - и файловой системы NTFS. Детально рассмотрены системные механизмы: диспетчеризация ловушек и прерываний, DPC, APC, LPC, RPC, синхронизация, системные рабочие потоки, глобальные флаги и др. Также описываются все этапы загрузки операционной системы и завершения ее работы. B четвертом издании книги больше внимания уделяется глубокому анализу и устранению проблем, из-за которых происходит крах операционной системы или из-за которых ее не удается загрузить. Кроме того, рассматриваются детали реализации поддержки аппаратных платформ AMD x64 и Intel IA64. Книга состоит из 14 глав, словаря терминов и предметного указателя. Книга предназначена системным администраторам, разработчикам серьезных приложений и всем, кто хочет понять, как устроена операционная система Windows. Названия всех команд, диалоговых окон и других интерфейсных элементов операционной системы приведены как на английском языке, так и на русском. Версия Fb2 редакции - 1.0. Об ошибках просьба сообщать по адресу - general2008@ukr.net.

М.Руссинович, Д.Соломон

1- Внутреннее устройство MS Windows (гл. 1-4)

Microsoft Windows:

Windows Server™ 2003, Windows XP и Windows 2000

4-е издание

Посвящается Дэйву Катлеру - отцу ядра Windows

Предыстория

Я вновь признателен Дэвиду Соломону (David Solomon) и Марку Руссиновичу (Mark Russinovich) за то, что они предоставили мне возможность сказать несколько слов о новом издании их книги «Внутреннее устройство Microsoft Windows». Прошло уже более трех лет с момента выхода последнего издания этой книги, и за это время на свет появились два выпуска операционной системы Windows - очень значимые обновления клиентской и серверной систем.

Перед авторами стояли две задачи, которые постоянно усложняются: отслеживание эволюционного развития системы Microsoft Windows NT и документирование того, как менялась реализация ее компонентов в каждой версии. B этом смысле авторы проделали просто выдающуюся работу.

(Слева направо) Дэвид Соломон, Дэвид Катлер и Марк Руссинович

Впервые я познакомился с Дэвидом Соломоном, когда ему было всего 16 лет, а я работал в Digital Equipment Corporation над операционной системой VMS для VAX. C тех пор он участвовал в разработке операционных систем, а также преподавал в этой области. C Марком Руссиновичем я познакомился позже, но уже задолго до этого был немало наслышан о его глубоких познаниях в области операционных систем. B числе его заслуг версия файловой системы NTFS, которую он заставил работать в Microsoft Windows 98, и «живой» отладчик ядра Windows, позволяющий заглянуть внутрь системы Windows в процессе ее выполнения.

Истоки Windows NT восходят к октябрю 1988 года, когда было решено создать переносимую операционную систему, совместимую с OS/2, поддерживающую POSIX и многопроцессорную обработку, обладающую высокой защищенностью, надежностью и интегрированными средствами работы в сетях. C приходом Windows 3.0 и ее колоссальным успехом заявленные цели несколько изменились: совместимость с OS/2 была перенесена с уровня всей системы на уровень отдельной подсистемы.

Поначалу мы полагали, что сумеем создать Windows NT за пару лет, но в действительности ее первая версия вышла лишь через четыре с половиной года - летом 1993-го. Эта версия поддерживала процессоры Intel i386, Intel i486 и MIPS R4000. Шесть недель спустя мы ввели поддержку и для процессоров Digital Alpha.

Первая версия Windows NT получилась более громоздкой и медленной, чем ожидалось, так что следующей вехой стал проект Daytona (так называется автострада во Флориде). Главной целью в этой версии было уменьшение размера системы, повышение ее быстродействия и, разумеется, надежности. Через полгода после выпуска Windows NT 3.5 осенью 1994-го мы подготовили Windows NT 3.51, которая представляла собой обновленную версию с дополнительной поддержкой процессора IBM PowerPC.

Толчком к созданию следующей версии Windows NT стало желание сделать пользовательский интерфейс, совместимый с Windows 95, и включить технологии Cairo, уже находившиеся в разработке пару лет. Ha создание этой системы ушло еще два года, и летом 1996 года была представлена Windows NT 4.0.

Название следующей версии NT было изменено на Windows 2000. Она стала последней системой, для которой одновременно выпускались клиентская и серверная версии. Windows 2000 была построена на той же технологии Windows NT, что и предыдущие версии, но обладала новой важной функциональностью, поддерживая, в частности, Active Directory. Ha разработку Windows 2000 ушло три с половиной года, и на тот период она была самой оптимизированной и наиболее тщательно протестированной версией технологии Windows NT Windows 2000 стала кульминацией более чем одиннадцатилетних разработок, реализованных на четырех архитектурах.

B конце разработки Windows 2000 мы приступили к работе над амбициозным планом реализации новых версий клиентской и серверной систем, которые должны были предоставить новые, более совершенные возможности потребителям и улучшить характеристики серверов. Ho потом стало ясно, что реализация серверных средств привела бы к задержке в реализации клиентских, и поэтому было решено разделить выпуски. B августе 2001 года на свет появились Windows XP Professional и Windows XP Home Edition, a через год с небольшим, в марте 2003 года была выпущена Microsoft Windows Server 2003. Помимо архитектуры Intel x86, эти системы поддерживали Intel IA64, благодаря чему Windows NT впервые вышла на стезю 64-разрядных вычислений.

Эта книга единственная, где так глубоко и полно рассмотрены внутренние структуры и принципы функционирования Windows XP и Windows Server

2003. Кроме того, она предлагает заглянуть в будущее - перевод Windows на 64-разрядные «рельсы», т. е. на ее поддержку архитектур x64 (AMD64) и Intel EM64T, объявленных AMD в 2003 году и Intel в феврале 2004 года соответственно. Выпуск клиентской и серверной версий с полной поддержкой x64 запланирован на первую половину 2005 года, и в этой книге содержится масса информации о внутренних деталях реализации х64-системы.

Архитектура х64 - это начало новой эры для Windows NT в тот момент, когда время архитектуры x86 подходит к концу. Архитектура x64 обеспечивает совместимость с 32-разрядной х86-платформой и предоставляет 64-разрядную адресацию для наиболее требовательных, совершенно новых приложений. Это позволит сохранить инвестиции в 32-разрядное программное обеспечение, в то же время вдохнув новую жизнь в Windows NT на ближайшее десятилетие или даже на более длительный период.

Хотя название NT-системы за последние несколько лет неоднократно менялось, она по-прежнему полностью основана на исходной кодовой базе Windows NT. Ho время бежит, появляются новые технологии, и реализация многих внутренних компонентов и функций значительно изменилась. Авторы проделали внушительную работу, собрав столько детальной информации о кодовой базе Windows NT и ее реализациях в разных выпусках на разных платформах, а также создав примеры и утилиты, которые помогают читателю разобраться в том, как работают компоненты и подсистемы Windows. Экземпляр этой книги должен лежать на столе у каждого разработчика серьезного программного обеспечения.

Дэвид H. Катлер, заслуженный старший инженер корпорации Microsoft

Предисловие

Microsoft Windows была частью моей жизни целых 14 лет. За это время - от версии к версии - наша операционная система развивалась вширь и вглубь. Сегодня работа над Windows - один из самых важных и сложных проектов в мире. B выпуске Windows участвуют более 5000 инженеров. Среди пользователей Windows есть представители уже почти всех культур, она используется как на крупных предприятиях, так и маленькими детьми. Пользователи Windows постоянно требуют ее совершенствования практически во всех сферах - от эффективной работы на крупнейших серверах до применения в дошкольном обучении. Windows поставляется в самых разных ипостасях - от встраиваемых версий и выпусков для медиа-центров до редакций для центров обработки данных. Все эти продукты опираются на одни и те же базовые компоненты Windows, которые развиваются и совершенствуются в каждой новой версии.

Это фундаментальная книга о внутреннем устройстве базовых компонентов Windows. Если вы хотите как можно быстрее освоить принципы внутренней работы Windows, тогда эта книга для вас. Освоение всех частей столь основательного продукта - задача устрашающая. Ho если вы начнете с базовых концепций системы, сложить фрагменты головоломки воедино будет гораздо легче. C эволюцией самой Windows развивается и эта книга - сейчас публикуется ее четвертое издание. Мы уже давно используем ее для обучения новых сотрудников Microsoft, так что предлагаемые вам материалы проверены на практике.

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

Дэвид и Марк проделали превосходную работу, написав книгу о технической «изнанке» Windows. A инструменты, которые они предлагают вам, - отличное средство для самостоятельного обучения и диагностики. Прочитав эту книгу, вы будете гораздо лучше понимать, как взаимодействуют между собой различные компоненты и подсистемы, какие усовершенствования внесены в новую версию и как выжать из них максимум возможного.

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

Джим Олчин, вице-президент группы платформ корпорации Microsoft

Благодарности

B первую очередь мы хотим особо поблагодарить следующих людей.

(o) Дэйва Катлера (Dave Cutler), заслуженного старшего инженера и первого архитектора Microsoft Windows NT. Дэйв разрешил Дэвиду Соломону (David Solomon) доступ к исходному коду и всячески поддерживал его преподавательскую деятельность, посвященную объяснению деталей внутреннего устройства Windows NT, а также его работу над вторым и третьим изданием книги. Помимо рецензирования главы по процессам и потокам, Дэйв ответил на массу вопросов об архитектуре ядра и написал об истории создания Windows для нашей книги.

(o) Джима Олчина Jim Allchin), нашего главного спонсора, - за предисловие к этой книге и за отстаивание интересов нашего дела в Microsoft.

(o) Роба Шорта (Rob Short), вице-президента, который позаботился о том, чтобы нам предоставили ресурсы и доступ к нужным людям.

Мы также выражаем признательность двум разработчикам из отдела Windows за подготовку новых материалов, включенных в это издание:

(o) Адриану Маринеску (Adrian Marinescu), который написал заметно разросшийся раздел по диспетчеру куч в главе, где рассматривается диспетчер памяти.

(o) Самеру Арафеху (Samer Arafeh), который предоставил материалы по Wow64.

Спасибо нашему старому приятелю, Джеффри Рихтеру Jeffrey Richter), с которым мы часто вместе обедаем, за врезку «Как насчет .NET и WinFX?» в главе 1 и за постоянное напоминание о том, как мало людей, по-настоящему интересующихся тем, о чем мы говорим в своей книге.

B этой книге не было бы такой глубины и точности изложения технических сведений без поддержки, замечаний и предложений ключевых членов команды разработчиков Microsoft Windows. Вот эти люди:

Были и другие, кто отвечал на наши вопросы в коридорах или кафетериях, - если мы вас пропустили, пожалуйста, простите нас!

Мы также выражаем благодарность Джейми Ханрахан Jamie Hanrahan) из Azius Developer Training (www.azius.com), которая в соавторстве с Дэвидом подготовила учебный курс по внутренней архитектуре исходной версии Windows. Ha основе этого курса было написано второе издание этой книги. Джейми, у которой настоящий талант доходчиво объяснять сложнейшие вещи, предоставила нам отдельные материалы, а также ряд схем и иллюстраций.

Спасибо Дэйву Проберту (Dave Probert) за то, что разместил в сети наши черновые материалы для распространения среди рецензентов внутри Microsoft.

Благодарим Джонатана Славза (Jonathan Sloves) из AMD, с помощью которого нам предоставили тестовые системы AMD64; они очень помогли нам в написании материалов по 64-разрядной архитектуре и в переносе ряда утилит Sysinternals на платформу x64.

Наконец, мы хотим выразить благодарность следующим сотрудникам Microsoft Press за их вклад в эту книгу.

(o) Робину Ван-Штеенбергу (Robin van Steenburgh), рецензенту издательства, за терпение в работе с нами над этим проектом.

(o) Салли Стикни (Sally Stickney), которая на первых порах по-прежнему была редактором нашего проекта, но потом ее закрутил водоворот административных дел. Мы очень скучали без вас!

(o) Валери Вулли (Valerie Woolley), которая приняла бразды правления от Салли и стала нашим новым редактором проекта. Вы замечательная и не такая резкая, как Салли!

(o) РоджеруЛебланку (Roger LeBlanc), который одолел все главы и сумел сократить в них текст, найти несогласованности и вообще довести нашу рукопись до высоких стандартов Microsoft Press.

Дэвид Соломон иМаркРуссинович сентябрь 2004 г.

Введение

Четвертое издание этой книги ориентировано на квалифицированных специалистов (программистов, разработчиков и системных администраторов), желающих разобраться в принципах внутренней работы основных компонентов операционных систем Microsoft Windows 2000, Microsoft Windows XP и Microsoft Windows Server 2003. Зная их, разработчики смогут принимать более эффективные решения на этапах проектирования приложений для платформы Windows. Такие знания помогут программистам и в отладке - при устранении сложных проблем. Информация, изложенная в книге, будет также полезна системным администраторам: понимание того, как устроена и работает операционная система, упростит им оптимизацию своих систем и устранение неполадок в случае каких-либо сбоев. Прочитав эту книгу, вы лучше поймете, как функционирует Windows и почему она ведет себя именно так, а не как-то иначе.

Структура книги

Первые две главы закладывают фундамент, вводя термины и концепции, используемые во всей книге. Следующие три главы описывают ключевые механизмы операционной системы. B следующих восьми главах детально рассматриваются базовые компоненты Windows - процессы, потоки и задания, управление памятью, защита, подсистема ввода-вывода, управление внешней памятью, диспетчер кэша, файловые системы и поддержка сетей. Наконец, в последней главе поясняется, как проводить анализ аварийных дампов памяти.

История написания книги

Это четвертое издание книги, которая изначально называлась «Inside Windows NT» (Microsoft Press, 1992) и была написана Хелен Кастер (Helen Custer) еще до выпуска Microsoft Windows NT 3.1. Она стала первой книгой по Windows NT и представляла собой глубокий обзор архитектуры этой системы. Второе издание, «Inside Windows NT» (Microsoft Press, 1998), было написано Дэвидом Соломоном. B него вошла новая информация по Windows NT 4.0, а сама книга стала гораздо более глубокой. Третье издание, «Inside Windows 2000» (Microsoft Press, 2000), было подготовленоДэвидом Соломоном и Марком Руссиновичем. B нем появилось много новых тематических разделов, в том числе по этапам загрузки и завершения работы системы, внутреннему устройству сервисов и реестра, по драйверам файловых систем, поддержке

сетей, а также по новой функциональности ядра Windows 2000, например модели WDM, Plug and Play, WMI, шифрованию, Terminal Services и др.

Особенности четвертого издания

Новое издание дополнено информацией об изменениях в ядре, которые были внесены в Windows XP и Windows Server 2003, в том числе касающихся поддержки 64-разрядных систем. Материалы для экспериментов также были обновлены, чтобы отразить изменения в усовершенствованных утилитах и научить вас пользоваться новыми инструментами, которых не было на момент подготовки третьего издания.

Так как отличия новых версий Windows от Windows 2000 относительно невелики (по сравнению с различиями между Windows NT 4.0 и Windows 2000), основная часть книги равно применима к Windows 2000, Windows XP и Windows Server 2003. Поэтому, если не оговорено иное, все сказанное относится ко всем трем версиям.

Инструменты для проведения экспериментов

Даже без доступа к исходному коду существующие инструменты вроде отладчика ядра позволяют многое прояснить во внутреннем устройстве Windows. B том месте, где для демонстрации какого-либо аспекта поведения Windows используется тот или иной инструмент, во врезке «Эксперимент» даются инструкции по его применению. Такие врезки часто встречаются в книге, и мы рекомендуем вам проделывать эти эксперименты в процессе чтения: наглядно увидев, как ведет себя Windows в конкретной ситуации, вы гораздо лучше усвоите прочитанный материал.

Тематика, не рассматриваемая в книге

Windows - большая и сложная операционная система. Нельзя объять необъятное, и поэтому основное внимание в книге уделяется только базовым системным компонентам. Например, мы не рассматриваем COM+, инфраструктуру объектно-ориентированного программирования распределенных приложений для Windows, или .NET Framework, платформу для следующего поколения приложений с управляемым кодом.

Поскольку наша книга о внутреннем устройстве Windows, а не о том, как пользоваться этой операционной системой, программировать для нее или администрировать системы, созданные на ее основе, вы не найдете здесь никаких сведений об использовании, программировании и конфигурировании Windows.

Подводные камни

B книге описываются недокументированные внутренние структуры и функции ядра, архитектура и различные аспекты внутренней работы Windows, и часть таких структур и функций может измениться в следующем выпуске этой операционной системы. (Впрочем, внешние интерфейсы вроде Windows API всегда сохраняют совместимость с аналогичными интерфейсами в очередных выпусках.)

Говоря «может измениться», мы не имеем в виду, что детали устройства системы обязательно изменятся в следующем выпуске, а лишь обращаем внимание на то, что достоверность информации гарантируется исключительно для данных версий. Любое программное обеспечение, использующее недокументированные интерфейсы, может перестать работать в будущих версиях Windows. Более того, такое программное обеспечение, если оно работает в режиме ядра (как, например, драйверы устройств), может привести к краху более новых версий Windows.

Техническая поддержка

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

От авторов

Эта книга отнюдь не совершенна. Несомненно в ней есть какие-то неточности; может быть, мы упустили что-то важное. Если вы найдете то, что считаете ошибочным, или если вы сочтете, что в книгу следует включить дополнительный материал, пожалуйста, пошлите свое сообщение по адресу win dowsinternals@sysinternals.com. Обновления и исправления будут выкладываться на страницу wwwsysinternals.com/windowsinternals*

От Microsoft Press

Microsoft публикует исправления к книгам по адресу http://www.microsoftcom/learning/'support. Для прямого подключения к Microsoft Learning Knowledge Base и ввода запроса по проблеме, с которой вы столкнулись, заходите на страницу http://www.microsoft.comflearning/support/search.asp.

B переводе учтены исправления, опубликованные на этой Web-странице, по состоянию на 1 июля 2005 года. - Прим. перев.

Г Л A B A 1 Концепции и инструменты

Г Л A B A 1 Концепции и инструменты

B этой главе мы познакомим вас с основными концепциями и терминами операционной системы Microsoft Windows, которые будут использоваться в последующих главах, в том числе с Windows API, процессами, потоками, виртуальной памятью, режимом ядра и пользовательским режимом, объектами, описателями (handles), защитой, реестром. Мы также расскажем об инструментах, с помощью которых вы сможете исследовать внутреннее устройство Windows. K ним относятся, например, отладчик ядра, оснастка Performance и важнейшие утилиты с сайта wwwsysinternals.com. Кроме того, мы поясним, как пользоваться Windows Device Driver Kit (DDK) и Platform Software Development Kit (SDK) в качестве источника дополнительной информации о внутреннем устройстве Windows.

Вы должны хорошо понимать все, что написано в этой главе, - в остальной части книги мы предполагаем именно так.

Версии операционных систем Windows

Эта книга охватывает три последние версии операционной системы Microsoft Windows, основанные на кодовой базе Windows NT: Windows 2000, Windows XP (32- и 64-разрядные версии) и Windows Server 2003 (32- и 64-разрядные версии). Текст относится ко всем трем версиям, если не оговорено иное. B таблице 1-1 перечислены выпуски кодовой базы Windows NT, номера версий и названия продуктов.

Windows NT и Windows 95

При первом выпуске Windows NT компания Microsoft дала ясно понять, что это долгосрочная замена Windows 95 (и ее последующих выпусков - Windows 98 и Windows Millennium Edition). Вот список некоторых архитектурных различий и преимуществ Windows NT (и ее последующих выпусков) над Windows 95 (и ее последующими выпусками).

(o) Windows NT поддерживает многопроцессорные системы, a Windows 95 - нет.

(o) Файловая система Windows NT поддерживает средства защиты, например управление избирательным доступом (discretionary access control). B файловой системе Windows 95 этого нет.

(o) Windows NT - полностью 32-разрядная (а теперь и 64-разрядная) операционная система, в ней нет 16-разрядного кода, кроме того, который предназначен для выполнения 16-разрядных Windows-приложений. Windows 95 содержит большой объем старого 16-разрядного кода из предшествующих операционных систем - Windows 3.1 nMS-DOS.

(o) Windows NT полностью реентерабельна, а многие части Windows 95 нереентерабельны (в основном это касается 16-разрядного кода, взятого из Windows 3.1). Большинство функций, связанных с графикой и управлением окнами (GDI и USER), включают именно нереентерабельный код. Когда 32-разрядное приложение в Windows 95 пытается вызвать системный сервис, реализованный как нереентерабельный 16-разрядный код, оно должно сначала получить общесистемную блокировку (или мьютекс), чтобы предотвратить вход других потоков в нереентерабельную кодовую базу. Еще хуже, что 16-разрядное приложение удерживает такую блокировку в течение всего времени своего выполнения. B итоге, хотя ядро Windows 95 содержит 32-разрядный планировщик с поддержкой мно-гопоточности и вытесняющей многозадачности, приложения часто работают как однопоточные из-за того, что большая часть системы реализована как нереентерабельный код.

(o) Windows NT позволяет выполнять 16-разрядные Windows-приложения в выделенном адресном пространстве, a Windows 95 всегда выполняет такие приложения в общем адресном пространстве, в котором они могут навредить друг другу и привести к зависанию системы.

(o) Разделяемая (общая) память процесса в Windows NT видна только тем процессам, которые имеют проекцию на один и тот же блок разделяемой памяти. B Windows 95 вся общая память видна и доступна для записи всем процессам. Таким образом, любой процесс может что-то записать и повредить какие-то данные в общей памяти, используемые другими процессами.

(o) Некоторые критически важные страницы памяти, занимаемые операционной системой Windows 95, доступны для записи из пользовательского режима, а значит, обычное приложение может повредить содержимое этих страниц и привести к краху системы. Единственное, что умеет Windows 95 и чего никогда не смогут делать операционные системы на основе Windows NT, - выполнять все старые программы для MS-DOS и Windows 3.1 (а именно программы, требующие прямого доступа к оборудованию), а также 16-разрядные драйверы устройств MS-DOS. Если одной из основных целей разработки Windows 95 была 100%-я совместимость с MS-DOS и Windows 3.1, то исходной целью разработки Windows NT - возможность выполнения большинства существующих 16-разрядных приложений при условии сохранения целостности и надежности системы.

Базовые концепции и термины

B книге будут часто встречаться ссылки на концепции и структуры, с которыми некоторые читатели, возможно, не знакомы. Здесь мы определимся с используемой в дальнейшем терминологией.

Windows API

Это системный интерфейс программирования в семействе операционных систем Microsoft Windows, включая Windows 2000, Windows XP, Windows Server 2003, Windows 95, Windows 98, Windows Millennium Edition (Me) и Windows CE. Каждая операционная система реализует разное подмножество Windows API. Windows 95, Windows 98, Windows Me и Windows CE в этой книге не рассматриваются.

ПРИМЕЧАНИЕ Windows API описывается в документации Platform Software Development Kit (SDK). (См. раздел «Platform Software Development Kit (SDK)» далее в этой главе.) Этудокументацию можно бесплатно просмотреть на сайте msdn.microsoft.com. Она также поставляется с Microsoft Developer Network (MSDN) всех уровней подписки. (MSDN - это программа Microsoft для поддержки разработчиков. Подробности см. на сайте msdn.microsqft.com.) Отличное описание того, как программировать с использованием базового Windows API, см. в четвертом издании книги Джеффри Рихтера Jeffrey Richter) «Microsoft Windows для профессионалов» (Русская Редакция, 2000).

До появления 64-разрядных версий Windows XP и Windows Server 2003 интерфейс программирования 32-разрядных версий операционных систем Windows назывался Win32 API, чтобы отличать его от исходного 16-разрядного Windows API. B этой книге термин «Windows API» относится к 32-разряд-

ному интерфейсу программирования Windows 2000, а также к 32- и 64-разрядным интерфейсам программирования Windows XP и Windows Server 2003.

Windows API включает тысячи вызываемых функций, которые сгруппированы в следующие основные категории:

(o) базовые сервисы (Base Services);

(o) сервисы компонентов (Component Services);

(o) сервисы пользовательского интерфейса (User Interface Services);

(o) сервисы графики и мультимедиа (Graphics and Multimedia Services);

(o) коммуникационное взаимодействие и совместная работа (Messaging and Collaboration);

(o) сети (Networking);

(o) Web-сервисы (Web Services).

Основное внимание в нашей книге уделяется внутреннему устройству ключевых базовых сервисов, в частности поддержки процессов и потоков (threads), управления памятью, ввода-вывода и защиты.

Как насчет .NET и WinFX?

.NET Framework состоит из библиотеки классов, называемой Framework Class Library (FCL), и общеязыковой исполняющей среды (Common Language Runtime, CLR), которая предоставляет среду для выполнения управляемого кода с такими возможностями, как компиляция по требованию (just-in-time compilation, JIT compilation), верификация типов, сбор мусора и защита по правам доступа кода (code access security). Благодаря этому CLR создает среду разработки, которая повышает продуктивность труда программистов и уменьшает вероятность появления распространенных ошибок программирования. Отличное описание .NET Framework и ее базовой архитектуры см. в книге Джеффри Рихтера «Программирование на платформе Microsoft .NET Frame-work» (Русская Редакция, 2003).

CLR реализована как классический СОМ-сервер, код которой хранится в стандартной Windows DLL пользовательского режима. Фактически все компоненты .NET Framework реализованы как стандартные Windows DLL пользовательского режима, занимающие уровень поверх неуправляемых функций Windows APL (Никакие компоненты .NET Framework не работают в режиме ядра.) Ha рис. 1 -1 показаны взаимосвязи этих компонентов.

WinFX - «новый Windows API». Это результат эволюционного развития .NET Framework, которая будет поставляться с версией Windows под кодовым названием «Longhorn», следующим выпуском Windows. WinFX также можно установить в Windows XP и Windows Server 2003. WinFX образует фундамент для приложений следующего поколения, создаваемых для операционной системы Windows.

История создания Win32 API

Интересно, что поначалу Win32 не рассматривался как интерфейс программирования для Microsoft Windows NT. Поскольку проект Windows NT начинался как замена OS/2 версии 2, основным интерфейсом программирования был 32-разрядный OS/2 Presentation ManagerAPI. Однако год спустя на рынке появилась Microsoft Windows 3.0, быстро ставшая очень популярной. B результате Microsoft сменила курс и перенацелила проект Windows NT на будущую замену семейства продуктов Windows, а не OS/2. Вот на этом-то перепутье и встал вопрос о создании Windows API - до этого Windows API существовал только как 16-разрядный интерфейс.

Хотя в Windows API должно было появиться много новых функций, отсутствующих в Windows 3.1, Microsoft решила сделать новый API по возможности совместимым с именами функций, семантикой и типами данных в 16-разрядном Windows API, чтобы максимально облегчить бремя переноса существующих 16-разрядных Windows-приложений в Windows NT Поэтому тот, кто, впервые глядя на Windows API, удивляется, почему многие имена и интерфейсы функций кажутся противоречивыми, должен учитывать, что одной из причин такой противоречивости было стремление сделать Windows API совместимым со старым 16-разрядным Windows API.

Сервисы, функции и процедуры

Несколько терминов в документации Windows для пользователей и программистов имеет разный смысл в разных контекстах. Например, понятие «сервис» (service) может относиться к вызываемой функции операционной системы, драйверу устройства или серверному процессу (в последнем случае сервис часто называют службой). Ниже показано, что означают подобные термины в этой книге.

(o) Функции Windows API Документированные, вызываемые подпрограммы в Windows API, например CreateProcess, CreateFile и GetMessage.

(o) Неуправляемые («родные») системные сервисы (или исполняемые системные сервисы) Недокументированные низкоуровневые сервисы операционной системы, которые можно вызывать в пользовательском режиме. Так, NtCreateProcess - это внутрисистемный сервис, вызываемый Windows-функцией CreateProcess при создании нового процесса. (Определение неуправляемых функций см. в разделе «Диспетчеризация системных сервисов» главы 3.)

(o) Функции (или процедуры) ядра Подпрограммы внутри операционной системы Windows, которые можно вызывать только в режиме ядра (определение мы дадим чуть позже). Например, ExAllocatePool - процедура, вызываемая драйверами устройств для выделения памяти из системных куч (динамически распределяемых областей памяти) Windows.

(o) Windows-сервисы Процессы, запускаемые диспетчером управления сервисами в Windows. (Хотя в документации на реестр драйверы устройств Windows определяются как сервисы, мы не пользуемся таким термином в этой книге.) Например, сервис Task Scheduler выполняется в процессе пользовательского режима, который поддерживает команду at (аналогичную UNIX-команде at или cron).

(o) DLL (динамически подключаемая библиотека) Набор вызываемых подпрограмм, включенных в один двоичный файл, который приложения, использующие эти подпрограммы, могут динамически загружать во время своего выполнения. B качестве примера можно привести модули Msvcrt.dll (библиотека исполняющей подсистемы C) и Kernel32.dll (одна из библиотек подсистемы Windows API). DLL активно используются компонентами и приложениями Windows пользовательского режима. Преимущество DLL над статическими библиотеками в том, что приложения могут разделять DLL-модули, a Windows гарантирует, что в памяти будет находиться лишь по одному экземпляру используемых DLL.

Процессы, потоки и задания

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

программы. Ha самом высоком уровне абстракции процесс в Windows включает следующее:

(o) закрытое виртуальное адресное пространство - диапазон адресов виртуальной памяти, которым может пользоваться процесс;

(o) исполняемую программу - начальный код и данные, проецируемые на виртуальное адресное пространство процесса;

(o) список открытых описателей (handles) различных системных ресурсов - семафоров, коммуникационных портов, файлов и других объектов, доступных всем потокам в данном процессе;

(o) контекст защиты (security context), называемый маркером доступа (access token) и идентифицирующий пользователя, группы безопасности и привилегии, сопоставленные с процессом;

(o) уникальный идентификатор процесса (во внутрисистемной терминологии называемый идентификатором клиента);

(o) минимум один поток.

Каждый процесс также указывает на свой родительский процесс (процесс-создатель). Однако, если родитель существует, эта информация не обновляется. Поэтому есть вероятность, что некий процесс указывает на уже несуществующего родителя. Это не создает никакой проблемы, поскольку никто не полагается на наличие такой информации. Следующий эксперимент иллюстрирует данный случай.

ЭКСПЕРИМЕНТ: просмотр дерева процессов

Большинство утилит не отображает такой уникальный атрибут, как идентификатор родительского процесса. Значение этого атрибута можно получить программно или с помощью оснастки Performance, запросив значение счетчика Creating Process ID [Код (ID) создавшего процесса]. Дерево процессов показывается утилитой Tlist.exe (из Windows Debugging Tools), если вы указываете ключ /t. Вот образец вывода этой команды:

Взаимоотношения процессов (дочерний-родительский) Tlist показывает отступами. Имена процессов, родительские процессы которых на данный момент завершились, выравниваются по левому краю, потому что установить их родственные связи невозможно - даже если процессы-прапредки еще существуют. Windows сохраняет идентификатор только родительского процесса, так что проследить его создателя нельзя. Чтобы убедиться в этом, выполните следующие операции.

1. Откройте окно командной строки.

2. Наберите start cmd для запуска второго окна командной строки.

3. Откройте диспетчер задач.

4. Переключитесь на второе окно командной строки.

5. Введите mspaint для запуска Microsoft Paint.

6. Щелкните второе окно командной строки.

7. Введите exit. (Заметьте, что окно Paint остается.)

8. Переключитесь в диспетчер задач.

9. Откройте его вкладку Applications (Приложения). Ю.Щелкните правой кнопкой мыши задачу Command Prompt (Командная строка) и выберите Go To Process (Перейти к процессам).

11.Щелкните процесс Cmd.exe, выделенный серым цветом.

12.Щелкнув правой кнопкой мыши, выберите команду End Process Tree

(Завершить дерево процессов).

13.B окне Task Manager Warning (Предупреждение диспетчера задач) щелкните Yes (Да).

Первое окно командной строки исчезнет, но вы по-прежнему сможете наблюдать окно Paint, так как оно является внуком первого из завершенных процессов Command Prompt. A поскольку второй (родительский процесс Paint) тоже завершен, связь между родителем и внуком потеряна.

Для просмотра (и модификации) процессов и информации, связанной с ними, существует целый набор утилит. Следующие эксперименты демонстрируют, как получить ту или иную информацию о процессе с помощью некоторых из этих утилит. Они включаются непосредственно в саму Windows, а также в Windows Support Tools, Windows Debugging Tools, ресурсы Windows и Platform SDK. Многие из этих утилит выводят перекрывающиеся подмножества информации о базовых процессах и потоках, иногда идентифицируемые по разным именам.

Вероятно, наиболее широко применяемая утилита для анализа активности процессов - Task Manager (Диспетчер задач). (Любопытно, что в ядре Windows нет такого понятия, как задача, так что Task Manager на самом деле является инструментом для управления процессами.) Следующий эксперимент показывает разницу между тем, что Task Manager перечисляет как приложения и процессы.

ЭКСПЕРИМЕНТ: просмотр информации о процессах через диспетчер задач

Диспетчер задач Windows отображает список выполняемых в системе процессов. Его можно запустить тремя способами: 1) нажав клавиши Ctrl+Shift+Esc; 2) щелкнув панель задач правой кнопкой мыши и выбрав команду Task Manager (Диспетчер задач); 3) нажав клавиши Ctrl+Alt+Del. После запуска диспетчера задач откройте вкладку Processes (Процессы). Заметьте, что процессы идентифицируются по имени образа, экземплярами которого они являются. B отличие от некоторых объектов в Windows процессам нельзя присваивать глобальные имена. Для просмотра более подробных сведений выберите из меню View (Вид) команду Select Columns (Выбрать столбцы) и укажите, какая дополнительная информация вас интересует.

Если вкладка Processes окна диспетчера задач со всей очевидностью показывает список процессов, то содержимое вкладки Applications (Приложения) нуждается в пояснениях. Ha ней отображается список видимых окон верхнего уровня всех объектов «рабочий стол» интерактивного объекта WindowStation. (По умолчанию существуют два объекта «рабочий стол», но вы можете создать дополнительные рабочие столы через Windows-функцию CreateDesktop.) Колонка Status (Состояние) дает представление о том, находится ли поток - владелец окна в состоянии ожидания Windows-сообщения. «Running» («Выполняется») означает, что поток ожидает ввода в окно, a «Not Responding» («He

отвечает») - что не ожидает (т. е. занят либо ждет завершения операции ввода-вывода или освобождения какого-либо синхронизирующего объекта).

Вкладка Applications позволяет идентифицировать процесс, которому принадлежит поток, владеющий каким-либо окном задачи. Для этого щелкните правой кнопкой мыши имя задачи и выберите команду Go To Process (Перейти к процессам).

Утилита Process Explorer показывает больше информации о процессах и потоках, чем любой другой доступный инструмент; вот почему она используется нами во многих экспериментах, которые вы увидите в этой книге. Ниже перечислены некоторые уникальные сведения, выводимые утилитой Process Explorer, и ее возможности:

(o) полное имя (вместе с путем) выполняемого образа;

(o) маркер защиты процесса (список групп и привилегий);

(o) выделение изменений в списке процессов и потоков;

(o) список сервисов внутри процессов - хостов сервисов с выводом отображаемого имени (display name) и описания;

(o) процессы, которые являются частью задания, и детальные сведения о заданиях;

(o) процессы, выполняющие .NET/WinFX-приложения, и сведения, специфичные для .NET (например, список доменов приложений и счетчики производительности, относящиеся к CLR);

(o) время запуска процессов и потоков;

(o) полный список файлов, проецируемых в память (не только DLL-модулей);

(o) возможность приостановки процесса;

(o) возможность принудительного завершения индивидуальных потоков;

(o) простота выявления процессов, использующих наибольшую долю процессорного времени за определенный период. (Оснастка Performance позволяет просматривать процент использования процессора для заданного набора процессов, но не показывает автоматически процессы, созданные после начала сеанса мониторинга.)

Process Explorer также упрощает доступ к информации, предоставляемой другими утилитами, создавая единую точку ее просмотра:

(o) дерево процессов с возможностью свертывания отдельных частей этого дерева;

(o) открытые описатели в процессе без предварительной настройки (утилиты Microsoft для вывода открытых описателей требуют предварительной установки общесистемного флага и перезагрузки);

(o) список DLL (и файлов, проецируемых в память) в каком-либо процессе;

(o) активность потоков в каком-либо процессе;

(o) стеки потоков пользовательского режима (с сопоставлением адресов именам, используя механизм поддержки символов для инструментов отладки);

(o) стеки системных потоков режима ядра (с сопоставлением адресов именам, используя механизм поддержки символов для инструментов отладки);

(o) разница в переключении контекстов (context switch delta) (более наглядное представление активности процессора, как поясняется в главе 6);

(o) лимиты памяти режима ядра (пулов подкачиваемой и неподкачиваемой памяти) (остальные утилиты показывают только текущие размеры). Попробуем провести первый эксперимент с помощью Process Explorer.

ЭКСПЕРИМЕНТ: просмотр детальных сведений о процессах с помощью Process Explorer

Скачайте последнюю версию Process Explorer и запустите ее. При первом запуске вы увидите сообщение о том, что на данный момент символы не сконфигурированы. Когда они корректно сконфигурированы, Process Explorer может обращаться к символьной информации для отображения символьного имени стартовой функции потока и функций в его стеке вызовов (для этого нужно дважды щелкнуть процесс и выбрать вкладку Threads). Эта информация полезна для идентификации того, что именно делают потоки внутри процесса. Для доступа к символам вы должны установить Debugging Tools (об этом мы еще поговорим в данной главе). Потом щелкнуть Options, выбрать Configure Symbols и набрать подходящий путь Symbols. Например:

B предыдущем примере для доступа к символам использовался сервер символов по требованию (on-demand symbol server), а копии файлов символов хранились на локальном компьютере в папке c:\symbols. Подробнее о конфигурировании сервера символов см. по ссылке http:/ /www.microsoft.com/whdc/ddk/debugging/symbols.mspx.

При запуске Process Explorer по умолчанию выводит список процессов в верхней половине окна, а список открытых описателей для выбранного на данный момент процесса - в нижней половине. Если вы задержите курсор мыши над именем процесса, Process Explorer также показывает описание образа, название компании и полный путь.

Вот как использовать некоторые базовые возможности Process Explorer:

1. Отключите нижнюю секцию, сбросив View, Show Lower Pane. (Нижняя секция может отображать открытые описатели или проецируемые DLL и файлы - об этом речь пойдет в главах 3 и 7.)

2. Обратите внимание на то, что процессы, являющиеся хостами сервисов, по умолчанию выделяются розовым цветом. Ваши собственные процессы выделяются синим. (Эти цвета можно настроить.)

3. Задержите курсор мыши над именем образа и обратите внимание на то, что в подсказке отображается полный путь.

4. Щелкните View, Select Columns и добавьте путь образа.

5. Отсортируйте по колонке процессов и вы увидите, что представление в виде дерева исчезло. (Вы можете либо вывести представление в виде дерева, либо сортировать по любой из отображаемых колонок.) Снова щелкните для сортировки по алфавиту в обратном порядке (от Z к А). После этого очередной щелчок вернет представление в виде дерева.

6. Сбросьте View, Show Processes From All Users для отображения только ваших процессов.

7. Перейдите в Options, Difference Highlight Duration и смените значение на 5 секунд. Потом запустите новый процесс (какой угодно) и обратите внимание на то, что этот процесс выделяется зеленым в течение 5 секунд. Закройте новый процесс и заметьте, что этот процесс выделяется красным в течение 5 секунд, прежде чем исчезнуть из древовидного списка. Эта функция может пригодиться для обнаружения создаваемых и завершаемых процессов в системе.

8. Наконец, дважды щелкните какой-нибудь процесс и изучите вкладки, доступные в окне свойств процесса. (Эти вкладки понадобятся нам в дальнейших экспериментах; там же мы поясним, какую информацию они сообщают.)

Поток (thread) - некая сущность внутри процесса, получающая процессорное время для выполнения. Без потока программа процесса не может выполняться. Поток включает следующие наиболее важные элементы:

(o) содержимое набора регистров процессора, отражающих состояние процессора;

(o) два стека, один из которых используется потоком при выполнении в режиме ядра, а другой - в пользовательском режиме;

(o) закрытую область памяти, называемую локальной памятью потока (thread-local storage, TLS) и используемую подсистемами, библиотеками исполняющих систем (run-time libraries) и DLL;

(o) уникальный идентификатор потока (во внутрисистемной терминологии также называемый идентификатором клиента: идентификаторы процессов и потоков генерируются из одного пространства имен и никогда не перекрываются);

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

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

структура данных специфична для конкретной платформы. Windows-функция GetThreadContext предоставляет доступ к этой аппаратно-зависимой информации (называемой блоком CONTEXT).

Волокна и потоки

Волокна (fibers) позволяют приложениям планировать собственные «потоки» выполнения, не используя встроенный механизм планирования потоков на основе приоритетов. Волокна часто называют «облегченными» потоками. Они невидимы ядру, так как Kernel32.dll реализует их в пользовательском режиме. Для использования волокна нужно вызвать Windows-функцию ConvertTbreadToFiber, которая преобразует поток в волокно. Полученное волокно может создавать дополнительные волокна через функцию CreateFiber (у каждого волокна может быть свой набор волокон). Выполнение волокна (в отличие от потока) не начинается до тех пор, пока оно не будет вручную выбрано вызовом SwitchToFiber. Волокно работает до завершения или до переключения процессора на другое волокно вызовом все той же SwitcbToFiber. Подробнее о функциях, связанных с волокнами, см. документацию Platform SDK.

Хотя у потоков свой контекст выполнения, каждый поток внутри одного процесса делит его виртуальное адресное пространство (а также остальные ресурсы, принадлежащие процессу). Это означает, что все потоки в процессе могут записывать и считывать содержимое памяти любого из потоков данного процесса. Однако потоки не могут случайно сослаться на адресное пространство другого процесса. Исключение возможно в ситуации, когда тот предоставляет часть своего адресного пространства как раздел общей памяти (shared memory section), в Windows API называемый объектом «проекция файла» (file mapping object), или когда один из процессов имеет право на открытие другого процесса и использует функции доступа к памяти между процессами, например ReadProcessMemory и WriteProcessMemory.

Кроме закрытого адресного пространства и одного или нескольких потоков у каждого процесса имеются идентификация защиты и список открытых описателей таких объектов, как файлы и разделы общей памяти, или синхронизирующих объектов вроде мьютексов, событий и семафоров (рис. 1-2).

Каждый процесс обладает контекстом защиты, который хранится в объекте - маркере доступа. Маркер доступа содержит идентификацию защиты и определяет полномочия данного процесса. По умолчанию у потока нет собственного маркера доступа, но он может получить его, и это позволит ему подменять контекст защиты другого процесса (в том числе выполняемого на удаленной системе Windows). Подробнее на эту тему см. главу 8.

Дескрипторы виртуальных адресов (virtual address descriptors, VAD) - это структуры данных, используемые диспетчером памяти для учета виртуальных адресов, задействованных процессом (см. главу 7).

Windows предоставляет расширение для модели процессов - задания (jobs). Они предназначены в основном для того, чтобы группами процессов можно было оперировать и управлять как единым целым. Объект-задание позволяет устанавливать определенные атрибуты и накладывать ограничения на процесс или процессы, сопоставленные с заданием. B этом объекте также хранится информация обо всех процессах, которые были сопоставлены с заданием, но к настоящему времени уже завершены. B каких-то отношениях объект-задание компенсирует отсутствие иерархического дерева процессов в Windows, а в каких-то - даже превосходит по своим возможностям дерево процессов UNIX.

Более детальное описание внутренней структуры заданий, процессов и потоков, механизмов создания потоков и процессов, а также алгоритмов планирования потоков вы найдете в главе 6.

Виртуальная память

B Windows реализована система виртуальной памяти, основанная на плоском (линейном) адресном пространстве. Она создает каждому процессу иллюзию того, что у него есть собственное большое и закрытое адресное пространство. Виртуальная память дает логическое представление, не обязательно соответствующее структуре физической памяти. B период выполнения диспетчер памяти, используя аппаратную поддержку, транслирует, или проецирует (maps), виртуальные адреса на физические, по которым реально хранятся данные. Управляя проецированием и защитой страниц памяти, операционная система гарантирует, что ни один процесс не помешает другому и не сможет повредить данные самой операционной системы. Ha рис. 1-3 показано, как три смежные страницы виртуальной памяти проецируются на три разрозненные страницы физической памяти.

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

Размер виртуального адресного пространства зависит от конкретной аппаратной платформы. Ha 32-разрядных х86-системах теоретический максимум для общего виртуального адресного пространства составляет 4 Гб. По умолчанию Windows выделяет нижнюю половину этого пространства (в диапазоне адресов от x00000000 до x7FFFFFFF) процессам, а вторую половину (в диапазоне адресов от x80000000 до xFFFFFFFF) использует в собственных целях. Windows 2000 Advanced Server, Windows 2000 Datacenter Server, Windows XP (SP2 и выше) и Windows Server 2003 поддерживают загрузочные параметры /3GB и /USERVA, которые указываются в файле Boot.ini (см. главу 5), что позволяет процессам, выполняющим программы со специальным флагом в заголовке исполняемого образа, использовать до 3 Гб закрытого адресного пространства и оставляет операционной системе только 1 Гб. Этот вариант дает возможность приложению вроде сервера базы данных хранить в адресном пространстве своего процесса большие порции базы данных и тем самым уменьшить частоту проецирования отдельных представлений этой базы. Две структуры виртуальных адресных пространств, поддерживаемые 32-разрядной Windows, показаны на рис. 1-4.

Хотя три гигабайта лучше двух, этого все равно недостаточно для проецирования очень больших баз данных. B связи с этим в 32-разрядных Windows появился механизм Address Windowing Extension (AWE), который позволяет 32-разрядному приложению выделять до 64 Гб физической памяти, а затем проецировать представления (views), или окна (windows), на свое 2-гигабайтное виртуальное адресное пространство. Применение AWE усложняет управление проекциями виртуальной памяти на физическую, но снимает проблему прямого доступа к объему физической памяти, превышающему лимиты 32-разрядного адресного пространства процесса.

64-разрядная Windows предоставляет процессам гораздо большее адресное пространство: 7152 Гб на Itanium-системах и 8192 Гб на х64-системах. Ha рис. 1-5 показана упрощенная схема структур 64-разрядных адресных пространств (детали см. в главе 7). Заметьте, что эти размеры отражают не архитектурные лимиты для данных платформ, а ограничения реализации в текущих версиях 64-разрядной Windows.

Подробнее о реализации диспетчера памяти, в том числе о трансляции адресов и управлении физической памятью в Windows, см. главу 7.

Режим ядра и пользовательский режим

Для предотвращения доступа приложений к критически важным данным операционной системы и устранения риска их модификации Windows использует два режима доступа к процессору (даже если он поддерживает более двух режимов): пользовательский (user mode) и ядра (kernel mode). Код

приложений работает в пользовательском режиме, тогда как код операционной системы (например, системные сервисы и драйверы устройств) - в режиме ядра. B режиме ядра предоставляется доступ ко всей системной памяти и разрешается выполнять любые машинные команды процессора. Предоставляя операционной системе более высокий уровень привилегий, чем прикладным программам, процессор позволяет разработчикам операционных систем реализовать такие архитектуры, которые не дают возможности сбойным приложениям нарушать стабильность работы всей системы.

ПРИМЕЧАНИЕ B архитектуре процессора Intel x86 определено четыре уровня привилегий, или колец (rings), предназначенных для защиты кода и данных системы от случайной или умышленной перезаписи кодом с меньшим уровнем привилегий. Windows использует уровень привилегий 0 (или кольцо 0) для режима ядра и уровень привилегий 3 (или кольцо 3) для пользовательского режима. Почему Windows использует только два уровня? Дело в том, что на некоторых из ранее поддерживавшихся аппаратных платформ (например, Compaq Alpha и Silicon Graphics MIPS) реализовано лишь два уровня привилегий.

Хотя каждый Windows-процесс имеет свою (закрытую) память, код операционной системы и драйверы устройств, работающие в режиме ядра, делят единое виртуальное адресное пространство. Каждая страница в виртуальной памяти помечается тэгом, определяющим, в каком режиме должен работать процессор для чтения и/или записи данной страницы. Страницы в системном пространстве доступны лишь в режиме ядра, а все страницы в пользовательском адресном пространстве - в пользовательском режиме. Страницы только для чтения (например, содержащие лишь исполняемый код) ни в каком режиме для записи недоступны.

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

Это также подчеркивает, насколько надо быть осторожным при загрузке драйвера устройства от стороннего поставщика: перейдя в режим ядра, он получит полный доступ ко всем данным операционной системы. Такая уязвимость стала одной из причин, по которым в Windows введен механизм проверки цифровых подписей драйверов, предупреждающий пользователя о попытке установки неавторизованного (неподписанного) драйвера (подробнее на эту тему см. главу 9). Кроме того, механизм Driver Verifier (верификатор драйверов) помогает разработчикам драйверов устройств находить в них ошибки (вызывающие, например, утечку памяти или переполнения буферов). Driver Verifier поясняется в главе 7.

Как вы увидите в главе 2, прикладные программы могут переключаться из пользовательского режима в режим ядра, обращаясь к системному сервису. Например, Windows-функции ReadFile в ходе своего выполнения приходится вызывать внутреннюю подпрограмму Windows - она-то и считывает данные из файла. Так как эта подпрограмма обращается к внутрисистемным структурам данных, она должна выполняться в режиме ядра. Переключение из пользовательского режима в режим ядра осуществляется специальной командой процессора. Операционная система перехватывает эту команду, обнаруживает запрос системного сервиса, проверяет аргументы, которые поток передал системной функции, и выполняет внутреннюю подпрограмму. Перед возвратом управления пользовательскому потоку процессор переключается обратно в пользовательский режим. Благодаря этому операционная система защищает себя и свои данные от возможной модификации пользовательскими процессами.

ПРИМЕЧАНИЕ Переключение из пользовательского режима в режим ядра (и обратно) не влияет на планирование потока, так как контекст в этом случае не переключается. O диспетчеризации системных сервисов см. главу 3.

Так что ситуация, когда пользовательский поток часть своего времени работает в пользовательском режиме, а часть - в режиме ядра, совершенно нормальна. A поскольку подсистема, отвечающая за поддержку графики и окон, функционирует в режиме ядра, то приложения, интенсивно работающие с графикой, большую часть времени действуют в режиме ядра, а не в пользовательском режиме. Самый простой способ проверить это - запустить приложение вроде Microsoft Paint или Microsoft Pinball и с помощью одного из счетчиков оснастки Performance (Производительность), перечисленных в таблице 1-2, понаблюдать за показателями времени работы в пользовательском режиме и в режиме ядра.

ЭКСПЕРИМЕНТ: наблюдение за активностью потоков с помощью QuickSlice

QuickSlice позволяет в динамике наблюдать за соотношением времени, проведенного каждым процессом в режиме ядра и в пользовательском режиме. Ha диаграмме красная часть столбца отражает количество процессорного времени в режиме ядра, а синяя - в пользовательском режиме. (Хотя в книге эти столбцы воспроизведены в черно-белом цвете, на самом деле они всегда красные и синие.) Сумма всех показателей, отображаемых столбцами в окне QuickSlice, должна соответствовать 100% процессорного времени. Для запуска QuickSlice щелкните кнопку Start (Пуск), выберите команду Run (Выполнить) и введите Qslice.exe (в переменной PATH должен быть указан путь к ресурсам Windows). Например, попробуйте запустить такое интенсивно использующее графику приложение, как Paint (Mspaint.exe). Откройте QuickSlice, расположив его окно рядом с окном Paint, и нарисуйте в Paint несколько кривых. B это время вы сможете наблюдать за выполнением Mspaint.exe в окне QuickSlice, как показано ниже.

Чтобы получить дополнительную информацию о потоках процесса, дважды щелкните имя нужного процесса или соответствующий цветной столбик на диаграмме. Вы увидите список потоков этого процесса и относительное процессорное время, используемое каждым потоком (в рамках процесса, а не всей системы).

ЭКСПЕРИМЕНТ: режим ядра и пользовательский режим

C помощью оснастки Performance вы можете выяснить, сколько времени ваша система работает в режиме ядра и в пользовательском режиме.

1. Запустите оснастку Performance (Производительность), открыв меню Start (Пуск) и последовательно выбрав команды Programs (Программы), Administrative Tools (Администрирование), Performance (Производительность).

2. Щелкните на панели инструментов кнопку Add (Добавить) (на этой кнопке изображен большой знак плюс).

3. Выберите в списке объект Processor (Процессор), щелкните счетчик % Privileged Time (% работы в привилегированном режиме) и, удерживая клавишу Ctrl в нажатом состоянии, щелкните счетчик % User Time (% работы в пользовательском режиме).

4. Щелкните кнопку Add (Добавить), а затем Close (Закрыть).

5. Быстро подвигайте мышью. При этом вы должны заметить всплеск на линии % Privileged Time (рис. 1-6), который отражает время, затраченное на обслуживание прерываний от мыши, и время, понадобившееся подсистеме поддержки окон на отрисовку графики (эта подсистема, как поясняется в главе 2, работает преимущественно как драйвер устройства в режиме ядра).

6. Закончив, щелкните на панели инструментов кнопку New Counter Set (Новый набор счетчиков) (или просто закройте оснастку).

За той же активностью можно понаблюдать через Task Manager (Диспетчер задач). Просто перейдите в нем на вкладку Performance (Быстродействие), а затем выберите из меню View (Вид) команду Show Kernel Times (Вывод времени ядра). Процент загруженности процессора отражается зеленым цветом, а процент времени работы в режиме ядра - красным.

Чтобы увидеть, как сама оснастка Performance использует время в двух режимах, запустите ее снова, но добавьте те же счетчики для объекта Process (Процесс).

1. Если вы закрыли оснастку Performance, снова запустите ее. (Если она уже работает, откройте новый экран, щелкнув на панели инструментов кнопку New Counter Set.)

2. Щелкните кнопку Add на панели инструментов.

3. Выберите в списке объект Process.

4. Выберите счетчики % Privileged Time и % User Time.

5. B списке экземпляров объекта выберите все процессы (кроме процесса _Total).

6. Щелкните кнопку Add, а затем Close.

7. Быстро подвигайте мышью.

8. Нажмите комбинацию клавиш Ctrl+H для активизации режима выделения - текущий выбранный счетчик будет выделен белым цветом в Windows 2000 и черным в Windows XP или Windows Server 2003.

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

Вы должны заметить, как значения счетчиков для процесса оснастки Performance - ищите mmc в колонке Instance (Экземпляр) - резко увеличиваются при перемещении мыши, поскольку код приложения выполняется в пользовательском режиме, а вызываемые им Windows-функции - в режиме ядра. Вы также заметите, что при перемещении мыши увеличивается активность работы в режиме ядра потока процесса csrss. Он представляет поток необработанного ввода (raw input thread) подсистемы Windows, принимающий ввод от клавиатуры и мыши и передающий его процессу, к которому он подключен. (Подробнее о системных потоках см. главу 2.) Наконец, процесс с именем Idle, потоки которого, как вы убедитесь, тратят почти 100% своего времени в режиме ядра, на самом деле не является процессом. Это лжепроцесс, используемый для учета тактов процессора в состоянии простоя. Таким образом, когда Windows нечего делать, она предается этому занятию в режиме ядра.

Terminal Services и несколько сеансов

Terminal Services (службы терминала) обеспечивают в Windows поддержку нескольких интерактивных сеансов пользователей на одной системе. C помощью Terminal Services удаленный пользователь может установить сеанс на другой машине, зарегистрироваться на ней и запускать приложения на сервере. Сервер предоставляет клиенту графический пользовательский интерфейс (GUI), а клиент возвращает серверу пользовательский ввод. (Это отличается от того, как ведет себя X Windows на UNIX-системах, где разрешается выполнять индивидуальные приложения на сервере, а клиенту предоставляется удаленный дисплей, так как удаленным является весь сеанс пользователя - не только одно приложение.)

Первый сеанс входа на физической консоли компьютера считается консольным сеансом, или нулевым сеансом (session zero). Дополнительные сеансы можно создать с помощью программы соединения с удаленным рабочим столом (Mstsc.exe), а в Windows XP - через механизм быстрого переключения пользователей (об этом позже).

Возможность создания удаленного сеанса поддерживается Windows 2000 Server, но не Windows 2000 Professional. Windows XP Professional позволяет одному удаленному пользователю подключаться к машине, однако если кто-то начинает процедуру входа в консоли, рабочая станция блокируется (т. е. систему можно использовать либо локально, либо удаленно, но не и то, и другое одновременно).

Windows 2000 Server и Windows Server 2003 поддерживают два одновременных удаленных сеанса. (Это упрощает удаленное управление, например облегчает применение инструментов, требующих от администратора входа на удаленный компьютер.) Windows 2000 Advanced Server, Datacenter Server и все издания Windows Server 2003 способны поддерживать более двух сеансов одновременно при условии правильного лицензирования и настройки системы в качестве сервера терминала.

Хотя Windows XP Home и Professional не поддерживают несколько удаленных подключений к рабочему столу, они все же поддерживают несколько сеансов, созданных локально через механизм быстрого переключения пользователей. (Этот механизм отключается в Windows XP Professional, если система присоединяется к домену.) Когда пользователь выбирает выключение своего сеанса вместо выхода [например, последовательным выбором Start (Пуск), Log Off (Выход из системы) и Switch User (Смена пользователя) или нажатием клавиши L при одновременном удерживании клавиши Windows], текущий сеанс (т. е. процессы, выполняемые в этом сеансе, и все структуры данных, глобальные для сеанса и описывающие его) остается в системе, а Windows возвращается к основному окну входа. Если в систему входит новый пользователь, создается новый сеанс.

Для приложений, которым нужно знать, выполняются ли они в сеансе сервера терминала, предназначен набор Windows API-функций, позволяющих программно распознавать такую ситуацию и контролировать различные аспекты служб терминала. (Детали см. в Platform SDK.)

B главе 2 кратко описывается, как создаются сеансы, и проводится несколько экспериментов, показывающих, как просматривать информацию о сеансе с помощью различных инструментов, включая отладчик ядра. B разделе «Диспетчер объектов» главы 3 поясняется, как создается сеансовый экземпляр системного пространства имен для объектов и как приложения могут узнавать о других своих экземплярах в той же системе. Наконец, в главе 7 рассказывается, как диспетчер памяти настраивает данные, глобальные для сеанса, и управляет ими.

Объекты и описатели

B операционной системе Windows объект - это единственный экземпляр периода выполнения (run-time instance) статически определенного типа объекта. Тип объекта состоит из общесистемного типа данных, функций, оперирующих экземплярами этого типа данных, и набора атрибутов. Если вы пишете Windows-приложения, вам наверняка знакомы такие объекты, как процесс, поток, файл и событие, - продолжать можно еще долго. Эти объекты базируются на объектах более низкого уровня, создаваемых и управляемых Windows. B Windows процесс является экземпляром объекта типа «процесс», файл - экземпляром объекта типа «файл» и т. д.

Атрибут объекта (object attribute) - это поле данных в объекте, частично определяющее состояние этого объекта. Например, объект типа «процесс», имеет атрибуты, в число которых входят идентификатор процесса, базовый приоритет и указатель на объект маркера доступа. Методы объекта (средства для манипулирования объектами) обычно считывают или изменяют какие-либо атрибуты. Так, метод open процесса мог бы принимать идентификатор процесса и возвращать указатель на этот объект.

ПРИМЕЧАНИЕ He путайте параметр ObjectAttributes, предоставляемый вызывающей программой при создании объекта через Windows API или его родные сервисы, с термином «атрибуты объекта», имеющим более общий смысл.

Самое главное различие между объектом и обычной структурой данных заключается в том, что внутренняя структура объекта скрыта. Чтобы получить данные из объекта или записать в него какую-то информацию, вы долж-

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

Объекты очень удобны для поддержки четырех важных функций операционной системы:

(o) присвоения понятных имен системным ресурсам;

(o) разделения ресурсов и данных между процессами;

(o) защиты ресурсов от несанкционированного доступа;

(o) учета ссылок (благодаря этому система узнает, когда объект больше не используется, и автоматически уничтожает его).

He все структуры данных в Windows являются объектами. B объекты помещаются лишь те данные, которые нужно разделять, защищать, именовать или делать доступными программам пользовательского режима (через системные сервисы). Структуры, используемые только одним из компонентов операционной системы для поддержки каких-то внутренних функций, к объектам не относятся. Подробнее объекты и их описатели (ссылки на экземпляр объекта) рассматриваются в главе 3.

Безопасность

Windows с самого начала разрабатывалась как защищенная система, удовлетворяющая требованиям различных правительственных и промышленных стандартов безопасности, например спецификации Common Criteria for Information Technology Security Evaluation (CCITSE). Подтверждение правительством рейтинга безопасности операционной системы позволяет ей конкурировать в сферах, требующих повышенной защиты. Разумеется, многим из этих требований должна удовлетворять любая многопользовательская система.

Базовые возможности защиты в Windows таковы: избирательная защита любых разделяемых системных объектов (файлов, каталогов, процессов, потоков и т. д.), аудит безопасности (для учета пользователей и инициируемых ими операций), аутентификация паролей при входе и предотвращение доступа одного из пользователей к неинициализированным ресурсам (например, к памяти или дисковому пространству), освобожденным другим пользователем.

Windows поддерживает два вида контроля доступа к объектам. Первый из них - управление избирательным доступом (discretionary access control) - является механизмом, который как раз и связывается большинством пользователей с защитой. Это метод, при котором владельцы объектов (например, файлов или принтеров) разрешают или запрещают доступ к ним для других пользователей. При входе пользователь получает набор удостоверений защиты (security credentials), или контекст защиты (security context). Когда он пытается обратиться к объекту, его контекст защиты сверяется со списком управления доступом (access control list, ACL) для данного объекта, чтобы определить, имеет ли он разрешение на выполнение запрошенной операции.

Второй метод -управление привилегированным доступом Q3riv1leged access control) - необходим в тех случаях, когда управления избирательным доступом недостаточно. Данный метод гарантирует, что пользователь сможет обратиться к защищенным объектам, даже если их владелец недоступен. Например, если какой-то сотрудник увольняется из компании, администратору нужно получить доступ к файлам, которые могли быть доступны только бывшему сотруднику. B таких случаях Windows позволяет администратору стать владельцем этих файлов и при необходимости управлять правами доступа к ним.

Защита пронизывает весь интерфейс Windows APL Подсистема Windows реализует защиту на основе объектов точно так же, как и сама операционная система. При первой попытке доступа приложения к общему (разделяемому) объекту подсистема Windows проверяет, имеет ли это приложение соответствующие права. Если проверка завершается успешно, подсистема Windows разрешает приложению доступ.

Подсистема Windows реализует защиту для общих объектов, часть из которых построена на основе родных объектов Windows. K Windows-объектам относятся объекты рабочего стола, меню, окна, файлы, процессы, потоки и ряд синхронизирующих объектов.

Детальное описание защиты в Windows см. в главе 8.

Реестр

Если вы работали хоть с какой-нибудь операционной системой Windows, то, вероятно, слышали о реестре или даже просматривали его. Рассказать о внутреннем устройстве Windows, не упоминая реестр, вряд ли возможно, так как это системная база данных с информацией, необходимой для загрузки и конфигурирования системы; в ней содержатся общесистемные параметры, контролирующие работу Windows, база данных защиты и конфигурационные настройки, индивидуальные для каждого пользователя.

Кроме того, реестр - это окно, через которое можно заглянуть в переменные системные данные, чтобы, например, выяснить текущее состояние аппаратной части системы (какие драйверы устройств загружены, какие ресурсы они используют и т. д.) или значения счетчиков производительности Windows. Счетчики производительности, которые на самом деле в реестре не хранятся, доступны через функции реестра (см. главу 4).

Хотя у многих пользователей и администраторов Windows никогда не возникает необходимости работать непосредственно с реестром (большую часть параметров можно просматривать или модифицировать с помощью стандартных административных утилит), он все же является источником полезной информации о внутренних структурах данных Windows, так как содержит множество параметров, влияющих на быстродействие и поведение системы. (Будьте крайне осторожны, напрямую изменяя параметры реестра: любые изменения могут отрицательно сказаться на быстродействии или, что гораздо хуже, привести к краху системы.)

Ссылки на различные разделы реестра, относящиеся к описываемым компонентам, будут встречаться на протяжении всей книги. Большинство таких разделов находится в ветви HKEY_LOCAL_MACHINE, которую мы сокращенно называем HKLM. Подробнее о реестре и его внутренней структуре см. главу 4.

Unicode

Windows отличается от большинства других операционных систем тем, что в качестве внутреннего формата для хранения и обработки текстовых строк использует Unicode. Unicode - это стандартная кодировка, которая поддерживает многие известные в мире наборы символов и в которой каждый символ представляется 16-битным (двухбайтовым) кодом. (Подробнее о Unicode см. www.unicode.org и документацию на компакт-дисках MSDN Library.)

Поскольку многие приложения имеют дело с 8-битными (однобайтовыми) ANSI-символами, Windows-функции, принимающие строковые параметры, существуют в двух версиях: для Unicode и для ANSI. B Windows 95, Windows 98 и Windows ME реализована лишь часть Unicode-версий Windows-функций, поэтому приложения, рассчитанные на выполнение как в одной из этих операционных систем, так и в NT-подобных Windows, обычно используют ANSI-версии функций. Если вы вызываете ANSI-версию Windows-функции, входные строковые параметры перед обработкой системой преобразуются в Unicode, а выходные - из Unicode в ANSI (перед возвратом приложению). Таким образом при использовании в Windows устаревшего сервиса или фрагмента кода, написанного в расчете на ANSI-строки, эта операционная система будет вынуждена преобразовывать ANSI-символы в Unicode. Однако Windows никогда не преобразует данные внутри файлов - решения о том, в какой кодировке хранить текстовую информацию в файлах, принимают лишь сами приложения.

B предыдущих версиях Windows ее азиатский и ближневосточный выпуски представляли собой надмножество базовых американского и европейского выпусков, в которые включались дополнительные Windows-функции для обработки более сложных раскладок клавиатур и принципов ввода текста (например, набора текста справа налево). Начиная с Windows 2000, все языковые выпуски содержат одинаковые Windows-функции. Единая для всех стран двоичная кодовая база Windows способна поддерживать множество языков за счет простого добавления нужных компонентов языковой поддержки. Используя эти Windows-функции, разработчики могут создавать универсальные приложения, способные работать со множеством языков.

Изучение внутреннего устройства Windows

Хотя большая часть информации, представленная в этой книге, получена при чтении исходного кода Windows и общении с разработчиками, вы не обязаны принимать все на веру. Многие детали внутреннего устройства Windows можно вытащить на свет с помощью самых разнообразных средств, в том числе поставляемых с Windows, входящих в Windows Support Tools и ресурсы Windows, а также с использованием отладочных средств самой Windows. Чуть позже мы вкратце рассмотрим эти пакеты инструментальных средств.

Чтобы упростить вам исследование внутреннего устройства Windows, мы часто даем в книге врезки «Эксперимент» с пошаговыми инструкциями для изучения какого-либо аспекта поведения Windows. (Вы уже видели такие врезки в этой главе.) Советуем проводить эти эксперименты - это позволит увидеть в действии многие вещи, о которых рассказывается в книге.

B таблице 1-3 перечислены все используемые нами инструменты и утилиты.

Таблица 1-3. Средства просмотра внутренней информации Windows

Оснастка Performance

Мы часто ссылаемся на этот инструмент, доступный через папку Administrative Tools (Администрирование) в меню Start (Пуск) или через Control Panel (Панель управления). Оснастка Performance (Производительность) предназначена для мониторинга системы, просмотра журналов, в которых регистрируются значения счетчиков производительности, и оповещения при достижении заданных пороговых значений тех или иных счетчиков. Говоря об оснастке Performance, мы подразумеваем лишь ее функцию системного мониторинга.

Оснастка Performance способна сообщить о том, как работает система, гораздо больше, чем любая другая, отдельно взятая утилита. Она предусматривает сотни счетчиков для различных объектов. По каждому счетчику можно получить краткое описание. Чтобы увидеть описание, выберите счетчик в окне Add Counters (Добавить счетчики) и щелкните кнопку Explain (Объяснение). Или откройте справочный файл Performance Counter Reference с компакт-диска «Ресурсы Windows». Информацию о том, как интерпретировать показания счетчиков для устранения «узких мест» в системе или для планирования пропускной способности сервера, см. раздел «Performance Monitoring» в книге «Windows 2000 Server Operations Guide» из набора Windows 2000 Server Resource Kit. Для Windows XP и Windows Server 2003 см. документацию Performance Counters Reference в Windows Server 2003 Resource Kit.

Заметьте, что все счетчики производительности Windows доступны программным путем. Краткое описание соответствующих компонентов см. в разделе «HKEY_PERFORMANCE_DATA» главы 4.

Windows Support Tools

Windows Support Tools включают около 40 утилит, полезных в администрировании систем на базе Windows и устранении неполадок в них. Многие из этих утилит раньше были частью ресурсов Windows NT 4.

Вы можете установить Support Tools, запустив Setup.exe из папки \Support\ Tools в дистрибутиве любого издания Windows. Support Tools одинаковы в Windows 2000 Professional, Server, Advanced Server и Datacenter Server, a для Windows XP, равно как и для Windows Server 2003, существует своя версия Support Tools.

Ресурсы Windows

Ресурсы Windows (Windows Resource Kits) расширяют Support Tools, предлагая дополнительные утилиты для администрирования и поддержки систем. Утилиты Windows Server 2003 Resource Kit можно бесплатно скачать с microsoft.com (выполните поиск по ключевым словам «resource kit tools»). Их можно установить в Windows XP или Windows Server 2003.

Ресурсы Windows 2000 существуют в двух изданиях: Windows 2000 Professional Resource Kit и Windows 2000 Server Resource Kit* (самая последняя

Последнее издание переведено на русский язык издательством «Русская Редакция» и выпущено в 2001 г. в виде серии «Ресурсы Microsoft Windows 2000 Server» которая включает 4 книги: «Сети TCP/IP», «Сопровождение сервера», «Распределенные системы» и «Межсетевое взаимодействие». - Прим. перев.

его версия - Supplement 1). Хотя последний набор представляет собой надмножество первого и может быть установлен на системах с Windows 2000 Professional, утилиты, входящие только в Windows 2000 Server Resource Kit, ни в одном из наших экспериментов не используются. B отличие от утилит Windows Server 2003 Resource Kit эти утилиты нельзя скачать бесплатно. Однако Windows 2000 Server Resource Kit поставляется с подписками на MSDN и TechNet.

Отладка ядра

Отладка ядра подразумевает изучение внутренних структур данных ядра и/ или пошаговый проход по функциям в ядре. Это полезный способ исследования внутреннего устройства Windows, потому что он позволяет увидеть внутрисистемную информацию, недоступную при использовании каких-либо других способов, и получить более ясное представление о схеме выполнения кода внутри ядра.

Отладку ядра можно проводить с помощью разнообразных утилит: Windows Debugging Tools от Microsoft, LiveKD от wwwsysinternals.com или SoftIce от Compuware NuMega. Прежде чем описывать эти средства, давайте рассмотрим файл, который понадобится при любом виде отладки ядра.

Символы для отладки ядра

Файлы символов (symbol files) содержат имена функций и переменных. Они генерируются компоновщиком (linker) и используются отладчиками для ссылки и отображения этих имен в сеансе отладки. Эта информация обычно не хранится в двоичном образе, потому что она не нужна при выполнении кода. To есть двоичные образы имеют меньший размер и работают быстрее. Ho это означает, что вам нужно позаботиться о том, чтобы у отладчика был доступ к файлам символов, сопоставляемым с образами, на которые вы ссылаетесь в сеансе отладки.

Для изучения внутренних структур данных ядра Windows (например, списка процессов, блоков потока, списка загруженных драйверов, информации об использовании памяти и т. д.) вам понадобятся подходящие файлы символов как минимум для образа ядра, Ntoskrnl.exe. (Подробнее этот файл рассматривается в разделе «Обзор архитектуры» главы 2.) Файлы таблиц символов должны соответствовать версии образа. Так, если вы установили Windows Service Pack или какое-то оперативное исправление, то должны получить обновленные файлы символов хотя бы для образа ядра; иначе возникнет ошибка из-за неправильной контрольной суммы при попытке отладчика ядра загрузить их.

Хотя можно скачать и установить символы для разных версий Windows, обновленные символы для оперативных исправлений доступны не всегда. Самый простой способ получить подходящую версию символов для отладки - обратиться к Microsoft-серверу символов с запросом, в котором используется специальный синтаксис пути к символам, как в отладчике. Например,

следующий путь к символам заставляет средства отладки загружать требуемые символы с Интернет-сервера символов и сохранять локальную копию в папке c:\symbols:

srv*c:\symbols* http://msdl.microsoft.com/downloacl/symbols

Подробные инструкции о том, как пользоваться сервером символов, см. в справочном файле Debugging Tools или на Web-странице wwwmicrosoft. com/whdc/ddk/debugging/symbols.mspx.

Windows Debugging Tools

Пакет Windows Debugging Tools содержит дополнительные средства отладки, применяемые в этой книге для исследования внутреннего устройства Windows. Вы найдете их последние версии по ссылке www.microsoft.com/ whdc/ddk/debugging. Эти средства можно использовать для отладки как процессов пользовательского режима, так и ядра (см. следующую врезку).

ПРИМЕЧАНИЕ Windows Debugging Tools регулярно обновляются и выпускаются независимо от версий операционной системы Windows, поэтому почаще проверяйте наличие новых версий отладочных средств.

Отладка в пользовательском режиме

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

(o) Invasive (инвазивный) Если не указано иное, то, когда вы подключаетесь к выполняемому процессу, Windows-функция DebugAc-tiveProcess устанавливает соединение между отладчиком и отлаживаемым процессом. Это позволяет изучать и/или изменять память процесса, устанавливать точки прерывания (breakpoints) и выполнять другие отладочные действия. B Windows 2000 при завершении отладчика закрывается и отлаживаемый процесс. Однако в Windows XP отладчик можно отключать, не уничтожая целевой процесс.

(o) Noninvasive (неинвазивный) B этом случае отладчик просто открывает процесс через функцию OpenProcess. Он не подключается к процессу как отладчик Это позволяет изучать и/или изменять память целевого процесса, но не дает возможности устанавливать точки прерывания. Преимущество данного варианта в том, что в Windows 2000 можно закрыть отладчик, не завершая целевой процесс.

C помощью отладочных средств также можно открывать файлы дампов процессов пользовательского режима. Что представляют собой эти файлы, поясняется в главе 3 в разделе по диспетчеризации исключений.

Microsoft предлагает отладчики ядра в двух версиях: командной строки (Kd.exe) и с графическим пользовательским интерфейсом p^indbg.exe). Оба

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

(o) Откройте файл дампа, полученный в результате краха системы с Windows (подробнее о таких дампах см. главу 14).

(o) Подключитесь к работающей системе и изучите ее состояние (или поставьте точки прерывания, если вы отлаживаете код драйвера устройства). Эта операция требует двух компьютеров - целевого и управляющего. Целевой считается отлаживаемая система, а управляющей - та, в которой выполняется отладчик. Целевая система может быть либо локальной (соединенной с управляющей нуль-модемным кабелем или по IEEE 1394), либо удаленной (соединенной по модему). Вы должны загрузить целевую систему со спецификатором /DEBUG, или нажать при загрузке клавишу F8 и выбрать Debug Mode, или добавить соответствующую запись в файл Boot.ini.

(o) B случае Windows XP и Windows Server 2003 подключитесь к локальной системе и изучите ее состояние. Это называется локальной отладкой ядра. Чтобы инициировать такую отладку ядра, выберите в меню FiIe команду Kernel Debug, перейдите на вкладку Local и щелкните ОК. Пример окна с выводом показан на рис. 1-7. Некоторые команды отладчика ядра в этом режиме не работают (например, просмотр стеков ядра и создание дампа памяти командой .dump невозможны). Однако вы можете пользоваться бесплатной утилитой LiveKd с сайта wwwsysinternals.com в тех случаях, когда родные средства локальной отладки не срабатывают (см. следующий раздел).

Подключившись в режиме отладки ядра, вы можете использовать одну из многих команд расширения отладчика (команды, которые начинаются с «!») для вывода содержимого внутренних структур данных, например потоков, процессов, пакетов запроса на ввод-вывод и информации, связанной с управлением памятью. Команды отладчика ядра и их вывод будут обсуждаться при рассмотрении соответствующей тематики. A пока добавим, что команда dt (display type) может форматировать свыше 400 структур ядра благодаря тому, что файлы символов ядра для Windows 2000 Service Pack 3, Windows XP и Windows Server 2003 содержат информацию о типах, которая и позволяет отладчику форматировать структуры.

ЭКСПЕРИМЕНТ: отображение информации о типах для структур ядра

Чтобы вывести список структур ядра, чья информация о типах включена в символы ядра, наберите dt nt!_* в отладчике ядра. Пример части вывода показан ниже:

Команда dt позволяет искать конкретные структуры по шаблонам. Например, если вы ищете имя структуры для объекта прерывания (interrupt object), введите dt nt!*interrupt*:

Заметьте, что по умолчанию dt не показывает подструктуры (структуры внутри структур). Для рекурсивного прохода по подструктурам, используйте ключ -r. Например, указав этот ключ для отображения объекта ядра «прерывание», вы увидите формат структуры _LIST_ENTRY, хранящейся в поле InterruptListEntry:

B справочном файле Windows Debugging Tools объясняется, как устанавливать и использовать отладчики ядра. Дополнительные сведения о применении отладчиков ядра, предназначенных в основном разработчикам драйверов устройств, см. в документации Windows DDK. Есть также несколько полезных статей в Knowledge Base по отладчикам ядра. Выполните поиск по ключевому слову «debugref» в Windows Knowledge Base (онлайновой базе данных технических статей) на supportmicrosoft.com.

Утилита LiveKd

LiveKd - бесплатная утилита, которая позволяет использовать стандартные отладчики ядра от Microsoft на «живой» системе - без подключения второго компьютера. Если встроенная поддержка локальной отладки ядра действует только в Windows XP и Windows Server 2003, то LiveKd обеспечивает такую отладку в Windows NT 4.0, Windows 2000, Windows XP и Windows Server 2003.

LiveKd запускается точно так же, как Windbg или Kd. Эта утилита передает любые указанные параметры командной строки выбранному вами отладчику. По умолчанию LiveKd запускает отладчик Kd. Для запуска GUI-отлад-

чика (Windbg), задайте ключ -w. Чтобы получить подсказку по ключам Live-Kd, укажите ключ -?.

LiveKd предоставляет отладчику смоделированный файл аварийного дампа (crash dump), поэтому вы можете выполнять в LiveKd любые операции, поддерживаемые для аварийных дампов. Поскольку LiveKd хранит смоделированный дамп в физической памяти, отладчик ядра может попасть в такую ситуацию, в которой структуры данных находятся в рассогласованном состоянии в процессе их изменения системой. При каждом запуске отладчик получает снимок состояния системы; если вы хотите обновить этот снимок, выйдите из отладчика (командой q), и LiveKd спросит вас, нужно ли начать сначала. Если отладчик, выводя информацию на экран, вошел в цикл, нажмите клавиши Ctrl+C, чтобы прервать вывод, выйдите из отладчика и запустите его снова. Если он завис, нажмите клавиши Ctrl+Break, которые заставят завершить процесс отладчика. После этого вам будет предложено снова запустить отладчик.

SoftlCE

Еще один инструмент, не требующий двух машин для прямой отладки ядра, - SoftICE, который можно приобрести у Compuware NuMega ( wwtv.com- puware.com). SoftICE обладает во многом теми же возможностями, что и Windows Debugging Tools, но поддерживает переход между кодом пользовательского режима и режима ядра. Он также поддерживает DLL-модули расширения ядра Microsoft, поэтому большинство команд, описываемых нами в книге, будут работать и в SoftICE. Ha рис. 1-8 показан пользовательский интерфейс SoftICE, появляющийся при нажатии клавиши активизации SoftICE (по умолчанию - Ctrl+D); этот интерфейс представляет собой окно на рабочем столе системы, в которой он выполняется.

Platform Software Development Kit (SDK)

Platform SDK является частью подписки на MSDN уровня Professional и выше; кроме того, его можно бесплатно скачать с msdn.microsoft.com. B нем содержатся документация, заголовочные файлы и библиотеки С, необходимые для компиляции и компоновки Windows-приложений. (Microsoft Visual C++ тоже поставляется с этими файлами, но их версии в Platform SDK всегда более новые и соответствуют самым последним версиям операционных систем Windows.) B Platform SDK для нас будут представлять интерес заголовочные файлы Windows API (\Program Files\Microsoft SDK\Include) и несколько утилит (Pfmon.exe, Pstat.exe, Pview.exe, Vadump.exe и Winobj.exe). Некоторые из них также поставляются с Ресурсами Windows и Support Tools. Наконец, отдельные утилиты поставляются с Platform SDK и MSDN Library как примеры исходного кода.

Device Driver Kit (DDK)

Windows DDK является частью подписки на MSDN уровня Professional и выше, но в отличие от Platform SDK его нельзя скачать бесплатно (впрочем, можно заказать CD-ROM за минимальную цену). Документация Windows DDK включается в MSDN Library.

Хотя DDK нацелен на разработчиков драйверов устройств, он представляет собой богатый источник информации о внутреннем устройстве Windows. Например, в главе 9 мы даем описание архитектуры подсистемы ввода-вывода, модели драйверов и структур данных базовых драйверов устройств, но не вдаемся в детали соответствующих функций ядра. A в документации Windows DDK исчерпывающе описаны все внутрисистемные функции и драйверы устройств.

Кроме документации в DDK входят заголовочные файлы, определяющие ключевые внутренние структуры данных, константы и интерфейсы многих внутрисистемных подпрограмм (в частности, обратите внимание на файлы Ntddk.h и Wdm.h). Эти файлы очень полезны в исследовании внутренних структур данных Windows с помощью отладчика ядра, так как мы даем лишь обобщенное описание внутренних структур, а в заголовочных файлах можно найти все подробности о каждом поле таких структур. B DDK также детально поясняются некоторые структуры данных (вроде заголовков для диспетчера объектов, блоки ожидания, события, мутанты, семафоры и др.).

Поэтому, если вы хотите поглубже покопаться в подсистеме ввода-вывода и в модели драйверов, читайте документацию DDK (особенно руководства Kernel-Mode Driver Architecture Design Guide и Reference). Еще один превосходный источник - книга Уолта Они (Walt Oney) «Programming the Microsoft Windows Driver Model, Second Edition» (Microsoft Press).

Утилиты Sysinternals

Bo многих экспериментах мы используем свободно распространяемые утилиты, которые можно скачать с wwwjsysinternals.com. Большинство этих ути-

лит написано Марком Руссиновичем, соавтором этой книги. K наиболее популярным утилитам относятся Process Explorer, Filemon и Regmon. Многие из этих утилит требуют установки и запуска драйверов устройств, работающих в режиме ядра, а значит, вам понадобятся полномочия администратора.

Резюме

B этой главе вы познакомились с ключевыми техническими концепциями и терминами Windows, которые будут использоваться во всей книге. Вы также получили первое представление о многих полезных инструментах, позволяющих изучать внутренние структуры данных Windows. Теперь вы готовы вместе с нами приступить к исследованию внутреннего устройства системы. Мы начнем с общего обзора архитектуры системы и ее основных компонентов.

Г Л A B A 2 Архитектура системы

Г Л A B A 2 Архитектура системы

Теперь, познакомившись с необходимыми терминами, понятиями и инструментами, мы можем рассмотреть задачи, которые ставились при разработке операционной системы Microsoft Windows. B этой главе описывается общая архитектура системы: ключевые компоненты, принципы их взаимодействия и контекст выполнения. Чтобы получить базовое представление о внутреннем устройстве Windows, давайте сначала обсудим требования и цели, обусловившие структуру и спецификацию этой системы.

Требования и цели проекта

Характеристики Windows NT в 1989 году определялись следующими требованиями. Операционная система должна:

(o) быть истинно 32-разрядной, реентерабельной, поддерживать вытесняющую многозадачность и работу с виртуальной памятью;

(o) работать на разных аппаратных платформах;

(o) хорошо масштабироваться в системах с симметричной мультипроцессорной обработкой;

(o) быть распределенной вычислительной платформой, способной выступать в роли как клиента сети, так и сервера;

(o) поддерживать большинство существующих 16-разрядных приложений

MS-DOS и Microsoft Windows 3.1; (o) отвечать требованиям правительства к соответствию POSIX 1003.1;

(o) отвечать требованиям правительства и промышленности к безопасности операционных систем;

(o) обеспечивать простоту адаптации к глобальному рынку за счет поддержки Unicode.

Для создания системы, соответствующей предъявленным требованиям, нужно было принять тысячи решений. Поэтому перед командой разработчиков Windows NT на начальном этапе проекта были поставлены следующие цели.

(o) Расширяемость Код должен быть написан так, чтобы системы можно было легко наращивать и модифицировать по мере изменения потребностей рынка.

(o) Переносимость Система должна работать на разных аппаратных архитектурах и обладать способностью к сравнительно легкому переносу на новые аппаратные архитектуры, если на рынке возникнет такая потребность.

(o) Отказоустойчивость и надежность Система должна быть защищенной как от внутренних сбоев, так и от внешних деструктивных действий. У приложений не должно быть возможности нарушить работу операционной системы или других приложений.

(o) Совместимость Хотя Windows NT должна расширить существующую технологию, ее пользовательский интерфейс и API должны быть совместимы с предыдущими версиями Windows и MS-DOS. Она также должна уметь взаимодействовать с другими системами вроде UNIX, OS/2 и NetWare.

(o) Производительность C учетом ограничений, налагаемых поставленными целями, система должна быть максимально быстрой и отзывчивой независимо от аппаратной платформы.

По мере изучения деталей внутренней структуры Windows вы увидите, насколько успешно были реализованы все эти требования и цели. Ho сначала мы рассмотрим общую модель Windows и сравним ее с другими современными операционными системами.

Модель операционной системы

B большинстве многопользовательских операционных систем приложения отделены от собственно операционной системы: код ее ядра выполняется в привилегированном режиме процессора (называемом режимом ядра), который обеспечивает доступ к системным данным и оборудованию. Код приложений выполняется в непривилегированном режиме процессора (называемом пользовательским) с неполным набором интерфейсов, ограниченным доступом к системным данным и без прямого доступа к оборудованию. Когда программа пользовательского режима вызывает системный сервис, процессор перехватывает вызов и переключает вызывающий поток в режим ядра. По окончании работы системного сервиса операционная система переключает контекст потока обратно в пользовательский режим и продолжает его выполнение.

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

Основана ли Windows на микроядре?

Хотя некоторые объявляют ее таковой, Windows не является операционной системой на основе микроядра в классическом понимании этого термина. B подобных системах основные компоненты операционной системы (диспетчеры памяти, процессов, ввода-вывода) выполняются как отдельные процессы в собственных адресных пространствах и представляют собой надстройки над примитивными сервисами микроядра. Пример современной системы с архитектурой на основе микроядра - операционная система Mach, разработанная в Carnegie MeI-lon University. Она реализует крошечное ядро, которое включает сервисы планирования потоков, передачи сообщений, виртуальной памяти и драйверов устройств. Все остальное, в том числе разнообразные API, файловые системы и поддержка сетей, работает в пользовательском режиме. Однако в коммерческих реализациях на основе микроядра Mach код файловой системы, поддержки сетей и управления памятью выполняется в режиме ядра. Причина проста: системы, построенные строго по принципу микроядра, непрактичны с коммерческой точки зрения из-за слишком низкой эффективности.

Означает ли тот факт, что большая часть Windows работает в режиме ядра, ее меньшую надежность в сравнении с операционными системами на основе микроядра? Вовсе нет. Рассмотрим следующий сценарий. Допустим, в коде файловой системы имеется ошибка, которая время от времени приводит к краху системы. Ошибка в коде режима ядра (например, в диспетчере памяти или файловой системы) скорее всего вызовет полный крах традиционной операционной системы. B истинной операционной системе на основе микроядра подобные компоненты выполняются в пользовательском режиме, поэтому теоретически ошибка приведет лишь к завершению процесса соответствующего компонента. Ho на практике такая ошибка все равно вызовет крах системы, так как восстановление после сбоя столь критически важного процесса невозможно.

Все эти компоненты операционной системы, конечно, полностью защищены от сбойных приложений, поскольку такие программы не имеют прямого доступа к коду и данным привилегированной части операционной системы (хотя и способны вызывать сервисы ядра). Эта защита - одна из причин, по которым Windows заслужила репутацию отказоустойчивой и стабильной операционной системы в качестве сервера приложений и платформы рабочих станций, обеспечивающей быстродействие основных системных сервисов вроде поддержки виртуальной памяти, файлового ввода-вывода, работы с сетями и доступа к общим файлам и принтерам.

Компоненты Windows режима ядра также построены на принципах объектно-ориентированного программирования (ООП). Так, для получения информации о каком-либо компоненте они, как правило, не обращаются к его структурам данных. Вместо этого для передачи параметров, доступа к структурам данных и их изменения используются формальные интерфейсы.

Однако, несмотря на широкое использование объектов, представляющих разделяемые системные ресурсы, Windows не является объектно-ориентированной системой в строгом понимании этого термина. Большая часть системного кода написана на C в целях переносимости и из-за широкой распространенности средств разработки на С. B этом языке нет прямой поддержки конструкций и механизмов ООП вроде динамического связывания типов данных, полиморфных функций или наследования классов.

Обзор архитектуры

Теперь обратимся к ключевым компонентам системы, составляющим ее архитектуру. Упрощенная версия этой архитектуры показана на рис. 2-1. Учтите, что упрощенная схема не отражает всех деталей архитектуры (например, здесь не показаны уровни сетевых компонентов и различных типов драйверов устройств).

Рис. 2-1. Упрощенная схема архитектуры Windows

Ha рис. 2-1 прежде всего обратите внимание на линию, разделяющую те части Windows, которые выполняются в режиме ядра и в пользовательском режиме. Прямоугольники над этой линией соответствуют процессам пользовательского режима, а компоненты под ней - сервисам режима ядра. Как говорилось в главе 1, потоки пользовательского режима выполняются в защищенных адресных пространствах процессов (хотя при выполнении в режиме ядра они получают доступ к системному пространству). Таким образом, процессы поддержки системы, сервисов, приложений и подсистем окружения имеют свое адресное пространство.

Существует четыре типа пользовательских процессов:

(o) фиксированные процессы поддержки системы (system support processes) - например, процесс обработки входа в систему и диспетчер сеансов, не являющиеся сервисами Windows (т. е. не запускаемые диспетчером управления сервисами);

(o) процессы сервисов (service processes) - носители Windows-сервисов вроде Task Scheduler и Spooler. Многие серверные приложения Windows, например Microsoft SQL Server и Microsoft Exchange Server, тоже включают компоненты, выполняемые как сервисы;

(o) пользовательские приложения (user applications) - бывают шести типов: для 32-разрядной Windows, 64-разрядной Windows, 16-разрядной Windows 3.1, 16-разрядной MS-DOS, 32-разрядной POSIX и 32-разрядной OS/2;

(o) подсистемы окружения (environment subsystems) - реализованы как часть поддержки среды операционной системы, предоставляемой пользователям и программистам. Изначально Windows NT поставлялась с тремя подсистемами окружения: Windows, POSIX и OS/2. Последняя была изъята в Windows 2000. Что касается Windows XP, то в ней исходно поставляется только подсистема Windows - улучшенная подсистема POSIX доступна как часть бесплатного продукта Services for UNIX. Обратите внимание на прямоугольник «DLL подсистем», расположенный на рис. 2-1 под прямоугольниками «процессы сервисов» и «пользовательские приложения». B Windows пользовательские приложения не могут вызывать родные сервисы операционной системы напрямую, вместо этого они работают с одной или несколькими DLL подсистем. Их назначение заключается в трансляции документированных функций в соответствующие внутренние (и обычно недокументированные) вызовы системных сервисов Windows. Трансляция может осуществляться как с помощью сообщения, посылаемого процессу подсистемы окружения, обслуживающему пользовательское приложение, так и без него.

Windows включает следующие компоненты режима ядра.

(o) Исполнительная система (executive) Windows, содержащая базовые сервисы операционной системы, которые обеспечивают управление памятью, процессами и потоками, защиту, ввод-вывод и взаимодействие между процессами.

(o) Ядро (kernel) Windows, содержащее низкоуровневые функции операционной системы, которые поддерживают, например, планирование потоков, диспетчеризацию прерываний и исключений, а также синхронизацию при использовании нескольких процессоров. Оно также предоставляет набор процедур и базовых объектов, применяемых исполнительной системой для реализации структур более высокого уровня.

(o) Драйверы устройств (device drivers), в состав которых входят драйверы аппаратных устройств, транслирующие пользовательские вызовы функций ввода-вывода в запросы, специфичные для конкретного устройства, а также сетевые драйверы и драйверы файловых систем.

(o) Уровень абстрагирования от оборудования (hardware abstraction layer, HAL), изолирующий ядро, драйверы и исполнительную систему Windows от специфики оборудования на данной аппаратной платформе (например, от различий между материнскими платами).

(o) Подсистема поддержки окон и графики (windowing and graphics system), реализующая функции графического пользовательского интерфейса (GUI), более известные как Windows-функции модулей USER и GDL Эти функции обеспечивают поддержку окон, элементов управления пользовательского интерфейса и отрисовку графики.

B таблице 2-1 перечислены имена файлов основных компонентов Windows. (Вы должны знать их, потому что в дальнейшем мы будем ссылаться на некоторые системные файлы по именам.) Каждый из этих компонентов подробно рассматривается в этой и последующих главах.

Прежде чем детально рассматривать эти компоненты, давайте проясним, как достигается переносимость Windows между различными аппаратными платформами.

Переносимость

Windows рассчитана на разные аппаратные платформы, включая как CISC-системы Intel, так и RISC-системы. Windows NT первого выпуска поддерживала архитектуры x86 и MIPS. Спустя некоторое время была добавлена поддержка Alpha AXP производства DEC (DEC была приобретена Compaq, а позднее произошло слияние компаний Compaq и Hewlett Packard). (Хотя Alpha AXP был 64-разрядным процессором, Windows NT работала с ним в 32-разрядном режиме. B ходе разработки Windows 2000 была создана ее 64-разрядная версия специально под Alpha AXP, но в свет она так и не вышла.) B Windows NT 3.51 ввели поддержку четвертой процессорной архитектуры - Motorola PowerPC. B связи с изменениями на рынке необходимость в поддержке MIPS и PowerPC практически отпала еще до начала разработки Windows 2000. Позднее Compaq отозвала поддержку архитектуры Alpha AXP, и в Windows 2000 осталась поддержка лишь архитектуры x86. B самые последние выпуски - Windows XP и Windows Server 2003 - добавлена поддержка трех семейств 64-разрядных процессоров: Intel Itanium IA-64, AMD x86-64 и Intel 64-bit Extension Technology (EM64T) для x86 (эта архитектура совместима с архитектурой AMD x86-64, хотя есть небольшие различия в поддерживаемых командах). Последние два семейства процессоров называются системами с 64-разрядными расширениями и в этой книге обозначаются как x64. (Как 32-разрядные приложения выполняются в 64-разрядной Windows, объясняется в главе 3.)

Переносимость Windows между системами с различной аппаратной архитектурой и платформами достигается главным образом двумя способами.

(o) Windows имеет многоуровневую структуру. Специфичные для архитектуры процессора или платформы низкоуровневые части системы вынесены в отдельные модули. Благодаря этому высокоуровневая часть системы не зависит от специфики архитектур и аппаратных платформ. Ключевые компоненты, обеспечивающие переносимость операционной системы, - ядро (содержится в файле Ntoskrnl.exe) и уровень абстрагирования от оборудования (HAL) (содержится в файле Hal.dll). Функции, специфичные для конкретной архитектуры (переключение контекста потоков, диспетчеризация ловушек и др.), реализованы в ядре. Функции, которые могут отличаться на компьютерах с одинаковой архитектурой (например, в системах с разными материнскими платами), реализованы в HAL. Еще один компонент, содержащий большую долю кода, специфичного для конкретной архитектуры, - диспетчер памяти (memory manager), но если рассматривать систему в целом, такого кода все равно немного.

(o) Подавляющее большинство компонентов Windows написано на C и лишь часть из них - на C++. Язык ассемблера применяли только при создании частей системы, напрямую взаимодействующих с системным оборудованием (например, при написании обработчика ловушек прерываний) или требующих исключительного быстродействия (скажем, при переключении контекста). Ассемблерный код имеется не только в ядре и HAL, но и в составе некоторых других частей операционной системы: процедур, реализующих взаимоблокировку, механизма вызова локальных процедур (LPC), части подсистемы Windows, выполняемой в режиме ядра, и даже в некоторых библиотеках пользовательского режима (например, в коде запуска процессов в Ntdll.dll - системной библиотеке, о которой будет рассказано в этой главе несколько позже).

Симметричная многопроцессорная обработка

Многозадачность (multitasking) - механизм операционной системы, позволяющий использовать один процессор для выполнения нескольких потоков. Однако истинно одновременное выполнение, например, двух потоков возможно, только если на компьютере установлено два процессора. При многозадачности система лишь создает видимость одновременного выполнения множества потоков, тогда как многопроцессорная система действительно выполняет сразу несколько потоков - по одному на каждом процессоре.

Как уже говорилось в начале этой главы, одной из ключевых целей разработки Windows была поддержка многопроцессорных компьютерных систем. Windows является операционной системой, поддерживающей симметричную многопроцессорную обработку (symmetric multiprocessing, SMP). B этой модели нет главного процессора; операционная система, как и пользовательские потоки, может выполняться на любом процессоре. Кроме того, все процессоры используют одну итуже память. При асимметричной многопроцессорной обработке (asymmetric multiprocessing, ASMP) система, напротив, выбирает один из процессоров для выполнения кода ядра операционной системы, а другие процессоры выполняют только пользовательский код. Различия между этими двумя моделями показаны на рис. 2-2.

Windows XP и Windows Server 2003 поддерживают два новых типа многопроцессорных систем: логические процессоры (hyperthreading) и NUMA (Non-Uniform Memory Architecture). Об этом кратко рассказывается в абзаце ниже. (Полное описание поддержки планирования потоков для таких систем см. в разделе по планированию потоков в главе 6.)

Логические процессоры - это технология, созданная Intel; благодаря ей на одном физическом процессоре может быть несколько логических. Каждый логический процессор имеет свое состояние, но исполняющее ядро (execution engine) и набортный кэш (onboard cache) являются общими. Это позволяет одному из логических процессоров продолжать работу, пока другой логический процессор занят (например, обработкой прерывания, которая не дает потокам выполняться на этом логическом процессоре). Алгоритмы планирования в Windows XP были оптимизированы под компьютеры с такими процессорами.

B NUMA-системах процессоры группируются в блоки, называемые узлами (nodes). B каждом узле имеются свои процессоры и память, и он соединяется с остальными узлами специальной шиной. Windows в NUMA-систе-ме по-прежнему работает как SMP-система, в которой все процессоры имеют доступ ко всей памяти, - просто доступ к памяти, локальной для узла, осуществляется быстрее, чем к памяти в других узлах. Система стремится повысить производительность, выделяя потокам время на процессорах, которые находятся в том же узле, что и используемая память. Она также пытается выделять память в пределах узла, но при необходимости выделяет память и из других узлов.

Хотя Windows изначально разрабатывалась для поддержки до 32 процессоров, многопроцессорной модели не свойственны никакие внутренние особенности, которые ограничивали бы число используемых процессоров до 32. Просто это число легко представить битовой маской с помощью машинного 32-разрядного типа данных. И действительно, 64-разрядные версии Windows поддерживают до 64 процессоров, потому что размер слова на 64-разрядных процессорах равен 64 битам.

Реальное число поддерживаемых процессоров зависит от конкретного выпуска Windows (см. таблицы 2-3 и 2-4). Это число хранится в параметре реестра HKLM\SYSTEM\CurrentControlSet\Control\Session\Manager\Licensed-Processors. Учтите, что модификация этого параметра считается нарушением условий лицензионного соглашения на программное обеспечение, да и для увеличения числа поддерживаемых процессоров требуется нечто большее, чем простое изменение данного параметра.)

Для большей производительности ядро и HAL имеют одно- и многопроцессорную версии. B случае Windows 2000 это относится к шести ключевым системным файлам (см. примечание ниже), а в 32-разрядных Windows XP и Windows Server 2003 - только к трем (см. таблицу 2-2). B 64-разрядных системах Windows ядра PAE нет, поэтому одно- и многопроцессорные системы отличаются лишь ядром и HAL.

Соответствующие файлы выбираются и копируются в локальный каталог \Windows\System32 на этапе установки. Чтобы определить, какие файлы были скопированы, см. файл \Windows\Repair\Setup.log, где перечисляются все файлы, копировавшиеся на локальный системный диск, и каталоги на дистрибутивном носителе, откуда они были взяты.

ПРИМЕЧАНИЕ B папке \I386\UNIPROC в дистрибутиве Windows 2000 находится файл Winsrv.dll. Хотя он помещен в папку UNIPROC, название которой указывает на однопроцессорную версию, на самом деле для одно- и многопроцессорных систем существует только одна версия этого образа.

ЭКСПЕРИМЕНТ: поиск файлов поддержки многопроцессорных систем в Windows 2000

Вы можете убедиться в том, что для многопроцессорной 32-разрядной системы Windows 2000 используются другие файлы, просмотрев сведения о драйверах для Computer (Компьютер) в Device Manager (Диспетчер устройств).

1. Откройте окно свойств системы, дважды щелкнув System (Система) в окне Control Panel (Панель управления) или щелкнув правой кнопкой мыши My Computer (Мой компьютер) на рабочем столе и выбрав из контекстного меню команду Properties (Свойства).

2. Перейдите на вкладку Hardware (Оборудование).

3. Щелкните кнопку Device Manager (Диспетчер устройств).

4. Раскройте объект Computer (Компьютер).

5. Дважды щелкните дочерний узел объекта Computer.

6. Откройте вкладку Driver (Драйвер).

7. Щелкните кнопку Driver Details (Сведения о драйверах).

B многопроцессорной системе вы должны увидеть диалоговое окно, показанное ниже.

Специальные версии этих ключевых системных файлов для однопроцессорных систем созданы для максимального повышения производительности. Синхронизация работы нескольких процессоров - задача принципиально более сложная, и благодаря «однопроцессорным» версиям системных файлов устраняются издержки этой синхронизации, которая в однопроцессорных системах (а они составляют подавляющее большинство систем под управлением Windows) не нужна.

Интересно, что «однопроцессорная» и «многопроцессорная» версии Ntoskrnl создаются за счет условной компиляции одного и того же исходного кода, а «однопроцессорные» версии Ntdll.dll и Kernel32.dll для Windows 2000 требуют замены машинных х86-команд LOCK и UNLOCK, используемых для синхронизации множества потоков, командой NOP (которая ничего не делает).

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

ЭКСПЕРИМЕНТ: определение текущей версии Ntoskrnl

B Windows 2000 и выше нет утилиты, показывающей, с какой версией Ntoskrnl вы работаете. Однако при каждой загрузке в журнале системы регистрируется, какая версия ядра запускается - одно- или многопроцессорная, отладочная или конечная (см. следующую иллюстрацию). Выберите из меню Start (Пуск) команду Programs (Программы), затем Administrative Tools (Администрирование) и Event Viewer (Просмотр событий). Далее выберите System Log (Журнал системы) и дважды щелкните запись с кодом события 6009 - она создается при загрузке системы.

Эта запись не содержит сведений о том, загружена ли РАЕ-версия образа ядра, поддерживающая более 4 Гб физической памяти (Ntkrnlpa.exe). Однако вы можете узнать это, проверив значение параметра SystemStartOptions в разделе реестра HKLM\SYSTEM\Current-ControlSet\Control. Кроме того, при загрузке РАЕ-версии ядра параметру PhysicalAddressExtension в разделе реестра HKLM\SYSTEM\Current-ControlSet\Control\Session Manager\Memory Management присваивается значение, равное 1.

Есть и другой способ определить, установлена ли многопроцессорная версия Ntoskrnl (или Ntkrnlpa): запустите Windows Explorer (Проводник), в каталоге \Windows\System32 щелкните правой кнопкой мыши файл Ntoskrnl.exe и выберите из контекстного меню команду Pro-

perties (Свойства). Перейдите на вкладку Version(Версия) и выберите свойство Original Filename (Исходное имя файла). Если вы работаете с многопроцессорной версией, то увидите диалоговое окно, показанное на предыдущей странице.

Наконец, просмотрев файл \Windows\Repair\Setup.log, можно точно выяснить, какие файлы ядра и HAL были выбраны при установке.

Масштабируемость

Масштабируемость (scalability) - одна из ключевых целей многопроцессорных систем. Для корректного выполнения в SMP-системах операционная система должна строго соответствовать определенным требованиям. Решить проблемы конкуренции за ресурсы и другие вопросы в многопроцессорных системах сложнее, чем в однопроцессорных, и это нужно учитывать при разработке системы. Некоторые особенности Windows оказались решающими для ее успеха как многопроцессорной операционной системы:

(o) способность выполнять код операционной системы на любом доступном процессоре и на нескольких процессорах одновременно;

(o) несколько потоков одного процесса можно параллельно выполнять на разных процессорах;

(o) тонкая синхронизация внутри ядра (спин-блокировки, спин-блокировки с очередями и др.; см. главу 3), драйверов устройств и серверных процессов позволяет выполнять больше компонентов на нескольких процессорах одновременно;

(o) механизмы вроде портов завершения ввода-вывода (см. главу 9), облегчающие эффективную реализацию многопоточных серверных процессов, хорошо масштабируемых в многопроцессорных системах. Масштабируемость ядра Windows со временем улучшалась. Например, в Windows Server 2003 имеются очереди планирования, индивидуальные для каждого процессора, что дает возможность планировать потоки параллельно на нескольких машинах. O планировании потоков в многопроцессорных системах см. главу 6, а о синхронизации в таких системах - главу 3.

Различия между клиентскими и серверными версиями

Windows поставляется в клиентских и серверных версиях. B Windows 2000 клиентская версия называется Windows 2000 Professional. Существует также три серверных версии Windows 2000: Windows 2000 Server, Advanced Server и Datacenter Server.

У Windows XP шесть клиентских версий: Windows XP Home Edition, Windows XP Professional, Windows XP Starter Edition, Windows XP Tablet PC Edition, Windows XP Media Center Edition и Windows XP Embedded. Последние три являются надмножествами Windows XP Professional и в книге детально не рассматриваются, так как все они построены на том же базовом коде, что и Windows XP Professional.

Windows Server 2003 выпускается в шести разновидностях: Windows Server 2003 Web Edition, Standard Edition, Small Business Server, Storage Server, Enterprise Edition и Datacenter Edition.

Эти версии различаются по следующим параметрам:

(o) числу поддерживаемых процессоров;

(o) объему поддерживаемой физической памяти;

(o) возможному количеству одновременных сетевых соединений (например, в клиентской версии допускается максимум 10 одновременных соединений со службой доступа к общим файлам и принтерам);

(o) наличием в выпусках Server сервисов, не входящих в Professional (например, служб каталогов, поддержкой кластеризации и многопользовательской службы терминала).

Эти различия для Windows 2000 суммируются в таблице 2-3. Ta же информация, но применительно к Windows XP и Windows Server 2003 дана в таблице 2-4.

Хотя существует несколько клиентских и серверных выпусков операционной системы Windows, у них общий набор базовых системных файлов, в том числе: ядро, Ntoskrnl.exe (а также версия РАЕ, Ntkrnlpa.exe), библиотеки HAL, драйверы, основные системные утилиты и DLL. Эти файлы идентичны для всех выпусков Windows 2000.

ПРИМЕЧАНИЕ Windows XP была первым клиентским выпуском кодовой базы Windows NT, который поставляется без соответствующих серверных версий. Вместо этого разработки продолжались, и примерно год спустя после выхода Windows XP была выпущена Windows Server 2003. Таким образом, базовые системные файлы Windows XP и Windows Server 2003 не идентичны. Однако они не столь значимы (и во многих случаях компоненты не изменялись).

Итак, если образ ядра для Windows 2000 Professional и Windows 2000 Server одинаков (и сходен для Windows XP и Windows Server 2003), то как же система определяет, какой именно выпуск загружается? Для этого она проверяет значения параметров ProductType и ProductSuite в разделе реестра HKLM\SYSTEM\CurrentControlSet\Control\ProductOptions. Параметр ProductType используется, чтобы отличить клиентскую систему от серверной (любого выпуска). Список допустимых значений этого параметра приведен в таблице 2-5. Результат проверки помещается в глобальную системную переменную MmProductType, значение которой может быть запрошено драйвером устройства через функцию MmIsTbisAnNtAsSystem режима ядра, описанную в документации Windows DDK.

Другой параметр, ProductSuite, позволяет различать серверные версии Windows (Standard, Enterprise, Datacenter Server и др.), а также Windows XP Home от Windows XP Professional. Для проверки текущего выпуска Windows пользовательские программы вызывают Windows-функцию VerifyVersionInfo, описанную в Platform SDK. Драйверы могут вызвать функцию RtlGetVersion режима ядра, документированную в Windows DDK.

Итак, если базовые файлы в целом одинаковы для клиентских и серверных версий, то чем же отличается их функционирование? Серверные системы оптимизированы для работы в качестве высокопроизводительных серверов приложений, а клиентские, несмотря на поддержку серверных возможностей, - для персональных систем. Так, некоторые решения по выделению ресурсов (например, о числе и размере системных пулов памяти, количестве внутрисистемных рабочих потоков и размере системного кэша данных) при загрузке принимаются по-разному, в зависимости от типа продукта. Политика принятия таких решений, как обслуживание диспетчером памяти запросов системы и процессов на выделение памяти, у серверной и клиентской версий тоже различается. B равной мере это относится и к особенностям планирования потоков по умолчанию (детали см. в главе 6). Существенные отличия в функционировании этих двух продуктов будут отмечены в соответствующих главах. Любые материалы в нашей книге, если явно не указано иное, относятся к обеим версиям - клиентской и серверной.

Проверочный выпуск

Специальная отладочная версия Windows 2000 Professional, Windows XP Professional или Windows Server 2003 называется проверочным выпуском (checked build). Она доступна только подписчикам MSDN уровня Professional (или выше). Проверочный выпуск представляет собой перекомпилированный исходный код Windows, для которого флаг «DBG» (заставляющий включать код отладки и трассировки этапа компиляции) был установлен как TRUE. Кроме того, чтобы облегчить восприятие машинного кода, отключается обработка двоичных файлов, при которой структура кода оптимизируется для большего быстродействия (см. раздел «Performance-Optimized Code» в справочном файле Debugging Tools).

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

ЭКСПЕРИМЕНТ: определяем, является ли данная система проверочным выпуском

Встроенной утилиты, которая позволяла бы увидеть, с каким выпуском вы имеете дело - проверочным или готовым, нет. Однако эта информация доступна через свойство «Debug» WMI-класса Windows Management Instrumentation) Win32_OperatingSystem. Следующий сценарий на Visual Basic отображает содержимое этого свойства:

Значительная часть дополнительного кода в собранных таким образом двоичных файлах является результатом работы макроса ASSERT, определенного в заголовочном файле Ntddk.h, который входит в состав DDK. Этот макрос проверяет некое условие (например, правильность структуры данных или параметра) и, если значение выражения получается равным FALSE, вызывает функцию RtlAssert режима ядра, которая в свою очередь обращается KDbgPrint, передающей текст отладочного сообщения в буфер отладочных сообщений (debug message buffer). Если отладчик ядра подключен, это сообщение выводится на экран, а за ним автоматически появляется запрос к пользователю, какое действие следует предпринять (игнорировать, завершить процесс или поток и т. д.). Если система загружена без отладчика ядра (в отсутствие ключа /DEBUG в файле Boot.ini) и этот отладчик сейчас не подключен, неудачный тест ASSERT вызовет крах системы. Список тестов

ASSERT, выполняемых некоторыми вспомогательными процедурами ядра, см. в разделе «Checked Build ASSERTs» документации Windows DDK.

ПРИМЕЧАНИЕ Сравнив файл Ntoskrnl.exe с Ntkrnlmp.exe или Ntkrnlpa. exe с Ntkrpamp.exe в проверочной версии системы, вы убедитесь, что они идентичны и являются «многопроцессорными» версиями соответствующих файлов. Иначе говоря, в проверочной версии системы нет отладочных вариантов файлов для однопроцессорных систем.

Проверочный выпуск также полезен системным администраторам, так как в нем можно включить детальную трассировку для определенных компонентов. (Подробные инструкции см. в статье 3H743 «HOWTO: Enable Verbose Debug Tracing in Various Drivers and Subsystems» в Microsoft Knowledge Base.) Вывод такой трассировки посылается в буфер отладочных сообщений с помощью функции DbgPrint, о которой мы уже упоминали. Для просмотра отладочных сообщений к целевой системе можно подключить отладчик ядра (что потребует загрузки целевой системы в отладочном режиме), использовать команду !dbgprint в процессе локальной отладки ядра или применить утилиту Dbgview.exe с сайта wwwsysinternals.com.

Для использования возможностей отладочной версии операционной системы необязательно устанавливать весь проверочный выпуск. Можно просто скопировать проверочную версию образа ядра (Ntoskrnl.exe) и соответствующий HAL (Hal.dll) в обычную систему. Преимущество этого подхода в том, что он позволяет тщательно протестировать драйверы устройств и другой код ядра, не устанавливая медленнее работающие версии всех компонентов системы. O том, как это сделать, см. раздел «Installing Just the Checked Operating System and HAL» в документации Windows DDK. Поскольку Microsoft не поставляет проверочный выпуск Windows 2000 Server, вы можете применить этот способ и получить проверочную версию ядра в системе Windows 2000 Server.

Наконец, проверочная версия пригодится и для тестирования кода пользовательского режима, но только в том смысле, что в проверочной версии системы устанавливаются другие интервалы ожидания (тайминги). (Это связано с тем, что в ядре выполняются дополнительные проверки, а сами компоненты компилируются без оптимизации.) B таких условиях часто проявляются ошибки, связанные с синхронизацией нескольких потоков приложения.

Ключевые компоненты системы

Теперь, ознакомившись с высокоуровневой архитектурой Windows, копнем поглубже и рассмотрим роль каждого ключевого компонента системы. Ha рис. 2-3 отражена более подробная схема системной архитектуры Windows. Заметьте, что на ней все равно не показаны некоторые компоненты (в частности, компоненты сетевой поддержки, о которых пойдет речь в главе 13).

Основные элементы этой схемы детально описываются в последующих главах. B главе 3 рассказывается об основных механизмах управления,

используемых системой (в том числе о диспетчере объектов, прерываниях и т. п.), в главе 5 - о процессах запуска и завершения Windows, а в главе 4 - о таких механизмах управления, как реестр, процессы сервисов и Windows Management Instrumentation (WMI). B остальных главах не менее подробно поясняется внутреннее устройство и функционирование ключевых элементов - процессов, потоков, подсистемы управления памятью, защиты, диспетчера ввода-вывода, диспетчера кэша, файловой системы NTFS, сетевой поддержки и др.

Рис. 2-3 Подсистемы окружения и их DLL:

Как показано на рис. 2-3, в Windows имеется три подсистемы окружения: OS/2, POSIX и Windows. Как мы уже говорили, подсистема OS/2 была удалена в Windows 2000. Начиная с Windows XP, базовая подсистема POSIX не поставляется с Windows, но ее гораздо более совершенную версию можно получить бесплатно как часть продукта Services for UNIX.

Подсистема Windows отличается от остальных двух тем, что без нее Windows работать не может (эта подсистема обрабатывает все, что связано с клавиатурой, мышью и экраном, и нужна даже на серверах в отсутствие интерактивных пользователей). Фактически остальные две подсистемы запускаются только по требованию, тогда как подсистема Windows работает всегда.

Стартовая информация подсистемы хранится в разделе реестра HKLM\ SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems. Значения параметров в этом разделе показаны на рис. 2-4.

Значением параметра Required является список подсистем, загружаемых при запуске системы. Параметр состоит из двух строк: Windows и Debug. B параметре Windows указывается спецификация файла подсистемы Windows, Csrss.exe (аббревиатура от Client/Server Run-Time Subsystem; см. примечание ниже). Параметр Debug остается незаполненным (он используется для внутреннего тестирования) и не выполняет никаких функций. Параметр Optional указывает, что подсистемы OS/2 и POSIX запускаются по требованию. Параметр Kmode содержит имя файла той части подсистемы Windows, которая работает в режиме ядра, - Win32k.sys (об этом файле чуть позже).

Подсистемы окружения предоставляют прикладным программам некое подмножество базовых сервисов исполнительной системы Windows. Каждая подсистема обеспечивает доступ к разным подмножествам встроенных сервисов Windows. Это значит, что приложения, созданные для одной подсистемы, могут выполнять операции, невозможные в другой подсистеме. Так, Windows-приложения не могут использовать POSIX-функцию fork.

Каждый исполняемый образ (EXE) принадлежит одной - и только одной - подсистеме. При запуске образа код, отвечающий за создание процесса, получает тип подсистемы, указанный в заголовке образа, и уведомляет соответствующую подсистему о новом процессе. Тип указывается спецификатором /SUBSYSTEM в команде link в Microsoft Visual C++; его можно просмотреть с помощью утилиты Exetype, входящей в состав ресурсов Windows.

ПРИМЕЧАНИЕ Процесс подсистемы Windows назван Csrss.exe потому, что в Windows NT все подсистемы изначально предполагалось выполнять как потоки внутри единственного общесистемного процесса. Когда подсистемы POSIX и OS/2 были выделены в собственные процессы, имя файла процесса подсистемы Windows осталось прежним.

Смешивать вызовы функций разных подсистем нельзя. Иными словами, приложения POSIX могут вызывать только сервисы, экспортируемые подсистемой POSIX, а приложения Windows - лишь сервисы, экспортируемые подсистемой Windows. Как вы еще убедитесь, это ограничение послужило одной из причин, по которой исходная подсистема POSIX, реализующая весьма ограниченный набор функций (только POSIX 1003.1), не стала полезной средой для переноса в нее UNIX-приложений.

Мы уже говорили, что пользовательские приложения не могут вызывать системные сервисы Windows напрямую. Вместо этого они обращаются к DLL подсистем. Эти DLL предоставляют документированный интерфейс между программами и вызываемой ими подсистемой. Так, DLL подсистемы Windows (Kernel32.dll, Advapi32.dll, User32.dll и Gdi32.dll) реализуют функции Windows API. DLL подсистемы POSIX (Psxdll.dll) реализует POSIX API.

ЭКСПЕРИМЕНТ: определение типа подсистемы, для которой предназначен исполняемый файл

Вы можете определить, для какой подсистемы предназначен исполняемый файл с помощью утилиты Exetype из набора ресурсов Windows или утилиты DependencyWalker (Depends.exe), входящей в состав Windows Support Tools и Platform SDK. Попробуем, например, выяснить тип подсистемы для двух принципиально разных Windows-образов: Notepad.exe (простого текстового редактора) и Cmd.exe (поддержки командной строки Windows).

Это показывает, что Notepad является GUI-программой, a Cmd - консольной, или программой текстового режима. Хотя вывод утилиты Exetype сообщает о наличии двух разных подсистем для GUI- и консольных программ, на самом деле существует лишь одна подсистема Windows. Кроме того, Windows не поддерживает процессор Intel 386 (или 486, если это имеет какое-то значение) - текст сообщений, выводимых программой Exetype, просто не обновили.

При вызове приложением одной из функций DLL подсистемы возможно одно из трех.

(o) Функция полностью реализована в пользовательском режиме внутри DLL подсистемы. Иначе говоря, никаких сообщений процессу подсистемы окружения не посылается, и вызова сервисов исполнительной системы Windows не происходит. После выполнения функции в пользовательском режиме результат возвращается вызвавшей ее программе. Примерами таких функций могут служить GetCurrentProcess (всегда возвращает -1, значение, определенное для ссылки на текущий процесс во всех функциях, связанных с процессами) и GetCurrentProcessId (идентификатор процесса не меняется в течение его срока жизни, поэтому его можно получить из кэша, что позволяет избежать переключения в режим ядра).

(o) Функция требует одного или более вызовов исполнительной системы Windows. Например, Windows-функции ReadFile и WriteFile обращаются к внутренним недокументированным сервисам ввода-вывода - соответственно к NtReadFile и NtWriteFile.

(o) Функция требует выполнения каких-либо операций в процессе подсистемы окружения (такие процессы, работающие в пользовательском режиме, отвечают за обслуживание клиентских приложений, выполняемых под их контролем). B этом случае подсистеме окружения выдается клиент-серверный запрос через сообщение с требованием выполнить какую-либо операцию, и DLL подсистемы, прежде чем вернуть управление вызвавшей программе, ждет соответствующего ответа.

Некоторые функции вроде CreateProcess и CreateThread могут требовать выполнения как второго, так и третьего пункта.

Хотя структура Windows позволяет поддерживать несколько независимых подсистем окружения, с практической точки зрения было бы неудобно включать в состав каждой подсистемы свой код для обработки окон и отображения ввода-вывода. Это привело бы к дублированию системных функций и в конечном счете негативно отразилось бы на объеме и производительности системы. Поскольку главной подсистемой была Windows, разработчики решили разместить эти базовые функции именно в ней. Так что другие подсистемы для отображения ввода-вывода вызывают соответствующие сервисы Windows. (Кстати, посмотрев на тип подсистемы в заголовках их файлов, вы убедитесь, что фактически они являются исполняемыми файлами Windows.)

Теперь поближе познакомимся с каждой подсистемой окружения.

Подсистема Windows

Эта подсистема состоит из следующих основных элементов.

(o) Процесса подсистемы окружения (Csrss.exe), предоставляющего:

(o) поддержку консольных (текстовых) окон;

(o) поддержку создания и удаления процессов и потоков;

(o) частичную поддержку процессов 1б-разрядной виртуальной DOS-машины (VDM);

(o) множество других функций, например GetTempFile, DefineDosDevice, ExitWindowsEx, а также несколько функций поддержки естественных языков.

(o) Драйвера режима ядра (Win32k.sys), включающего:

(o) диспетчер окон, который управляет отрисовкой и выводом окон на экран, принимает ввод с клавиатуры, мыши и других устройств, а также передает пользовательские сообщения приложениям;

(o) Graphics Device Interface (GDI), который представляет собой библиотеку функций для устройств графического вывода. B GDI входят функции для манипуляций с графикой и отрисовки линий, текста и фигур.

(o) DLL-модулей подсистем (Kernel32.dll, Advapi32.dll, User32.dll и Gdi32.dll), транслирующих вызовы документированных функций Windows API в вызовы соответствующих (и в большинстве своем недокументированных) сервисов режима ядра из Ntoskrnl.exe и Win32k.sys.

(o) Драйверов графических устройств, представляющих собой специфичные для конкретного оборудования драйверы графического дисплея, принтера и минипорт-драйверы видеоплат.

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

GDI предоставляет набор стандартных функций двухмерной графики, которые позволяют приложениям, не имеющим представления о графических устройствах, обращаться к ним. GDI-функции играют роль посредника между приложениями и драйверами дисплея и принтера. GDI интерпретирует запросы приложений на вывод графики и посылает соответствующие запросы драйверам. Он также предоставляет приложениям стандартный унифицированный интерфейс для использования самых разнообразных устройств графического вывода. Этот интерфейс обеспечивает независимость кода приложений от конкретного оборудования и его драйверов. GDI выдает свои запросы с учетом возможностей конкретного устройства, часто разделяя запрос на несколько частей для обработки. Так, некоторые устройства сами умеют формировать эллипсы, а другие требуют от GDI интерпретировать эллипсы как набор пикселов с определенными координатами. Подробнее об архитектуре подсистемы вывода графики и драйвере дисплея см. раздел «Design Guide» в книге «Graphics Drivers» из Windows DDK.

До Windows NT 4 диспетчер окон и графические сервисы были частью процесса подсистемы Windows пользовательского режима. B Windows NT 4 основная часть кода, ответственного за обработку окон и графики, перенесена из контекста процесса подсистемы Windows в набор вызываемых сервисов, выполняемых в режиме ядра (в файл Win32k.sys). Этот перенос был осуществлен в основном для повышения общей производительности системы. Отдельный серверный процесс, содержащий графическую подсистему, требовал многочисленных переключений контекста потоков и процессов, что отнимало большое количество тактов процессора и значительные ресурсы памяти, даже несмотря на высокую оптимизацию исходной архитектуры этой подсистемы.

Например, каждый клиентский поток обслуживается парным серверным потоком в процессе подсистемы Windows, ожидающем запросов от клиентского потока. Для передачи сообщений между потоками используется специальный механизм взаимодействия между процессами, так называемый быстрый LPC (fast LPC). B отличие от обычного переключения контекста потоков передача данных между парными потоками через быстрый LPC не вызывает в ядре события перепланирования, что позволяет серверному потоку выполняться в течение оставшегося кванта времени клиентского потока (вне очереди, определенной планировщиком). Более того, для быстрой передачи больших структур данных, например битовых карт, используются разделяемые буферы памяти, и клиенты получают прямой доступ (только для чтения) к ключевым структурам данных сервера, а это сводит к минимуму необходимость в частом переключении контекста между клиентами и сервером Windows.

GDI-операции выполняются в пакетном режиме. При этом серия графических объектов, запрошенных Windows-приложениями, не обрабатывается сервером и не прорисовывается на устройстве вывода до тех пор, пока не будет заполнена вся очередь GDI. Размер очереди можно установить через Windows-функцию GdiSetBatchLimit. B любой момент все объекты из очереди можно сбросить вызовом функции GdiFlush. C другой стороны, неизменяемые свойства и структуры данных GDI после получения от процессов подсистемы Windows кэшируются клиентскими процессами для ускорения последующего доступа к ним.

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

компонентам исполнительной системы Windows без перехода из пользовательского режима в режим ядра и обратно. Прямой доступ особенно важен в случае вызова GDI через видеодрайверы, когда взаимодействие с видеооборудованием требует высокой пропускной способности.

Так что же остается в той части процесса подсистемы Windows, которая работает в пользовательском режиме? Поскольку консольные программы не перерисовывают окна, все операции по отрисовке и обновлению консольных и текстовых окон проводятся именно этой частью Windows. Увидеть ее деятельность несложно: просто откройте окно командной строки и перетащите поверх него другое окно. Вы увидите, что процесс подсистемы Windows начинает расходовать процессорное время, перерисовывая консольное окно. Кроме поддержки консольных окон, только небольшая часть Windows-функций посылает сообщения процессу подсистемы Windows. K ним относятся функции, отвечающие за создание и завершение процессов и потоков, назначение букв сетевым дискам, создание временных файлов. Как правило, Windows-приложение нечасто переключает (если вообще переключает) контекст в процесс подсистемы Windows.

He пострадала ли стабильность Windows от перевода USER и GDI в режим ядра?

Некоторые интересуются, не повлияет ли на стабильность системы перевод такой значительной части кода в режим ядра. Ho риск снижения стабильности системы минимален. Дело в том, что до Windows NT 4 (равно как и в настоящее время) ошибка вроде нарушения доступа (access violation) в процессе подсистемы Windows пользовательского режима (Csrss.exe) приводила к краху системы, потому что процесс подсистемы Windows был и остается жизненно важным для функционирования всей системы. Поскольку структуры данных, определяющие окна на экране, содержатся именно в этом процессе, его гибель приводит к уничтожению пользовательского интерфейса. Однако даже при функционировании Windows в качестве сервера без интерактивных процессов система не могла бы работать без Csrss, поскольку серверные процессы иногда используют оконные сообщения для контроля внутреннего состояния приложений. Так что в Windows ошибки вроде нарушения доступа в том же коде, только выполняемом в режиме ядра, просто быстрее приводят к краху - исключения в режиме ядра требуют прекращения работы системы.

Правда, теоретически появляется другая опасность. Поскольку этот код выполняется в режиме ядра, ошибка (например, применение неверного указателя) может повредить защищенные структуры данных режима ядра. До Windows NT 4 это могло привести к нарушению доступа, так как запись в страницы режима ядра из пользовательского режима не разрешается. Ho результатом стал бы крах системы. Теперь же при выполнении кода в режиме ядра запись на какую-либо страницу памяти по неверному указателю не обязательно вызовет немедленный крах системы. Ho, если при этом будут повреждены какие-то структуры данных, крах скорее всего произойдет. Тем не менее возникает риск, что из-за такого указателя будет повреждена не структура данных, а буфер памяти, и это приведет к возврату пользовательской программе или записи на диск неверных данных.

Существует еще одно негативное последствие перевода графических драйверов в режим ядра. Ранее некоторые части графического драйвера выполнялись в Csrss, а остальные части - в режиме ядра. Теперь весь драйвер работает только в режиме ядра. Так как не все драйверы поддерживаемых Windows графических устройств разрабатываются Microsoft, она тесно сотрудничает с производителями оборудования, чтобы гарантировать разработку ими надежных и эффективных драйверов. Все поставляемые с системой драйверы тестируются так же тщательно, как и другие компоненты исполнительной системы.

Наконец, важно понимать, что такая схема (при которой подсистема поддержки окон и графики выполняется в режиме ядра) не является принципиально рискованной. Идентичный подход используется для многих других драйверов устройств (например, сетевых карт и жестких дисков). Все эти драйверы, выполняемые в режиме ядра, никогда не снижали надежности Windows NT

Некоторые распространяют измышления насчет снижения эффективности вытесняющей многозадачности Windows из-за перевода диспетчера окон и GDI в режим ядра. Теория, которая стоит за этой точкой зрения, - увеличивается время, затрачиваемое на дополнительную обработку Windows в режиме ядра. Это мнение возникло в результате ошибочного понимания архитектуры Windows. Действительно, во многих других операционных системах, формально поддерживающих вытесняющую многозадачность, планировщик никогда не вытесняет потоки, выполняемые в режиме ядра, или вытесняет, но лишь в отдельных ситуациях. Однако в Windows любые потоки, выполняемые в режиме ядра, планируются и вытесняются так же, как и потоки пользовательского режима, - код исполнительной системы полностью реентерабелен. Помимо многих других соображений, это просто необходимо для достижения высокого уровня масштабируемости системы на оборудовании с поддержкой SMR.

Другое направление спекуляций касалось снижения масштабируемости SMP в результате уже описанных изменений. Теоретические обоснования были такими: раньше во взаимодействии между приложением и диспетчером окон или GDI участвовали два потока-, один - в приложении и один - в Csrss.exe. Поэтому в SMP-системах, где эти потоки могут выполняться параллельно, пропускная способность возрастает. Это свидетельствует о непонимании технологий, применявшихся до Windows NT 4. B большинстве случаев клиентские приложения вызывают процесс подсистемы Windows синхронно, т. е. клиентский поток полностью блокируется в ожидании обработки вызова серверным потоком и возобновляется только после этого. Так что никакой параллелизм в SMP-системах недостижим. Это явление легко наблюдать в SMP-системах на примере приложений, интенсивно работающих с графикой. При этом обнаружится, что в двухпроцессорной системе каждый процессор загружен на 50%; также легко заметить единственный поток Csrss, отделенный от потока приложения. Действительно, поскольку два потока тесно взаимодействуют и находятся в сходном состоянии, для поддержания синхронизации процессорам приходится постоянно сбрасывать кэш. Именно по этой причине однопоточные графические приложения в SMP-системах под управлением Windows NT 3.51 обычно выполняются медленнее, чем в однопроцессорных системах.

B результате изменений, внесенных в Windows NT 4, удалось повысить пропускную способность SMP-систем для приложений, интенсивно использующих диспетчер окон и GDI, - особенно когда в приложении работает более одного потока. При наличии двух потоков приложения на двухпроцессорной машине под управлением Windows NT 3.51 за процессорное время конкурируют в общей сложности четыре потока (два - в приложении и два - в Csrss). Хотя в каждый момент к выполнению готовы, как правило, лишь два потока, их рассогласованность ведет к потере локальности ссылок и синхронизации кэша. Это происходит скорее всего из-за переключения потоков приложения с одного процессора на другой. B Windows NT 4 каждый из двух потоков приложения по сути имеет собственный процессор, а механизм автоматической привязки потоков в Windows пытается постоянно выполнять данный поток на одном и том же процессоре, максимально увеличивая локальность ссылок и сводя к минимуму потребность в синхронизации кэш-памяти индивидуальных процессоров.

B заключение отметим, что повышение производительности в результате перевода диспетчера окон и GDI из пользовательского режима в режим ядра достигнуто без сколько-нибудь значимого снижения стабильности и надежности системы - даже в случае нескольких сеансов, созданных в конфигурации с поддержкой Terminal Services.

Подсистема POSIX

POSIX, название которой представляет собой аббревиатуру от «portable operating system interface based on UNIX» (переносимый интерфейс операционной системы на основе UNIX), - это совокупность международных стандартов на интерфейсы операционных систем типа UNIX. Стандарты POSIX стимулировали производителей поддерживать совместимость реализуемых ими UNIX-подобных интерфейсов, тем самым позволяя программистам легко переносить свои приложения между системами.

B Windows реализован лишь один из многих стандартов POSIX, а именно POSIX.l, который официально называется ISO/IEC 9945-1:1990, или IEEE POSIX стандарта 1003 1-1990. Этот стандарт изначально был включен в основном для соответствия требованиям правительства США, установленным во второй половине 80-х годов. B федеральном стандарте Federal Information Processing Standard (FIPS) 151-2, разработанном государственным институтом стандартов и технологий (NIST), содержится требование совместимости с POSIX l.Windows NT 3.5, 3.51 и 4 прошли тестирование на соответствие FIPS 151-2.

Поскольку совместимость с POSIX. 1 была одной из обязательных целей, в Windows включена необходимая базовая поддержка подсистемы POSIX1 - например, функция fork, реализованная в исполнительной системе Windows, и поддержка файловой системой Windows жестких файловых связей (hard file links). Однако POSIX.l определяетлишь ограниченный набор сервисов (управление процессами, взаимодействие между процессами, простой символьный ввод-вывод и т. д.), и поэтому подсистема POSIX в Windows не является полноценной средой программирования. Так как вызов функций из разных подсистем Windows невозможен, набор функций, доступный приложениям POSIX по умолчанию, строго ограничен сервисами, определяемыми POSIX1. Смысл этих ограничений в следующем: приложение POSIX не может создать поток или окно в Windows, а также использовать RPC или сокеты.

Для преодоления этого ограничения предназначен продукт Microsoft Windows Services for UNIX, включающий (в версии 3.5) улучшенную подсистему окружения POSIX, которая предоставляет около 2000 функций UNIX и 300 инструментов и утилит в стиле UNIX. (Детали см. на wivwmicrosoft.com/ windows/sfu/default.asp).

Эта улучшенная подсистема POSIX реально помогает переносить UNIX-приложения в Windows. Однако, поскольку эти программы все равно связаны с исполняемыми файлами POSIX, Windows-функции им недоступны. Чтобы UNIX-приложения, переносимые в Windows, могли использовать Windows-функции, нужно приобрести специальные пакеты для переноса UNIX-программ в Windows, подобные продуктам MKS Toolkit, разработанные компанией Mortice Kern Systems Inc. ( www.mkssoJtware.com). Тогда UNIX-приложения можно перекомпилировать и заново собрать как исполняемые файлы Windows и начать постепенный переход на «родные» Windows-функции.

ЭКСПЕРИМЕНТ: наблюдаем старт подсистемы POSIX

Подсистема POSIX по умолчанию сконфигурирована на запуск в момент начала выполнения приложения POSIX. Таким образом, старт подсистемы POSIX можно наблюдать, запустив какую-нибудь программу POSIX, например одну из утилит POSIX из Windows Services for UNIX (небольшой набор утилит вы найдете и в каталоге \Apps\POSIX на компакт-диске ресурсов Windows 2000; они не устанавливаются как часть ресурсов). Для запуска подсистемы POSIX следуйте инструкциям, приведенным ниже.

1. Откройте окно командной строки.

2. Запустите Process Explorer и убедитесь, что подсистема POSIX еще не запущена (т. е. процесса Psxss.exe в системе нет). Также убедитесь, что Process Explorer отображает список процессов как дерево (нажмите Ctrl+T).

3. Запустите POSIX-программу (например C Shell или Korn Shell, поставляемую с Windows Services for UNIX) или утилиту POSIX из ресурсов Windows 2000, например \Apps\POSIX\Ls.exe.

4. Вернитесь в Process Explorer и обратите внимание на новый процесс Psxss.exe, являющийся дочерним процессом Smss.exe (который в зависимости от выбранного интервала подсветки может какое-то время оставаться выделенным как новый процесс).

Для компиляции и сборки приложения POSIX в Windows нужны заголовочные файлы и библиотеки POSIX из Platform SDK. Исполняемые файлы POSIX связываются с библиотекой подсистемы POSIX, Psxdll.dll. Поскольку Windows по умолчанию сконфигурирована на запуск подсистемы POSIX только по требованию, при первом запуске приложения POSIX должен запуститься процесс подсистемы POSIX (Psxss.exe). Его выполнение продолжается до перезагрузки системы. (Если вы завершите процесс подсистемы POSIX, запуск приложений POSIX станет невозможен до следующей перезагрузки системы.) Приложение POSIX не выполняется самостоятельно; для него запускается специальный файл поддержки Posix.exe, создающий дочерний процесс, из которого и запускаются приложения POSIX.

Подсистема OS/2

Подсистема окружения OS/2, как и подсистема POSIX, обладает довольно ограниченной функциональностью и поддерживает лишь 1б-разрядные приложения OS/2 версии 1.2 с символьным или графическим вводом-выводом. Кроме того, Windows запрещает прикладным программам прямой доступ к оборудованию и поэтому не поддерживает приложения OS/2, использующие расширенный ввод-вывод видео или включающие сегменты привилегированного ввода-вывода, которые пытаются выполнять инструкции IN/OUT (для доступа к некоторым аппаратным устройствам). Приложения, выдающие машинные команды CLI/STI, могут работать в Windows, но на время выполнения команды STI все другие приложения OS/2 в системе и потоки процессов OS/2, выдающих команды CLI, приостанавливаются.

Как показано на рис. 2-5, подсистема OS/2, использующая 32-разрядное виртуальное адресное пространство Windows, может предоставить приложениям OS/2 версии 1.2 до 512 Мб памяти, снимая тем самым исходное ограничение этой версии на объем адресуемой памяти (до 16 Мб).

Мозаичная область (tiled area) - это 512 Мб заранее резервируемого виртуального адресного пространства, откуда передается и куда возвращается память, выделяемая под сегменты, которыми пользуются 1б-разрядные приложения. Для каждого процесса подсистема OS/2 ведет таблицу локальных дескрипторов (local descriptor table, LDT), в которой сегменты разделяемой памяти занимают один и тот же LDT-слот для всех процессов OS/2.

Как будет детально показано в главе 6, потоки являются элементами выполняемой программы и, как таковые, подлежат планированию (подключению к процессору по определенной схеме). B OS/2 всего 64 уровня приоритетов (от 0 до 63), а в Windows - 32 (от 0 до 31). Несмотря на это, 64 уровня приоритетов OS/2 проецируются на динамические приоритеты Windows с 1-го по 15-й. Потоки OS/2, выполняемые в Windows, никогда не получают приоритеты реального времени (16-31).

Как и подсистема POSIX, подсистема OS/2 автоматически запускается при первой активизации OS/2-совместимого образа и продолжает выполняться до перезагрузки всей системы.

Подробнее о выполнении приложений POSIX и OS/2 в Windows см. главу 6.

Ntdll.dll

Ntdll.dll - специальная библиотека системной поддержки, нужная в основном при использовании DLL подсистем. Она содержит функции двух типов:

(o) интерфейсы диспетчера системных сервисов (system service dispatch stubs) к сервисам исполнительной системы Windows;

(o) внутренние функции поддержки, используемые подсистемами, DLL подсистем и другими компонентами операционной системы. Первая группа функций предоставляет интерфейс к сервисам исполнительной системы Windows, которые можно вызывать из пользовательского режима. Таких функций более 200, например NtCreateFile, NtSetEvent и т. д. Как уже говорилось, большинство из них доступно через Windows API (однако некоторые из них предназначены только для применения внутри самой операционной системы).

Для каждой из этих функций в Ntdll существует точка входа с тем же именем. Код внутри функции содержит специфичную для конкретной аппаратной архитектуры команду перехода в режим ядра для вызова диспетчера системных сервисов (о нем рассказывается в главе 3), который после проверки некоторых параметров вызывает уже настоящий сервис режима ядра из Ntoskrnl.exe.

Ntdll также включает множество функций поддержки, например загрузчик образов (функции, имена которых начинаются с Ldr), диспетчер куч, функции для взаимодействия с процессом подсистемы Windows (функции, имена которых начинаются с Csr), а также универсальные процедуры библиотек периода выполнения (функции, имена которых начинаются с Rtl). Там же находится диспетчер APC (asynchronous procedure call) пользовательского режима и диспетчер исключений. (Подробнее об APC и исключениях см. главу 3.)

Исполнительная система

Исполнительная система (executive) находится на верхнем уровне Ntoskrnl.exe (ядро располагается на более низком уровне). B ее состав входят функции следующего типа.

(o) Экспортируемые функции, доступные для вызова из пользовательского режима. Эти функции называются системными сервисами и экспортируются через Ntdll. Большинство сервисов доступно через Windows API или API других подсистем окружения. Однако некоторые из них недоступны через документированные функции (примером могут служить LPC, функции запросов вроде NtQueryInformationProcess, специализированные функции типа NtCreatePagingFile и т. д.).

(o) Функции драйверов устройств, вызываемые через функцию DeviceIoCont-rol. Последняя является универсальным интерфейсом от пользовательского режима к режиму ядра для вызова функций в драйверах устройств, не связанных с чтением или записью.

(o) Экспортируемые функции, доступные для вызова только из режима ядра и документированные в Windows DDK или Windows Installable File System (IFS) Kit (см. wwwmicrosoft.com/whdc/ddk/ifskit.default.mspx).

(o) Экспортируемые функции, доступные для вызова только из режима ядра, но не описанные в Windows DDK или IFS Kit (например, функции, которые используются видеодрайвером, работающим на этапе загрузки, и чьи имена начинаются с Inbv).

(o) Функции, определенные как глобальные, но не экспортируемые символы. Включают внутренние функции поддержки, вызываемые в Ntoskrnl; их имена начинаются с Iop (функции поддержки диспетчера ввода-вывода) или с Mi (функции поддержки управления памятью).

(o) Внутренние функции в каком-либо модуле, не определенные как глобальные символы. Исполнительная система состоит из следующих основных компонентов (каждый из них подробно рассматривается в последующих главах книги).

(o) Диспетчер конфигурации (см. главу 4), отвечающий за реализацию и управление системным реестром.

(o) Диспетчер процессов и потоков (см. главу 6), создающий и завершающий процессы и потоки. Низкоуровневая поддержка процессов и потоков реализована в ядре Windows, а исполнительная система дополняет эти низкоуровневые объекты своей семантикой и функциями.

(o) Монитор состояния защиты (см. главу 8), реализующий политики безопасности на локальном компьютере. Он охраняет ресурсы операционной системы, осуществляя аудит и контролируя доступ к объектам в период выполнения.

(o) Диспетчер ввода-вывода (см. главу 9), реализующий аппаратно-независимый ввод-вывод и отвечающий за пересылку ввода-вывода нужным драйверам устройств для дальнейшей обработки.

(o) Диспетчер Plug and Play (см. главу 9), определяющий, какие драйверы нужны для поддержки конкретного устройства, и загружающий их. Требования каждого устройства в аппаратных ресурсах определяются в процессе перечисления. B зависимости от требований каждого устройства диспетчер PnP распределяет такие ресурсы, как порты ввода-вывода, IRQ, каналы DMA и области памяти. Он также отвечает за посылку соответствующих уведомлений об изменениях в аппаратном обеспечении системы (при добавлении или удалении устройств).

(o) Диспетчер электропитания (см. главу 9), который координирует события, связанные с электропитанием, и генерирует уведомления системы управления электропитанием, посылаемые драйверам. Когда система не занята, диспетчер можно настроить на остановку процессора для снижения энергопотребления. Изменение энергопотребления отдельных устройств возлагается на их драйверы, но координируется диспетчером электропитания.

(o) Подпрограммы WDM Windows Management Instrumentation (см. главу 4), позволяющие драйверам публиковать информацию о своих рабочих характеристиках и конфигурации, а также получать команды от службы

WMI пользовательского режима. Потребители информации WMI могут находиться как на локальной машине, так и на любом компьютере в сети.

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

(o) Диспетчер памяти (см. главу 7), реализующий виртуальную память - схему управления памятью, позволяющую выделять каждому процессу большое закрытое адресное пространство, объем которого может превышать доступную физическую память. Диспетчер памяти также обеспечивает низкоуровневую поддержку диспетчера кэша.

(o) Средство логической предвыборки (см. главу 7), ускоряющее запуск системы и процессов за счет оптимизации загрузки данных, к которым происходит обращение при запуске системы или процессов. Кроме того, в состав исполнительной системы входят четыре основные группы функций поддержки, используемые вышеперечисленными компонентами. Примерно треть из них описана в DDK, поскольку драйверы тоже используют их. Вот что представляют собой четыре категории функций поддержки.

(o) Диспетчер объектов - создает, управляет и удаляет объекты и абстрактные типы данных исполнительной системы, используемые для представления таких ресурсов операционной системы, как процессы, потоки и различные синхронизирующие объекты. Подробнее о диспетчере объектов см. главу 3-

(o) Механизм LPC (см. главу 3) - передает сообщения между клиентским и серверным процессами на одном компьютере. LPC является гибкой, оптимизированной версией RPC (remote procedure call), стандартного механизма взаимодействия между клиентскими и серверными процессами через сеть.

(o) Большой набор стандартных библиотечных функций для обработки строк, арифметических операций, преобразования типов данных и обработки структур безопасности.

(o) Подпрограммы поддержки исполнительной системы, например для выделения системной памяти (пулов подкачиваемых и неподкачиваемых страниц), доступа к памяти со взаимоблокировкой, а также два специальных типа синхронизирующих объектов: ресурс (resource) и быстродействующий мьютекс (fast mutex).

Ядро

Ядро состоит из набора функций в Ntoskrnl.exe, предоставляющих фундаментальные механизмы (в том числе планирования потоков и синхронизации), которые используются компонентами исполнительной системы и низкоуровневыми аппаратно-зависимыми средствами поддержки (диспетчеризации прерываний и исключений), различными в каждой процессорной архитектуре. Код ядра написан в основном на С, а ассемблер использовали лишь для решения специфических задач, трудно реализуемых на С.

Как и функции поддержки исполнительной системы, упоминавшиеся в предыдущем разделе, часть функций ядра описана в DDK (см. функции, чьи имена начинаются с Ke), поскольку они необходимы для реализации драйверов устройств.

Объекты ядра

Ядро состоит из низкоуровневых, четко определенных и хорошо предсказуемых примитивов и механизмов операционной системы, позволяющих компонентам исполнительной системы более высокого уровня выполнять свои функции. Ядро отделено от остальной части исполнительной системы; оно реализует системные механизмы и не участвует в принятии решений, связанных с системной политикой. Практически все такие решения, кроме планирования и диспетчеризации потоков, принимаются исполнительной системой.

Вне ядра исполнительная система представляет потоки и другие разделяемые ресурсы в виде объектов. Управление этими объектами требует определенных издержек, так как нужны описатели, позволяющие манипулировать объектами, средства защиты и квоты ресурсов, резервируемых при их создании. B ядре можно избежать таких издержек, поскольку оно реализует набор более простых объектов, называемых объектами ядра (kernel objects). Эти объекты позволяют ядру контролировать обработку данных процессором и поддерживают объекты исполнительной системы. Большинство объектов уровня исполнительной системы инкапсулирует один или более объектов ядра, включая в себя их атрибуты, определенные ядром.

Одна из групп объектов ядра, называемых управляющими (control objects), определяет семантику управления различными функциями операционной системы. B эту группу входят объекты APC, DPC (deferred procedure call) и несколько объектов, используемых диспетчером ввода-вывода (например, объект прерывания).

Другая группа объектов под названием объекты диспетчера (dispatcher objects) реализует средства синхронизации, позволяющие изменять планирование потоков. B группу таких объектов входят поток ядра (kernel thread), мьютекс (mutex), событие (event), семафор (semaphore), таймер (timer), ожидаемый таймер (waitable timer) и некоторые другие. C помощью функций ядра исполнительная система создает объекты ядра, манипулирует ими и конструирует более сложные объекты, предоставляемые в пользовательском режиме. Объекты подробно рассматриваются в главе 3, а процессы и потоки - в главе 6.

Поддержка оборудования

Другая важная задача ядра - абстрагирование или изоляция исполнительной системы и драйверов устройств от различий между аппаратными архитектурами, поддерживаемыми Windows (т. е. различий в обработке прерываний, диспетчеризации исключений и синхронизации между несколькими процессорами).

Архитектура ядра нацелена на максимальное обобщение кода - даже в случае аппаратно-зависимых функций. Ядро поддерживает набор семантически идентичных и переносимых между архитектурами интерфейсов. Большая часть кода, реализующего переносимые интерфейсы, также идентична для разных архитектур.

Ho одна часть этих интерфейсов по-разному реализуется на разных архитектурах, а другая включает код, специфичный для конкретной архитектуры. Архитектурно-независимые интерфейсы могут быть вызваны на любой машине, причем семантика интерфейса будет одинаковой - независимо от специфического кода для той или иной архитектуры. Некоторые интерфейсы ядра (например, процедуры спин-блокировки, описанные в главе 3) на самом деле реализуются в HAL (см. следующий раздел), поскольку их реализация может отличаться даже в пределах семейства процессоров с одинаковой архитектурой.

B ядре также содержится небольшая порция кода с х86-специфичными интерфейсами, необходимыми для поддержки старых программ MS-DOS. Эти интерфейсы не являются переносимыми в том смысле, что их нельзя вызывать на машине с другой архитектурой, где они попросту отсутствуют. Этот х86-специфичный код, например, поддерживает манипуляции с GDT (global descriptor tables) и LDT (local descriptor tables) - аппаратными средствами x86.

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

Еще один пример - переключение контекста. Хотя на высоком уровне для выбора потоков и переключения контекста применяется один и тот же алгоритм (сохраняется контекст предыдущего потока, загружается контекст нового и запускается новый поток), существуют архитектурные различия между его реализациями для разных процессоров. Поскольку контекст описывается состоянием процессора (его регистров и т. д.), сохраняемая и загружаемая информация зависит от архитектуры.

Уровень абстрагирования от оборудования

Как отмечалось в начале этой главы, одной из важнейших особенностей архитектуры Windows является переносимость между различными аппаратными платформами. Ключевой компонент, обеспечивающий такую переносимость, - уровень абстрагирования от оборудования (hardware abstraction layer, HAL). HAL - это загружаемый модуль режима ядра (Hal.dll), предоставляющий низкоуровневый интерфейс с аппаратной платформой, на которой выполняется Windows. Он скрывает от операционной системы специфику конкретной аппаратной платформы, в том числе ее интерфейсов ввода-вывода, контроллеров прерываний и механизмов взаимодействия между процессорами, т. е. все функции, зависимые от архитектуры и от конкретной машины.

Когда внутренним компонентам Windows и драйверам устройств нужна платформенно-зависимая информация, они обращаются не к самому оборудованию, а к подпрограммам HAL, что и обеспечивает переносимость этой операционной системы. По этой причине подпрограммы HAL документированы в Windows DDK, где вы найдете более подробные сведения о HAL и о его использовании драйверами.

Хотя в Windows имеется несколько модулей HAL (см. таблицу 2-6), при установке на жесткий диск компьютера копируется только один из них - Hal.dll. (B других операционных системах, например в VMS, нужный модуль HAL выбирается при загрузке системы.) Поскольку для поддержки разных процессоров требуются разные модули HAL, системный диск от одной х8б-установки скорее всего не подойдет для загрузки системы с другим процессором.

Таблица 2-6. Список модулей HAL для x86 в \ Windows\Driver\Cache\i386\Driver.cab

ПРИМЕЧАНИЕ B базовой системе Windows Server 2003 нет HAL, специфических для конкретных вендоров.

ЭКСПЕРИМЕНТ: просмотр базовых HAL, включенных в Windows

Для просмотра HAL, включенных в Windows, откройте файл Driver.cab в соответствующем подкаталоге, специфичном для конкретной архитектуры, в каталоге \Windows\Driver Cache. (Например, для систем x86 имя этого файла - \Windows\Driver Cache\i386\Driver.cab.) Прокрутите список до файлов, начинающихся с «Hal», и вы увидите файлы, перечисленные в таблице 2-6.

ЭКСПЕРИМЕНТ: определяем используемый модуль HAL

Определить, какой модуль HAL используется на вашей машине, можно двумя способами.

1. Откройте файл \Windows\Repair\Setup.log, найдите строку с Haldll. Имя файла, стоящее в этой строке после знака равенства, соответствует имени модуля HAL, извлеченного из Driver.cab с дистрибутивного носителя.

2. Откройте Device Manager (Диспетчер устройств): щелкните правой кнопкой мыши значок My Computer (Мой компьютер) на рабочем столе, выберите команду Properties (Свойства), откройте вкладку Hardware (Оборудование) и щелкните кнопку Device Manager (Диспетчер устройств). Проверьте имя «драйвера» для устройства Computer (Компьютер).

ЭКСПЕРИМЕНТ: просмотр зависимостей NTOSKRNL и HAL

Вы можете просмотреть взаимосвязи образов ядра и HAL, изучив их таблицы импорта и экспорта с помощью утилиты Dependency Walker (Depends.exe), которая содержится в Windows Support Tools и Platform SDK. Для исследования файла в Dependency Walker откройте его командой Open из меню File.

Вот пример вывода этой утилиты при просмотре зависимостей в Ntoskrnl.

Обратите внимание, что Ntoskrnl связан с HAL, который в свою очередь связан с Ntoskrnl (оба используют функции друг у друга). Ntoskrnl также связан с Bootvid.dll, видеодрайвером, используемым для вывода заставки при запуске Windows. B Windows XP и выше вы увидите в списке дополнительную DLL, Kdcom.dll. Она содержит код инфраструктуры отладчика ядра, который раньше был частью Ntoskrnl.exe.

Подробное описание информации, выводимой Dependency Walker, см. в справочном файле этой утилиты (Depends.hlp).

Драйверы устройств

Драйверы устройств подробно описываются в главе 9, а здесь мы даем краткий обзор их типов и поясняем, как перечислить установленные драйверы, загруженные в системе.

Драйверы устройств являются загружаемыми модулями режима ядра (как правило, это файлы с расширением .sys); они образуют интерфейс между диспетчером ввода-вывода и соответствующим оборудованием. Эти драйверы выполняются в режиме ядра в одном из трех контекстов:

(o) в контексте пользовательского потока, инициировавшего функцию ввода-вывода;

(o) в контексте системного потока режима ядра;

(o) как результат прерывания (а значит, не в контексте какого-либо процесса или потока, который был текущим на момент прерывания). Как было сказано в предыдущем разделе, в Windows драйверы устройств не управляют оборудованием напрямую - вместо этого они вызывают функции HAL. Драйверы, как правило, пишутся на C (иногда на C++), поэтому при правильном использовании процедур HAL они являются переносимыми между поддерживаемыми Windows архитектурами на уровне исходного кода, а на уровне двоичных файлов - внутри семейства с одинаковой архитектурой. Существует несколько типов драйверов устройств.

(o) Драйверы аппаратныхустройств, которые управляют (через HAL) оборудованием, записывают на них выводимые данные и получают вводимые данные от физического устройства или из сети. Есть множество типов таких драйверов - драйверы шин, интерфейсов, устройств массовой памяти и т. д.

(o) Драйверы файловой системы - это драйверы Windows, принимающие запросы на файловый ввод-вывод и транслирующие их в запросы ввода-вывода для конкретного устройства.

(o) Драйверы фильтра файловой системы, которые обеспечивают зеркали-рование и шифрование дисков, перехват ввода-вывода и некоторую дополнительную обработку информации перед передачей ее на следующий уровень.

(o) Сетевые редиректоры и серверы, являющиеся драйверами файловых систем, которые передают запросы файловой системы на ввод-вывод другим компьютерам в сети и принимают от них аналогичные запросы.

(o) Драйверы протоколов, реализующие сетевые протоколы вроде TCP/IP, NetBEUI и IPX/SPX.

(o) Драйверы потоковых фильтров ядра, действующие по цепочке для обработки потоковых данных, например при записи и воспроизведении аудио- и видеоинформации.

Поскольку установка драйвера устройства - единственный способ добавления в систему стороннего кода режима ядра, некоторые программисты пишут драйверы просто для того, чтобы получить доступ к внутренним функциям или структурам данных операционной системы, недоступным из пользовательского режима (но документированным и поддерживаемым в DDK). Например, многие утилиты с www.sysinternals.com представляют собой комбинацию GUI-приложений Windows с драйверами устройств, используемыми для сбора сведений о состоянии внутрисистемных структур и вызова функций, доступных только в режиме ядра.

Усовершенствования в модели драйверов Windows

B Windows 2000 была введена поддержка Plug and Play и энергосберегающих технологий, а также расширена модель драйверов Windows NT, называемая Windows Driver Model (WDM). Windows 2000 и более поздние версии могут работать с унаследованными драйверами Windows NT 4, но, поскольку они не поддерживают Plug and Play и энергосберегающие технологии, функциональность системы в этом случае будет ограничена. C точки зрения WDM, существует три типа драйверов.

(o) Драйвер шины (bus driver), обслуживающий контроллер шины, адаптер, мост или любые другие устройства, имеющие дочерние устройства. Драйверы шин нужны для работы системы и в общем случае поставляются Microsoft. Для каждого типа шины (PCI, PCMCIA и USB) в системе имеется свой драйвер. Сторонние разработчики создают драйверы для поддержки новых шин вроде VMEbus, Multibus и Futurebus.

(o) Функциональный драйвер (function driver) - основной драйвер устройства, предоставляющий его функциональный интерфейс. Обязателен, кроме тех случаев, когда устройство используется без драйверов (т. е. ввод-вывод осуществляется драйвером шины или драйверами фильтров шины, как в случае SCSI PassThru). Функциональный драйвер по определению обладает наиболее полной информацией о своем устройстве. Обычно только этот драйвер имеет доступ к специфическим регистрам устройства.

(o) Драйвер фильтра (filter driver), поддерживающий дополнительную функциональность устройства (или существующего драйвера) или изменяющий запросы на ввод-вывод и ответы на них от других драйверов (это часто используется для коррекции устройств, предоставляющих неверную информацию о своих требованиях к аппаратным ресурсам). Такие драйверы не обязательны, и их может быть несколько. Они могут работать как на более высоком уровне, чем функциональный драйвер или драйвер шины, так и на более низком. Обычно эти драйверы предоставляются OEM-производителями или независимыми поставщиками оборудования (IHV).

B среде WDM один драйвер не может контролировать все аспекты устройства: драйвер шины информирует диспетчер PnP об устройствах, подключенных к шине, в то время как функциональный драйвер управляет устройством.

B большинстве случаев драйвер фильтра более низкого уровня модифицирует поведение устройства. Например, если устройство сообщает драйверу своей шины о том, что ему нужно 4 порта ввода-вывода, тогда как на самом деле ему требуется 16, драйвер фильтра может перехватить список аппаратных ресурсов, направляемый драйвером шины диспетчеру PnP и исправить число портов.

Драйвер фильтра более высокого уровня обычно придает устройству дополнительную функциональность. Так, высокоуровневый драйвер фильтра для клавиатуры может обеспечивать дополнительную защиту.

Обработка прерываний описывается в главе 3. Подробнее о диспетчере ввода-вывода, WDM, Plug and Play и энергосберегающих технологиях см. главу 9.

ЭКСПЕРИМЕНТ: просмотр установленных драйверов устройств

Чтобы вывести список установленных драйверов, запустите оснастку Computer Management ^Управление компьютером). Для этого выберите из меню Start (Пуск) команду Programs (Программы), затем Administrative Tools (Администрирование) и Computer Management ^правление компьютером) или откройте Control Panel (Панель управления) и дважды щелкните значок Computer Management. B окне Computer Management раскройте System Information (Сведения о системе), затем Software Environment (Программная среда) и Drivers (Драйверы). Ниже приведен пример списка драйверов.

B этом окне выводится список драйверов, определенных в реестре, а также их тип и состояние - Running (Работает) или Stopped (Остановлена). Драйверы устройств и процессы Windows-сервисов определяются в разделе реестра HKLM\SYSTEM\CurrentControlSet\Services. Однако они отличаются по коду типа, например 1 соответствует драйверу режима ядра. (Полный список хранящихся в реестре сведений, которые относятся к драйверам, см. в таблице 4-7.)

Список загруженных в текущий момент драйверов можно просмотреть и с помощью утилиты Drivers (Drivers.exe в ресурсах Windows 2000) или Pstat (Pstat.exe в Windows XP Support Tools, Windows Server 2003 Support Tools, ресурсах Windows 2000 и Platform SDK). Ниже приведен листинг части выходной информации утилиты Drivers.

Утилита перечисляет все загруженные компоненты режима ядра (Ntoskrnl, HAL и драйверы устройств) и сообщает размеры разделов в каждом образе.

Pstat также выводит список загруженных драйверов, но только после списка процессов и потоков в каждом процессе. Она показывает один вид очень важной информации, не сообщаемой утилитой Drivers: адрес загрузки модуля в системном пространстве. Как вы еще увидите, этот адрес нужен для увязки выполняемых системных потоков с драйвером, в котором они существуют.

Недокументированные интерфейсы

Просмотр имен экспортируемых или глобальных символов в ключевых системных файлах (Ntoskrnl.exe, Hal.dll или Ntdll.dll) может оказаться полезным: вы получите представление о том, что умеет делать Windows в сравнении с документированной и поддерживаемой частью. Конечно, знание имен этих функций еще не означает, что вы сможете или должны их вызывать. Эти интерфейсы не документированы и могут быть изменены. Мы предлагаем рассмотреть эти функции только для лучшего понимания внутренних функций Windows, а не для обхода поддерживаемых интерфейсов.

Например, просмотрев список функций в Ntdll.dll, вы сможете сравнить список всех системных сервисов, которые Windows предоставляет DLL-модулям подсистем пользовательского режима, с их подмножеством, предоставляемым каждой подсистемой. Хотя многие из этих функций точно соответствуют документированным и поддерживаемым Windows-функциям, некоторые из них недоступны через Windows API (см. статью «Inside the Native API» на wwwsysinternals.com).

C другой стороны, было бы интересно выяснить, что импортируют DLL-модули подсистемы Windows (скажем, Kernel32.dll или Adva-pi32.dll) и какие функции они вызывают в Ntdll.

Также представляет интерес содержимое Ntoskrnl.exe: хотя в Windows DDK документированы многие экспортируемые подпрограммы, используемые драйверами режима ядра, немалая их часть не описана. Возможно, вас заинтересует содержимое таблицы импорта для Ntoskrnl и HAL, где перечислены функции HAL, используемые Ntoskrnl, и наоборот.

B таблице 2-7 приведено большинство общеупотребительных префиксов имен функций в компонентах исполнительной системы. B каждом из таких компонентов также используются слегка модифицированные префиксы, обозначающие внутренние функции: либо за первой буквой префикса указывается / (от internal), либо префикс заканчивается на букву p (от private). Так, Ki относится к внутренним функциям ядра, a Psp - к внутренним функциям поддержки процессов.

Понимая схему именования системных процедур Windows, расшифровывать имена экспортируемых функций гораздо легче. Общий формат выглядит так:

‹префикс›‹операция›‹объект›

где префикс - внутренний компонент, экспортирующий процедуру, операция - название операции, выполняемой над объектом или ресурсом, а объект - объект, над которым проводится эта операция.

Например, ExAllocatePoolWithTag является процедурой поддержки исполнительной системы, которая выделяет память из пула подкачиваемых или неподкачиваемых страниц. KeInitializeThread представляет собой процедуру для создания и инициализации объекта ядра «поток».

Системные процессы

B каждой системе Windows выполняются перечисленные ниже процессы. (Два из них, IdIe и System, не являются процессами в строгом смысле этого слова, поскольку они не выполняют какой-либо код пользовательского режима.)

(o) Процесс IdIe (включает по одному потоку на процессор для учета времени простоя процессора).

(o) Процесс System (содержит большинство системных потоков режима ядра).

(o) Диспетчер сеансов (Smss.exe).

(o) Подсистема Windows (Csrss.exe).

(o) Процесс входа в систему (Winlogon.exe).

(o) Диспетчер управления сервисами (Services.exe) и создаваемые им дочерние процессы сервисов (например, универсальный процесс для хостинга сервисов, Svchost.exe).

(o) Серверный процесс локальной аутентификации (Lsass.exe).

Чтобы понять взаимоотношения этих процессов, полезно просмотреть «дерево» процессов, отражающее связи между родительскими и дочерними процессами. Увидев, кем создается тот или иной процесс, вам будет легче понять, откуда берется каждый процесс. Ha рис. 2-6 показана часть экранного снимка дерева процессов с комментариями по нескольким первым процессам. (Process Explorer позволяет добавлять комментарии для индивидуальных процессов и выводить их как дополнительную колонку в окне.)

B следующих разделах поясняются основные системные процессы, перечисленные на рис. 2-6. Хотя в этих разделах дается краткое описание последовательности запуска данных процессов, подробно все этапы загрузки Windows рассматриваются в главе 5.

Процесс IdIe

Первый процесс, показанный на рис. 2-6, является процессом простоя системы (system idle process). Как будет показано в главе 6, процессы идентифицируются по именам их образов. Однако этот процесс (как и процесс System) не выполняет реальный код пользовательского режима (в том смысле, что в каталоге \Windows нет «System IdIe Process.exe»). Кроме того, разные утилиты из-за особенностей реализации по-разному именуют его. B таблице 2-8 приводится несколько имен процесса IdIe (с идентификатором 0); подробнее о нем рассказывается в главе 6.

A теперь рассмотрим системные потоки и предназначение каждого системного процесса, выполняющего реальный код.

Прерывания и DPC

Две строки, помеченные как Interrupts и DPCs, отражают время, затраченное на обслуживание прерываний и обработку отложенных вызовов процедур (deferred procedure calls, DPC). Эти механизмы объясняются в главе 3. Заметьте: хотя Process Explorer показывает эти строки в списке процессов, они не имеют отношения к процессам. Они выводятся потому, что ведут учет процессорного времени, не выделенного какому-либо процессу. Ho Task Manager (Диспетчер задач) рассматривает время, затраченное на обработку преры-

ваний и DPC, как время простоя системы. Поэтому система, занятая интенсивной обработкой прерываний, будет выглядеть в Task Manager так, будто она ничем не занимается.

Процесс System и его потоки

Процесс System (с идентификатором 8 в Windows 2000 и идентификатором 4 в Windows XP и Windows Server 2003) служит носителем особых потоков, работающих только в режиме ядра, - системных потоков режима ядра (kernel-mode system threads). У системных потоков имеются все атрибуты и контексты обычных потоков пользовательского режима (например, контекст оборудования, приоритет и т. д.), но они отличаются тем, что выполняются только в режиме ядра внутри системного кода, загруженного в системное пространство, - будь то Ntoskrnl.exe или какой-либо драйвер устройства. Кроме того, у системных потоков нет адресного пространства пользовательского процесса, и поэтому нужная им динамическая память выделяется из куч памяти операционной системы, например из пула подкачиваемых или неподкачиваемых страниц.

Системные потоки создаются функцией PsCreateSystemThread (документирована в DDK), вызываемой только в режиме ядра. Windows, как и драйверы устройств, создает системные потоки при инициализации системы для выполнения действий, требующих получения контекста потока, например для выдачи и ожидания запросов на ввод-вывод или опроса устройства. Скажем, диспетчер памяти использует системные потоки для реализации таких функций, как запись измененных страниц в страничный файл (page file) или в спроецированные файлы, загрузки процессов в память или выгрузки из нее и т. д. Ядро создает системный поток под названием «диспетчер настройки баланса» (balance set manager), активизируемый раз в секунду для инициации при необходимости различных событий, связанных с планированием и управлением памятью. Диспетчер кэша также использует системные потоки для реализации как опережающего чтения, так и отложенной записи. Драйвер файл-сервера (Srv.sys) с помощью системных потоков отвечает на сетевые запросы ввода-вывода применительно к файлам на общих дисковых разделах, доступных в сети. Даже драйвер дисковода гибких дисков создает свой системный поток для опроса этого устройства (это повышает эффективность опроса, потому что драйвер дисковода гибких дисков, управляемый прерываниями, расходует много системных ресурсов). Подробнее о конкретных системных потоках см. главы, где рассматриваются соответствующие компоненты.

По умолчанию владельцем системных потоков является процесс System, но драйверы могут создавать системные потоки в любом процессе. Например, драйвер подсистемы Windows (Win32k.sys) создает системные потоки в процессе подсистемы Windows (Csrss.exe), чтобы облегчить доступ к данным в адресном пространстве этого процесса в пользовательском режиме.

Если вы занимаетесь поиском причин неполадок или системным анализом, полезно сопоставить выполнение индивидуальных системных потоков с создавшими их драйверами или даже с подпрограммой, содержащей соответствующий код. Так, на сильно загруженном файл-сервере процесс System скорее всего потребляет значительную часть процессорного времени. Ho для определения того, какой именно драйвер или компонент операционной системы выполняется, просто знать, что процесс System в данный момент выполняет «какой-то системный поток», недостаточно.

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

Оба этих метода демонстрируются следующими экспериментами.

ЭКСПЕРИМЕНТ: идентификация системных потоков в процессе System

Вы можете убедиться, что потоки внутри процесса System должны быть потоками режима ядра, поскольку стартовый адрес каждого из них больше адреса начала системного пространства (которое по умолчанию начинается с 0x80000000, если система загружена без параметра /3GB в Boot.ini). Кроме того, обратив внимание на процессорное время, выделяемое этим потокам, вы увидите, что они занимают процессорное время только при выполнении в режиме ядра. Чтобы определить драйвер, создавший системный поток, найдите стартовый адрес потока (с помощью Pviewer.exe) и ищите драйвер с базовым адресом, ближайшим (с меньшей стороны) к этому стартовому адресу. Утилита Pstat в конце своих выходных данных, как и команда !drivers отладчика ядра, сообщает базовые адреса каждого загруженного драйвера устройства.

Чтобы быстро найти текущий адрес потока, воспользуйтесь командой !stacks 0 отладчика ядра. Ниже приводится образец вывода, полученный на работающей системе с помощью LiveKd.

B первом столбце выводятся идентификаторы процесса и потока (в виде «Идентификатор процесса. Идентификатор потока»). Bo втором сообщается текущий адрес потока, в третьем - состояние потока: ожидает, готов или выполняется (о состояниях потоков см. главу 6). B последнем столбце показывается адрес вершины стека потока. Эта информация помогает определить драйвер, в котором началось выполнение того или иного потока. Имя функции потока в Ntoskrnl дает дополнительную подсказку о том, что именно делает поток.

Однако, если выполняемый поток является одним из рабочих потоков системы (ExpWorkerThread), вы не сможете точно сказать, что он делает, поскольку любой драйвер может давать задания рабочему потоку системы. B этом случае единственный выход - поставить точку прерывания в ExQueueWorkItem. По достижении этой точки введите !dso work_queue_item esp+4. Эта команда даст вам первый аргумент функции ExQueueWorkItem, представляющий собой указатель на структуру рабочего элемента, которая в свою очередь содержит адрес процедуры рабочего потока, вызываемой в его контексте. B качестве альтернативы можно использовать команду k отладчика ядра, которая сообщает текущее содержимое стека вызовов. A это подскажет, какой драйвер отправил задание рабочему потоку.

ЭКСПЕРИМЕНТ: увязка системного потока с драйвером устройства

B этом эксперименте мы посмотрим, как увязать активность процессора в процессе System с системным потоком (и драйвером, к которому он относится), вызывающим эту активность. Это важно: чтобы по-настоящему понять, что происходит, нужно перейти на уровень потоков процесса System. B данном случае мы вызовем активность системного потока, создав нагрузку файлового сервера на компьютере. (Драйвер файл-сервера Srv.sys создает системные потоки для обработки входящих запросов на файловый ввод-вывод. Подробнее об этом компоненте см. главу 13.)

1. Откройте окно командной строки.

2. Создайте список всех каталогов на диске С, используя сетевой путь для доступа к этому диску. Например, если имя вашего компьютера - COMPUTERl, введите dir \\computerl\c$ /s. (Ключ/s заставляет перечислять все подкаталоги.)

3. Запустите Process Explorer и дважды щелкните процесс System.

4. Откройте вкладку Threads.

5. Отсортируйте список по столбцу CSwitch Delta (разница по числу переключений контекста). Вы должны увидеть один или более потоков в Srv.sys, как показано на следующей иллюстрации.

Если вы видите работающий системный поток и не уверены, какой это драйвер, нажмите кнопку Module, которая открывает окно свойств для файла. Например, нажатие кнопки Module при выбранном, как на предыдущей иллюстрации, потоке в Srv.sys выводит результаты в следующем окне.

Диспетчер сеансов (Smss)

Диспетчер сеансов (Session Manager) (\Windows\System32\Smss.exe) является первым процессом пользовательского режима, создаваемым в системе. Он порождается системным потоком режима ядра, отвечающим за последний этап инициализации исполнительной системы и ядра.

Диспетчер сеансов отвечает за некоторые важные этапы запуска Windows, такие как создание дополнительных страничных файлов, выполнение отложенных операций по копированию, переименованию и удалению файлов, а также создание системных переменных окружения. Он также запускает процессы подсистем (обычно только Csrss.exe) и Winlogon, который в свою очередь создает остальные системные процессы.

Значительная часть сведений о конфигурации, хранящихся в реестре и используемых при инициализации Smss, находится в HKLM\SYSTEM\Current-ControlSet\Control\Session Manager. Некоторые из этих данных поясняются в главе 5 в разделе по Smss. (Более подробное описание разделов и параметров см. в справочном файле Regentry.chm из ресурсов Windows 2000).

После выполнения этих этапов инициализации главный поток Smss переходит к бесконечному ожиданию описателей процессов Csrss и Winlogon. Так как от них зависит функционирование Windows, при неожиданном завершении любого из них Smss вызывает крах системы (с кодом STATUSSYSTEM_ PROCESS_TERMINATED, или 0xC000021A). Smss также ожидает запросы на загрузку, события отладки и запросы на запуск новых сеансов сервера терминала. (Описание служб терминала см. в главе 1.)

Сеанс Terminal Services создается Smss. Когда Smss получает запрос на создание сеанса, он сначала вызывает NtSetSystemInformation с запросом на настройку сеансовых структур данных режима ядра. Это приводит к вызову внутренней функции диспетчера памяти MmSessionCreate, настраивающей виртуальное адресное пространство сеанса, которое будет содержать пул подкачиваемой памяти для сеанса и сеансовые структуры данных, создаваемые подсистемой Windows (а точнее, ее частью, работающей в режиме ядра), а также другими драйверами устройств. (Детали см. в главе 7). Затем Smss создает экземпляр Winlogon и Csrss для данного сеанса.

Winlogon, LSASS и Userinit

Процесс входа BWindows (\Windows\System32\Winlogon.exe) обрабатывает интерактивный вход пользователя в систему и выход из нее. При нажатии комбинации клавиш SAS (secure attention sequence) Winlogon получает уведомление о запросе пользователя на вход в систему. По умолчанию SAS в Windows представляет собой комбинацию клавиш Ctrl+Alt+Del. Назначение SAS - защита пользователя от программ перехвата паролей, имитирующих процесс входа в систему, так как эту комбинацию клавиш нельзя перехватить в приложении пользовательского режима.

Идентификация и аутентификация при входе в систему реализованы в заменяемой DLL под названием GINA (Graphical Identification and Authentication). Стандартная GINA Windows, Msgina.dll, реализует интерфейс для входа в систему по умолчанию. Однако разработчики могут включать свои GINA DLL, реализующие другие механизмы аутентификации и идентификации вместо стандартного метода Windows на основе проверки имени и пароля пользователя - например, на основе распознавания образцов голоса. Кроме того, Winlogon может загружать дополнительные DLL компонентов сетевого доступа для дальнейшей аутентификации. Эта функция позволяет нескольким компонентам доступа к сетям единовременно собирать все необходимые регистрационные данные в процессе обычного входа в систему.

После ввода имя и пароль пользователя посылаются для проверки серверному процессу локальной аутентификации (local security authentication server process, LSASS) (\Windows\System32\Lsass.exe, описываемому в главе 8). LSASS вызывает соответствующую функциональность (реализованную в виде DLL) для проверки соответствия введенного пароля с тем, что хранится в активном каталоге или SAM (части реестра, содержащей определения пользователей и групп).

После успешной аутентификации LSASS вызывает какую-либо функцию в мониторе состояния защиты (например, NtCreateToken), чтобы сгенерировать объект «маркер доступа» (access token object), содержащий профиль безопасности пользователя. Впоследствии Winlogon использует его для создания начального процесса оболочки. Информация о начальном процессе (или процессах) хранится в параметре Userinit в разделе реестра HKLM\ SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon. (По умолчанию начальным процессом считается Userinit.exe, но в списке может быть более одного образа.)

Userinit выполняет некоторые действия по инициализации пользовательской среды (например, запускает сценарии регистрации и активизирует групповые политики), а затем ищет в реестре параметр Shell (в указанном выше разделе Winlogon) и создает процесс для запуска определенной системной оболочки (по умолчанию - Explorer.exe). После этого процесс Userinit завершается. Вот почему для Explorer.exe не показывается родительский процесс - он уже завершился. Иначе говоря, Explorer является «внучатым» процессом Winlogon. (Имена процессов, чьи родительские процессы уже завершены, в списке Tlist выравниваются по левому краю.)

Winlogon активен не только при входе и выходе пользователя, но и при перехвате ввода SAS с клавиатуры. Например, когда вы нажимаете Ctrl+Alt+ DeI после входа в систему, Winlogon открывает диалоговое окно Windows Security (Безопасность Windows), предлагающее на выбор выход из системы, запуск Task Manager, блокировку рабочей станции, завершение работы системы и т. д.

Полное описание этапов процесса входа см. в разделе «Smss, Csrss и Winlogon» главы 5. Подробнее об аутентификации см. главу 8. Подробнее о вызываемых функциях интерфейса LSASS (чьи имена начинаются с Lsd) см. документацию Platform SDK.

Диспетчер управления сервисами (SCM)

Вспомните, что термин «сервисы» в Windows обозначает как серверные процессы, так и драйверы устройств. B этом разделе обсуждаются сервисы, являющиеся процессами пользовательского режима. Они похожи на демоны UNIX или обособленные процессы VMS в том смысле, что могут быть сконфигурированы на автоматический запуск при загрузке системы, не требуя интерактивного входа. Их также можно запустить вручную, например, с помощью оснастки Services (Службы) или вызовом Windows-функции StartSer-vice. Как правило, сервисы не взаимодействуют с вошедшим в систему пользователем, хотя при особых условиях это возможно (см. главу 4).

Этими сервисами управляет специальный системный процесс, диспетчер управления сервисами (service control manager) (\Windows\System32\Servi-ces.exe), отвечающий за запуск, остановку процессов сервисов и взаимодействие с ними. Сервисы представляют собой просто Windows-образы исполняемых программ, вызывающие особые Windows-функции для взаимодействия с диспетчером управления сервисами и с его помощью выполняющие такие операции, как регистрация успешного запуска сервиса, ответы на запросы о состоянии, приостановку или завершение работы сервиса. Сервисы определяются в разделе реестра HKLM\SYSTEM\CurrentControlSet\Services. Сведения о подразделах и параметрах, относящихся к сервисам, см. в справочном файле Regentry.chm в ресурсах Windows.

Учтите, что у сервисов есть три имени: имя процесса, выполняемого в системе, внутреннее имя в реестре и так называемое отображаемое имя (display name), которое можно увидеть в оснастке Services (Службы). (He у каждого сервиса есть отображаемое имя, и в случае его отсутствия используется внутреннее имя.) Сервисы Windows также содержат поле описания, где находится более подробная информация о том, что делает конкретный сервис.

Чтобы выяснить, какие именно сервисы содержатся в том или ином процессе, введите команду tlist /s. Ho заметьте, что иногда один процесс совместно используется несколькими сервисами. Код типа в реестре позволяет узнать, какие сервисы имеют собственные процессы и какие из них делят процессы с другими сервисами данного образа файла.

B виде сервисов реализуются некоторые компоненты Windows, например диспетчер очереди печати (спулер), журнал системных событий, планировщик задач, а также ряд сетевых компонентов.

ЭКСПЕРИМЕНТ: вывод списка установленных сервисов

Чтобы вывести список установленных сервисов (служб), дважды щелкните значок Administrative Tools (Администрирование) в окне Control Panel (Панель управления) и выберите Services (Службы). Вы должны увидеть что-нибудь в таком роде:

Для просмотра детальных сведений о сервисе щелкните правой кнопкой мыши имя сервиса и выберите команду Properties (Свойства). Ниже показан пример окна свойств для службы Print Spooler (Диспетчер очереди печати).

Обратите внимание, что поле Path To Executable (Исполняемый файл) указывает на программу, включающую данный сервис. Помните, что некоторые сервисы разделяют процессы с другими сервисами, поэтому число сервисов и используемых ими процессов не всегда находится в соотношении «один к одному».

ЭКСПЕРИМЕНТ: просмотр сервисов внутри сервисных процессов

Process Explorer выделяет процессы, которые являются хостами одного и более сервисов. (Для настройки поведения Process Explorer выберите Configure Highlighting в меню Options.) Дважды щелкнув процесс - хост сервисов, вы откроете вкладку Services, где перечисляются сервисы внутри этого процесса. При этом по каждому сервису выводится имя раздела реестра, где определен данный сервис, отображаемое имя, видимое администратору, и текст описания для этого сервиса (если такой текст есть). Например, в Windows XP список сервисов в процессе Svchost.exe, выполняемом под учетной записью System, выглядит следующим образом.

Подробнее о сервисах рассказывается в главе 4.

Резюме

B этой главе мы познакомились с общими аспектами системной архитектуры Windows. Мы также рассмотрели ключевые компоненты Windows и принципы их взаимодействия. B следующей главе будет подробнее рассказано о базовых системных механизмах, на которые опираются эти компоненты, в том числе о синхронизации и диспетчере объектов.

Г Л A B A 3 Системные механизмы

Г Л A B A 3 Системные механизмы

B Microsoft Windows существует несколько базовых механизмов, которыми пользуются компоненты режима ядра: исполнительная система (executive), ядро и драйверы устройств. B этой главе описываются следующие системные механизмы (а также способы их использования):

(o) диспетчеризация ловушек (trap dispatching), в том числе прерываний, DPC (deferred procedure call), APC (asynchronous procedure call), исключений и системных сервисов;

(o) диспетчер объектов исполнительной системы;

(o) синхронизация, в том числе спин-блокировки, объекты диспетчера ядра (kernel dispatcher objects) и реализация механизмов ожидания;

(o) системные рабочие потоки;

(o) различные механизмы вроде поддержки глобальных флагов Windows;

(o) LPC (local procedure call);

(o) Kernel Event Tracing;

(o) Wow64.

Диспетчеризация ловушек

Прерывания и исключения - такие ситуации в операционной системе, в которых нормальный поток выполнения кода процессором прерывается. Эти ситуации обнаруживаются как программным, так и аппаратным обеспечением. Термин ловушка (trap) относится к механизму, благодаря которому при прерывании или исключении процессор перехватывает контроль над выполняемым потоком и передает управление определенной части операционной системы. B Windows процессор передает управление обработчику ловушек (trap handler) - функции, специфичной для конкретного прерывания или исключения. Рис. 3-1 иллюстрирует некоторые ситуации, в которых активизируются обработчики ловушек.

Ядро различает прерывания и исключения: прерывание (interrupt) является асинхронным событием (т. е. оно может произойти в любой момент независимо от текущих команд, выполняемых процессором). Прерывания в основном генерируются устройствами ввода-вывода и таймерами. Их можно включать и отключать. Исключение (exception), напротив, представляет собой синхронное событие, являющееся результатом выполнения конкретной команды. Повторный запуск программы в аналогичных условиях с теми же данными позволит воспроизвести исключение. Примерами исключений могут служить нарушения доступа (ошибки защиты памяти), выполнение некоторых команд отладчика, а также попытки деления на нуль. Ядро также считает исключениями вызовы системных сервисов (хотя с точки зрения технической реализации это системные ловушки).

Прерывания и исключения можно генерировать как программно, так и аппаратно. Например, исключение «bus error» (ошибка шины) возникает из-за аппаратной ошибки, а причиной исключения «divide-by-zero» (деление на нуль) является ошибка в программе. Аналогичным образом прерывания могут генерироваться устройствами ввода-вывода или самим ядром (такие программные прерывания, как, например, APC или DPC).

При аппаратном прерывании или исключении процессор записывает статусную информацию в стек ядра для прерванного потока, чтобы впоследствии можно было вернуться к исходной точке в потоке управления и продолжить выполнение команд так, будто ничего не произошло. Если поток выполнялся в пользовательском режиме, Windows переключается на стек режима ядра для потока. Затем создает в стеке ядра прерванного потока фрейм ловушки (trap frame), в котором сохраняет информацию о состоянии потока. Фрейм ловушки является подмножеством полного контекста потока (см. главу 6), и вы можете просмотреть его определение, введя в отладчике ядра команду dt nt!_ktrap_frame. Программное прерывание ядро обслуживает либо при обработке аппаратного прерывания, либо синхронно - при вызове потоком функции ядра, относящейся к данному программному прерыванию.

B большинстве случаев ядро устанавливает функции, выполняющие общую обработку ловушек до и после передачи управления другим функциям, которые ставят ловушки. Например, когда устройство генерирует прерывание, обработчик ловушек аппаратных прерываний (принадлежащий ядру) передает управление процедуре обслуживания прерывания (interrupt service routine, ISR), предоставленной драйвером соответствующего устройства. Если прерывание возникло в результате вызова системного сервиса, обработчик ловушек общесистемных сервисов передает управление функции указанного системного сервиса в исполнительной системе. Ядро также устанавливает обработчики для ловушек, которые оно не ожидает или не обрабатывает. Эти обработчики, как правило, выполняют системную функцию KeBugCheckEx. Она останавливает компьютер, если ядро обнаруживает в работе системы отклонения, способные привести к повреждению данных (подробнее об этом см. главу 14). Диспетчеризация прерываний, исключений и системных сервисов детальнее описывается в следующих разделах.

Диспетчеризация прерываний

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

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

Для обработки аппаратных прерываний ядро устанавливает обработчики ловушек прерываний, которые передают управление внешней процедуре (ISR), обрабатывающей прерывание, или внутренней процедуре ядра, реагирующей на прерывание. Драйверы устройств предоставляют ISR для обслуживания прерываний от своих устройств, а ядро - внутренние процедуры для обработки других типов прерываний.

Далее мы рассмотрим, как процессор уведомляется об аппаратных прерываниях, какие типы прерываний поддерживаются ядром и как драйверы устройств взаимодействуют с ядром (в процессе обработки прерываний). Кроме того, мы поговорим о распознавании ядром программных прерываний и об объектах, используемых для реализации таких прерываний.

Обработка аппаратных прерываний

Ha аппаратных платформах, поддерживаемых Windows, прерывания, связанные с внешним вводом-выводом, поступают по одной из линий контроллера прерываний. Контроллер в свою очередь связан с процессором единственной линией, по которой и уведомляет о прерывании. Как только процессор прерывается, он требует от контроллера запрос прерывания (interrupt request, IRQ). Контроллер транслирует IRQ в номер прерывания, используемый как индекс в структуре, называемой таблицей диспетчеризации прерываний (interrupt dispatch table, IDT), и передает управление соответствующей процедуре. При загрузке Windows заносит в IDT указатели на процедуры ядра, обрабатывающие каждое прерывание и исключение.

ЭКСПЕРИМЕНТ: просмотр IDT

Просмотреть содержимое IDT, включая сведения об обработчиках ловушек, которые Windows назначила прерываниям, можно с помощью команды !idt отладчика ядра. Команда !idt без флагов показывает векторы, которые сопоставлены с адресами в модулях, отличных от Ntoskrnl.exe.

Ниже показано, что выводит команда !idt.

B системе, задействованной в этом эксперименте, номер прерывания 0x3C - с ISR драйвера клавиатуры (I8042prt.sys), а прерывание 0x3B совместно используется несколькими устройствами, в том числе видеоадаптером, шиной PCMCIA, портами USB и IEEE 1394, а также сетевым адаптером.

Windows увязывает аппаратные IRQ с номерами прерываний в IDT. Эта таблица используется системой и при конфигурировании обработчиков ловушек для исключений. Так, номер исключения для ошибки страницы на x86 и x64 (это исключение возникает, когда поток пытается получить доступ к отсутствующей или не определенной в виртуальной памяти странице) равен 0xe. Следовательно, запись 0xe в IDT указывает на системный обработчик ошибок страниц. Хотя архитектуры, поддерживаемые Windows, допус-каютдо 256 элементов в IDT, число IRQ на конкретной машине определяется архитектурой используемого в ней контроллера прерываний.

У каждого процессора имеется своя IDT, так что разные процессоры могут при необходимости выполнять разные ISR. Например, в многопроцессорной системе каждый процессор получает прерывания системного таймера, но обновление значения системного таймера в результате обработки этого прерывания осуществляется только одним процессором. Однако все процессоры используют это прерывание для измерения кванта времени, выделенного потоку, и для инициации новой процедуры планирования по истечении этого кванта. Аналогичным образом в некоторых конфигурациях может понадобиться, чтобы определенные аппаратные прерывания обрабатывал конкретный процессор.

Контроллеры прерываний на платформе x86

B большинстве систем x86 применяется либо программируемый контроллер прерываний (Programmable Interrupt Controller, PIC) i8259A, либо его разновидность, усовершенствованный программируемый контроллер прерываний (Advanced Programmable Interrupt Controller, APIC) i82489. Новые компьютеры, как правило, оснащаются APIC Стандарт PIC был разработан для оригинальных IBM PC PIC работает только в однопроцессорных системах и имеет 15 линий прерываний. APIC и SAPIC (о нем чуть позже) способен работать в многопроцессорных системах и предлагает 256 линий прерываний. Intel совместно с другими компаниями создали спецификацию Multiprocessor (MP) Specification, стандарт для многопроцессорных систем x86, основанный на использовании APIC Для совместимости с однопроцессорными операционными системами и загрузочным кодом, запускающим многопроцессорную систему в однопроцессорном режиме, APIC поддерживает PIC-совместимый режим с 15 линиями прерываний и передачей прерываний лишь главному процессору. Архитектура APIC показана на рис. 3-2. Ha самом деле APIC состоит из нескольких компонентов: APIC ввода-вывода, принимающего прерывания от устройств, локальных APIC, принимающих прерывания от APIC ввода-вывода по выделенной шине и прерывающих pa-ботутого процессора, с которым они связаны, а также 18259А-совместимого контроллера прерываний, транслирующего входные сигналы APIC в соответствующие PIC-эквиваленты. APIC ввода-вывода отвечает за реализацию алгоритмов перенаправления прерываний, и операционная система выбирает нужный ей алгоритм (в Windows выбор возлагается на HAL). Эти алгоритмы равномерно распределяют между процессорами нагрузку, связанную с обработкой прерываний от устройств, и в максимальной мере используют все преимущества локальности, направляя такие прерывания процессору, который только что обрабатывал прерывания аналогичного типа.

Контроллеры прерываний на платформе x64

Поскольку архитектура x64 совместима с операционными системами для x86, системы на базе x64 должны предоставлять те же контроллеры прерываний, что и на базе x86. Однако х64-версии Windows не будут работать на системах без APIC (т. е. они не поддерживают PIC).

Контроллеры прерываний на платформе IA64

B архитектуре IA64 используется контроллер прерываний Streamlined Advanced Programmable Interrupt Controller (SAPIC) - результат эволюционного развития APIC Главное различие между архитектурами APIC и SAPIC в том, что APIC ввода-вывода в APIC-системе направляет прерывания локальным APIC по выделенной шине APIC, тогда как в системе SAPIC прерывания передаются по шине ввода-вывода и системы (I/O and system bus) для большего быстродействия. Еще одно различие - перенаправление прерываний и балансировка нагрузки в APIC-системе обрабатывается самой шиной APIC, а в SAPIC-системе, где нет выделенной шины APIC, требуется, чтобы соответствующая поддержка была запрограммирована в микрокоде (прошивке). Ho, даже если эта поддержка имеется в микрокоде, Windows не использует ее - вместо этого она статически назначает прерывания процессорам по принципу карусели.

ЭКСПЕРИМЕНТ: просмотр конфигурации PIC и APIC

Конфигурацию PIC в однопроцессорной системе и APIC в многопроцессорной системе можно просмотреть с помощью команд !pic или !apic отладчика ядра. (Для этого эксперимента LiveKd не годится, так как она не может напрямую обращаться к оборудованию.) Ниже показан образец вывода команды !pic в однопроцессорной системе (учтите, что команда !pic не работает в системе, использующей APIC HAL).

Ha следующем листинге приводится выходная информация команды !apic в системе, использующей MPS HAL. Префикс «0:» в командной строке отладчика говорит о том, что текущие команды выполняются на процессоре 0, поэтому данный листинг относится к APIC ввода-вывода процессора 0.

Теперь взгляните на образец вывода команды !ioapic, показывающей конфигурацию APIC ввода-вывода:

Уровни запросов программных прерываний

Хотя контроллеры прерываний различают уровни приоритетов прерываний, Windows использует свою схему приоритетов прерываний, известную под названием уровни запросов прерываний (interrupt request levels, IRQL). Внутри ядра IRQL представляются в виде номеров 0-31 в системах x86 и 0-15 в системах x64 и IA64, причем больший номер соответствует прерыванию с более высоким приоритетом. Ядро определяет стандартный набор IRQL для программных прерываний, a HAL увязывает IRQL с номерами аппаратных прерываний. IRQL, определенные для архитектуры x86, показаны на рис. 3-3, а аналогичные сведения для архитектур x64 и IA64 - на рис. 3-4.

ПРИМЕЧАНИЕ Уровень SYNCH_LEVEL, используемый многопроцессорными версиями ядра для защиты доступа к индивидуальным для каждого процессора блокам PRCB (processor control blocks), не показан на этих схемах, так как его значение варьируется в разных версиях Windows. Описание SYNCH_LEVEL и его возможных значений см. в главе 6.

Рис. 3-4. Уровни запросов прерываний (IRQL) в системах x64 и IA64

Прерывания обслуживаются в порядке их приоритета, и прерывания с более высоким приоритетом вытесняют обработку прерываний с меньшим приоритетом. При возникновении прерывания с высоким приоритетом процессор сохраняет информацию о состоянии прерванного потока и активизирует сопоставленный с данным прерыванием диспетчер ловушки. Последний повышает IRQL и вызывает процедуру обслуживания прерывания (ISR). После выполнения ISR диспетчер прерывания понижает IRQL процессора до исходного уровня и загружает сохраненные ранее данные о состоянии машины. Прерванный поток возобновляется с той точки, где он был прерван. Когда ядро понижает IRQL, могут «материализоваться» ранее замаскированные прерывания с более низким приоритетом. Тогда вышеописанный процесс повторяется ядром для обработки и этих прерываний.

Уровни приоритетов IRQL имеют совершенно иной смысл, чем приоритеты в схеме планирования потоков (см. главу 6). Приоритет в этой схеме является атрибутом потока, тогда как IRQL - атрибутом источника прерывания, например клавиатуры или мыши. Кроме того, IRQL каждого процессора меняется во время выполнения команд операционной системы.

Значение IRQL определяет, какие прерывания может получать данный процессор. IRQL также используется для синхронизации доступа к структурам данных режима ядра (о синхронизации мы поговорим позже). При выполнении поток режима ядра повышает или понижает IRQL процессора либо напрямую (вызовом соответственно KeRaiseIrql или KeLowerIrqL), либо - что бывает гораздо чаще - опосредованно (через функции, которые обращаются к синхронизирующим объектам ядра). Как показано на рис. 3-5, прерывания от источника с IRQL, превышающим текущий уровень, прерывают работу процессора, а прерывания от источников, IRQL которых меньше или равен текущему уровню,маскируются до тех пор, пока выполняемый поток не понизит IRQL.

Поскольку доступ к PIC - операция довольно медленная, в HAL, использующих PIC, реализован механизм оптимизации «отложенный IRQL» (lazy IRQL), который избегает обращений к PIC Когда IRQL повышается, HAL - вместо того чтобы изменять маску прерывания - просто отмечает новый IRQL. Если вслед за этим возникает прерывание с более низким приоритетом, HAL устанавливает маску прерывания в соответствии с первым и откладывает обработку прерывания с более низким приоритетом до понижения IRQL. Таким образом, если при повышенном IRQL не возникнет прерываний с более низким приоритетом, HAL не потребуется обращаться к PIC.

Поток режима ядра повышает и понижает IRQL процессора, на котором он выполняется, в зависимости от того, что именно делает этот поток. Например, обработчик ловушки (или сам процессор) при прерывании повышает IRQL процессора до IRQL источника прерывания. B результате все прерывания с более низким или равным IRQL маскируются (только на этом процессоре), что не дает прерыванию с таким же или более низким IRQL помешать процессору обработать текущее прерывание. Замаскированные прерывания либо обрабатываются другим процессором, либо откладываются до понижения IRQL. Поэтому все системные компоненты, в том числе ядро и драйверы устройств, пытаются удерживать IRQL на уровне passive («пассивный»), иногда называемом низким уровнем. Если бы IRQL долго оставался неоправданно высоким, драйверы устройств не смогли бы оперативно реагировать на аппаратные прерывания.

ПРИМЕЧАНИЕ Прерывания APC_LEVEL являются исключением из правила, которое гласит, что повышение IRQL блокирует прерывания такого же уровня и ниже. Если поток повышает IRQL до уровня APC_ LEVEL, а затем отключается от процессора из-за появления прерывания DISPATCH_LEVEL, то система может доставить ему прерывание APC_LEVEL, как только он вновь получит процессорное время. Таким образом, APC_LEVEL можно считать IRQL, локальным для потока.

ЭКСПЕРИМЕНТ: определяем IRQL

Если вы работаете с отладчиком ядра в Windows Server 2003, то можете определить IRQL процессора командой !irql:

kd› !irql

Debugger saved IRQL for processor 0x0 - 0 (L0W_LEVEL)

Заметьте, что в структуре данных, называемой PCR (processor control region), и ее расширении - PRCB (processor control block) имеется поле с именем Irql. Эти структуры содержат информацию о состоянии каждого процессора в системе, в том числе текущий IRQL, указатель на аппаратную IDT, сведения о текущем потоке и потоке, который будет выполняться следующим. Ядро и HAL используют эту информацию для выполнения операций, специфичных для данной машины и ее архитектуры. Отдельные части структур PCR и PRCB открыто определены в заголовочном файле Ntddk.h (в Windows DDK). Загляните в него, чтобы получить представление об этих структурах.

Для просмотра содержимого PCR воспользуемся командой !pcr отладчика ядра.

K сожалению, Windows не поддерживает поле Irql на платформах, не использующих отложенные IRQL, поэтому в большинстве систем это поле всегда содержит 0.

Так как изменения IRQL процессора существенно влияют на функционирование системы, они возможны только в режиме ядра. Потоки пользовательского режима не могут изменять IRQL процессора. Это значит, что при выполнении потоков пользовательского режима значение IRQL процессора всегда равно passive. Только при выполнении кода режима ядра IRQL может быть выше этого уровня.

Каждый уровень прерывания имеет определенное назначение. Так, ядро генерирует межпроцессорное прерывание (interprocessor interrupt, IPI), чтобы потребовать выполнения какой-либо операции от другого процессора, например, при диспетчеризации некоего потока или обновлении кэша ассоциативного буфера трансляции [translation look-aside buffer (TLB) cache]. Системный таймер через регулярные промежутки генерирует прерывания, на которые ядро реагирует обновлением системного времени, и это используется для измерения продолжительности выполнения потока. Если аппаратная платформа поддерживает два таймера, то для измерения производительности ядро добавляет еще один уровень прерываний от таймера. HAL поддерживает несколько уровней запросов прерываний для устройств, управляемых прерываниями; конкретное число таких уровней зависит от процессора и конфигурации системы. Ядро использует программные прерывания для инициации планирования потоков и асинхронного вмешательства в выполнение потока.

Увязка прерываний с IRQL

Уровни IRQL и запросы прерываний (IRQ) - вещи разные. Концепция IRQL в архитектурах, на которых работает Windows, не реализована аппаратно. Тогда возникает вопрос как Windows определяет, какой IRQL следует присвоить прерыванию? Ответ нужно искать в HAL. B Windows за определение устройств на конкретной шине (PCI, USB и т. д.) и назначение им прерываний отвечают драйверы устройств особого типа - драйверы шин. Драйвер шины сообщает эту информацию диспетчеру Plug and Play, и тот, учитывая приемлемые для других устройств прерывания, принимает решение о конкретных прерываниях, выделяемых каждому устройству. Далее он вызывает HAL-функцию HalpGetSystemInterruptVector, которая увязывает прерывания со значениями IRQL.

Этот алгоритм неодинаков в различных версиях HAL. B однопроцессорных х86-системах HAL выполняет прямую трансляцию: IRQL данного вектора прерывания вычисляется путем вычитания значения вектора из 27. Таким образом, если устройство использует 5-й вектор прерывания, его ISR выполняется при IRQL, равном 22. B многопроцессорной х86-системе преобразования более сложны. APIC поддерживает более 200 векторов прерываний, поэтому при трансляции «один в один» имеющихся IRQL окажется недостаточно. Многопроцессорная версия HAL присваивает IRQL векторам прерываний, циклически перебирая значения из диапазона IRQL устройств (device IRQL, DIRQL). B итоге на многопроцессорной х86-системе не так-то просто предсказать или выяснить IRQL, назначаемый IRQ. Наконец, в x64- и IА64-системах HAL вычисляет IRQL для IRQ путем деления вектора прерывания, назначенного данному IRQ, на 16.

Предопределенные IRQL

Давайте повнимательнее приглядимся к предопределенным IRQL, начиная с самого верхнего уровня схемы, представленной на рис. 3-5.

(o) Уровень «high» (высокий) используется ядром, только если оно останавливает систему в функции KeBugCheckEx и маскирует все прерывания.

(o) Уровень «power fail» (отказ электропитания) был заложен еще в самый первый проект Microsoft Windows NT Он определяет поведение системы при отказе электропитания, но никогда не применялся.

(o) Уровень «interprocessor interrupt» (межпроцессорное прерывание) используется для того, чтобы запрашивать от другого процессора выполнение какой-либо операции, например, при постановке в очередь прерывания DISPATCH_EVEL для планирования конкретного потока к выполнению, при обновлении кэша TLB, завершении работы или крахе системы.

(o) Уровень «clock» (часы) используется для системных часов, с помощью которых ядро отслеживает время суток, измеряет и распределяет процессорное время между потоками.

(o) Уровень «profile» (профиль) используется системным таймером реального времени, если активизирован механизм профилирования ядра (kernel profiling), т. е. измерения его производительности. Когда он активен, обработчик ловушки профилирования регистрирует адрес команды, выполнявшейся на момент прерывания. Co временем создается таблица адресов, которую можно извлечь и проанализировать с помощью соответствующих утилит. Вы можете скачать утилиту Kernrate, позволяющую просматривать статистику, полученную при использовании механизма профилирования ядра. Подробнее об этой утилите см. описание эксперимента с Kernrate.

(o) Уровень «device» (устройство) применяется для задания приоритетов прерываний от устройств (о принципах увязки аппаратных прерываний с IRQL см. предыдущий раздел).

(o) Прерывания уровней «DPC/dispatch» и «APC» являются программными; они генерируются ядром и драйверами устройств (о DPC и APC будет рассказано позже).

(o) Самый низкий уровень IRQL, «passive» (пассивный), на самом деле вообще не является уровнем прерывания. При этом значении IRQL потоки выполняются обычным образом и могут возникать любые прерывания.

ЭКСПЕРИМЕНТ: применение утилиты Kernrate

Утилита профилирования ядра (Kernrate) позволяет включать таймер профилирования системы, собирать образцы кода, выполняемого при срабатывании таймера, и выводить сводную информацию, отражающую распределение процессорного времени по образам файлов и функциям. Ee можно использовать для отслеживания процессорного времени, потребляемого индивидуальными процессами, и/или времени, проведенного в режиме ядра независимо от процессов (например, для выполнения процедур обслуживания прерываний). Профилирование ядра полезно, когда вы хотите выявить точки, в которых на выполнение кода тратится больше всего процессорного времени.

B своей простейшей форме Kernrate сообщает, сколько процессорного времени было использовано каждым модулем ядра (Ntoskrnl, драйверами и т.д.). Попробуйте, к примеру, выполнить следующие операции.

1. Откройте окно командной строки.

2. Введите cd c:\program files\krview\kernrates.

3. Введите dir. (Вы увидите образы kernrate для каждой платформы.)

4. Запустите образ, который подходит для вашей платформы (без аргументов или ключей). Например, Kernrate_i386_XP.exe - это образ для Windows XP на платформе x86.

5. Пока Kernrate выполняется, поделайте что-нибудь в системе. Скажем, запустите Windows Media Player и проиграйте музыку, запустите игру, интенсивно работающую с графикой, или перечислите содержимое каталога на удаленном сетевом ресурсе.

6. Нажмите Ctrl+C, чтобы остановить Kernrate. Это заставит Kernrate вывести статистику за прошедший период.

Ниже приведена часть вывода Kernrate, когда выполнялся Windows Media Player, воспроизводивший дорожку с компакт-диска.

Сводные данные показывают, что система провела 11,7% времени в режиме ядра, 30,2% в пользовательском режиме, 58,1% в простое, 6,4% на уровне DPC и 1,3% на уровне прерываний (interrupt level). Модуль, чаще всего требовавший к себе внимания, был GV3.SYS, драйвер процессора для Pentium M (семейства Geyserville). Он используется для сбора информации о производительности, поэтому и оказался на первом месте. Модуль, занявший второе место, - Smwdm.sys, драйвер звуковой платы на компьютере, где проводился тест. Это вполне объяснимо, учитывая, что основную нагрузку в системе создавал Windows Media Player, посылавший звуковой ввод-вывод этому драйверу.

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

C:\Program Files\KrView\Kernrates›Kernrate_i386_XP.exe -z ntoskrnl -z win32k

B данном случае самым «прожорливым» был модуль Win32k.sys, драйвер системы, отвечающей за работу с окнами. Второй в списке - видеодрайвер. И действительно, основная нагрузка в системе была связана с рисованием окна на экране. B детальном выводе для Win32k.sys видно, что наиболее активна была его функция EngPaint, основная GDI-функция для рисования на экране.

Ha код, выполняемый на уровне «DPC/dispatch» и выше, накладывается важное ограничение: он не может ждать освобождения объекта, если такое ожидание заставило бы планировщик подключить к процессору другой поток (а это недопустимая операция, так как планировщик синхронизирует свои структуры данных на уровне «DPC/dispatch» и, следовательно, не может быть активизирован для выполнения перераспределения процессорного времени). Другое ограничение заключается в том, что при уровне IRQL «DPC/ dispatch» или выше доступна только неподкачиваемая память. Ha самом деле второе ограничение является следствием первого, так как обращение к отсутствующей в оперативной памяти странице вызывает ошибку страницы. Тогда диспетчер памяти должен был бы инициировать операцию дискового ввода-вывода, после чего ждать, когда драйвер файловой системы загрузит эту страницу с диска. Это в свою очередь вынудило бы планировщик переключить контекст (возможно, на поток простоя, если нет ни одного пользовательского потока, ждущего выполнения). B результате было бы нарушено правило, запрещающее вызов планировщика в таких ситуациях (поскольку при чтении с диска IRQL все еще остается на уровне «DPC/dispatch» или выше). При нарушении любого из этих двух ограничений происходит крах системы с кодом завершения IRQL_NOT_LESS_OR_EQUAL (подробнее о кодах завершения при крахе системы см. главу 4). Кстати, нарушение этих ограничений является довольно распространенной ошибкой в драйверах устройств. Локализовать причину ошибок такого типа помогает утилита Driver Verifier, о которой будет подробно рассказано в разделе «Утилита Driver Verifier» главы 7.

Объекты «прерывание» (interrupt objects)

Ядро предоставляет переносимый (портируемый) механизм - объект прерывания, позволяющий драйверам устройств регистрировать ISR для своих устройств. Этот объект содержит всю информацию, необходимую ядру для назначения конкретного уровня прерывания для ISR устройства, включая адрес ISR, IRQL устройства и запись в IDT ядра, с которой должна быть сопоставлена ISR. При инициализации в объект прерывания из шаблона обработки прерываний, KiIn-terruptTemplate, копируется несколько ассемблерных инструкций - код диспетчеризации. Этот код выполняется при возникновении прерывания.

Код, хранящийся в объекте прерывания, вызывает реальный диспетчер прерываний, которым обычно является процедура ядра KiInterruptDispatch или KiChainedDispatch, и передает ему указатель на объект прерывания. KiInterruptDispatch обслуживает векторы прерываний, для которых зарегистрирован только один объект прерывания, a KiChainedDispatch - векторы, разделяемые несколькими объектами прерываний. B объекте прерывания содержится информация, необходимая процедуре KiChainedDispatch для поиска и корректного вызова ISR драйвера. Объект прерывания также хранит значение IRQL, сопоставленное с данным прерыванием, так что KiDispatchInterrupt или KiChainedDispatch может перед вызовом ISR повысить IRQL до нужного уровня и вернуть его к исходному после завершения ISR. Этот двух-этапный процесс необходим из-за того, что при начальной диспетчеризации нельзя передать указатель (или какой-либо иной аргумент) объекту прерывания, так как она выполняется не программно, а аппаратно. B многопроцессорных системах ядро создает и инициализирует объект прерывания для каждого процессора, позволяя их локальным APlC принимать конкретные прерывания. Ha рис. 3-6 показана типичная схема обслуживания прерываний, сопоставленных с объектами прерываний.

ЭКСПЕРИМЕНТ: изучение внутреннего устройства прерываний

C помощью отладчика ядра вы можете просмотреть детальные сведения об объекте прерывания, в том числе его IRQL, адрес ISR и собственный код диспетчеризации прерывания (custom interrupt dispatching code). Во-первых, введите команду !idt и найдите запись со ссылкой на I8042KeyboardInterruptService - процедуру ISR для устройства «PS2-клавиатура»:

31: 8a39dc3c i8042prt!I8042KeyboardInterruptService (KINTERRUPT 8a39dc00)

Для просмотра содержимого объекта, сопоставленного с прерыванием, введите dt nt!_kinterrupt, указав адрес, следующий за KINTERRUPT:

B этом примере IRQL, назначенный Windows прерыванию, - 0x1a (или 26 в десятичной системе). Поскольку вывод получен на однопроцессорной х8б-системе, IRQ равно 1 (IRQL в таких системах вычисляются путем вычитания IRQ из 27). Это можно проверить, открыв Device Manager (Диспетчер устройств) [на вкладке Hardware (Оборудование) в окне свойств системы)], найдя PS/2-клавиатуру и посмотрев назначенные ей ресурсы, как показано на следующей иллюстрации.

B многопроцессорных х86-системах IRQ назначается в основном случайным образом, а в x64- или IАб4-системе вы увидите, что IRQ - это номер вектора прерываний [0x31 (49 в десятичной системе)], деленный на 16.

Адрес ISR для объекта прерывания хранится в поле ServiceRoutine (его и показывает !idt в своем выводе), а код прерывания, срабатывающий при появлении этого прерывания, - в массиве DispatchCode в конце объекта прерывания. Этот код программируется так, чтобы создавать фрейм ловушки в стеке и потом вызывать функцию, хранящуюся в поле DispatchAddress (в нашем примере это KilnterruptDispatcb}, с передачей ей указателя на объект прерывания.

Windows и обработка данных в реальном времени

K средам, предназначенным для работы в реальном времени, предъявляются либо жесткие, либо очень жесткие требования к максимальному времени реакции. Реакция системы реального времени, к которой предъявляются очень жесткие требования (например, системы управления атомной электростанцией), должна быть исключительно быстрой, иначе неизбежны катастрофы, опасные не только для оборудования, но и для жизни людей. Менее ответственные системы реального времени (например, система экономичного расхода топлива автомобиля) могут в какой-то мере отклоняться от этих требований, но их соблюдение все же желательно. B системах реального времени устройствами ввода служат датчики, а устройствами вывода - управляющие устройства. Проектировщик компьютерных систем реального времени должен знать величину максимально допустимого запаздывания между моментом генерации прерывания устройством ввода и ответом управляющего устройства, контролируемого драйвером. Анализ самых неблагоприятных вариантов должен учитывать как запаздывание операционной системы, так и запаздывание драйверов и приложений.

Поскольку проконтролировать расстановку приоритетов IRQ устройств операционной системой Windows нельзя, а пользовательские приложения выполняются лишь при IRQL уровня «passive», Windows не всегда подходит для обработки данных в реальном времени. Максимальное запаздывание определяется в конечном счете устройствами и драйверами системы, а не самой Windows. Этот фактор становится проблемой при использовании готового оборудования, имеющегося в продаже. Проектировщик может столкнуться с трудностями при определении максимального времени выполнения ISR или DPC драйвера готового устройства. Даже после тестирования он не сможет гарантировать, что запаздывание ни при каких обстоятельствах не превысит заданного предела. Более того, суммарное запаздывание системных DPC и ISR, как правило, существенно превосходит значения, приемлемые для чувствительных к задержкам систем.

Хотя ко многим типам встраиваемых систем (например, к принтерам и автомобильным компьютерам) предъявляются требования, как к системам реального времени, Windows XP Embedded не обладает соответствующими характеристиками. Это просто версия Windows XP, которая создана с использованием технологии, лицензированной Microsoft у компании VenturCom, и способна работать в системах с ограниченными ресурсами. Так, для устройства без сетевых функций исключается вся функциональность Windows XP, связанная с поддержкой работы в сетях, включая средства управления сетью, драйверы стека протокола и сетевого адаптера.

Тем не менее третьи фирмы поставляют версии ядра реального времени для Windows. Их подход заключается в том, что они встраивают ядро реального времени в собственный HAL и выполняют Windows как задачу в операционной системе реального времени. Windows, выполняемая в таком виде, служит в качестве пользовательского интерфейса системы и имеет меньший приоритет по сравнению с задачами, ответственными за управление нужным устройством. Пример расширения ядра Windows реального времени можно увидеть на Web-сайте VenturCom www.venturcom.com.

Сопоставление ISR с конкретным уровнем прерывания называется подключением объекта прерывания, а разрыв связи между ISR и записью в IDT - отключением. Эти операции, выполняемые функциями ядра IoCon-nectInterrupt и IoDisconnectInterrupt, позволяют драйверу устройства «включать» ISR после своей загрузки и «отключать» ISR, если он не загружен.

Применение объекта прерывания для регистрации ISR позволяет драйверам устройств избегать прямого взаимодействия с контроллером прерываний (разным на разных процессорных архитектурах) и исключает необходимость детального знания IDT. Это дает возможность создавать переносимые драйверы устройств, поскольку благодаря такой функциональности ядра программировать драйверы устройств на ассемблере и учитывать в них особенности конкретных процессоров больше не нужно.

Использование объекта прерывания дает и другие преимущества: ядро может синхронизировать выполнение ISR с другими частями драйвера устройства, которые могут разделять данные с ISR. (Подробнее о том, как драйверы устройств реагируют на прерывания, см. главу 9.)

Более того, объекты прерывания позволяют ядру легко вызывать более одной ISR для любого уровня прерывания. Если несколько драйверов создают объекты прерывания и сопоставляют их с одной записью в IDT, то при прерывании на определенной линии диспетчер вызывает каждую из этих процедур (ISR). Такая функциональность позволяет ядру поддерживать конфигурации в виде цепочек, когда несколько устройств совместно используют одну линию прерывания. Когда одна из ISR объявляет диспетчеру о захвате прерывания, происходит разрыв цепочки. Если несколько устройств, разделяющих одну линию прерывания, одновременно запрашивают обслуживание, то устройства, не получившие подтверждения от своих ISR, будут вновь генерировать прерывания, как только диспетчер понизит IRQL. Связывание устройств в цепочку разрешается, только если все драйверы устройств, стремящиеся использовать одно и то же прерывание, сообщат ядру о своей способности разделять данное прерывание. Если они не в состоянии совместно использовать это прерывание, диспетчер Plug and Play пере-

назначит IRQ с учетом запросов каждого устройства. Если разделяемым является вектор прерываний, объект прерывания вызывает KiChainedDispatch, которая поочередно обращается к ISR каждого зарегистрированного объекта прерывания, пока один из них не сообщит, что прерывание вызвано им, или пока все они не будут выполнены. B одном из предыдущих примеров вывода !idt вектор 0x3b был подключен к нескольким объектам прерываний, связанным в цепочку (chained interrupt objects).

Программные прерывания

Хотя большинство прерываний генерируется аппаратно, ядро Windows тоже может генерировать прерывания - только они являются программными. Этот вид прерываний служит для решения многих задач, в том числе:

(o) инициации диспетчеризации потоков;

(o) обработки прерываний, не критичных по времени;

(o) обработки событий таймеров;

(o) асинхронного выполнения какой-либо процедуры в контексте конкретного потока;

(o) поддержки асинхронного ввода-вывода. Эти задачи подробно рассматриваются ниже.

Прерывания DPC или диспетчеризации

Когда дальнейшее выполнение потока невозможно, например из-за его завершения или перехода в ждущее состояние, ядро напрямую обращается к диспетчеру, чтобы вызвать немедленное переключение контекста. Однако иногда ядро обнаруживает, что перераспределение процессорного времени (rescheduling) должно произойти при выполнении глубоко вложенных уровней кода. B этой ситуации ядро запрашивает диспетчеризацию, но саму операцию откладывает до выполнения текущих действий. Такую задержку удобно организовать с помощью программного прерывания DPC (deferred procedure call).

При необходимости синхронизации доступа к разделяемым структурам ядра последнее всегда повышает IRQL процессора до уровня «DPC/dispatch» или выше. При этом дополнительные программные прерывания и диспетчеризация потоков запрещаются. Обнаружив необходимость в диспетчеризации, ядро генерирует прерывание уровня «DPC/dispatch». Ho поскольку IRQL уже находится на этом уровне или выше, процессор откладывает обработку этого прерывания. Когда ядро завершает свои операции, оно определяет, что должно последовать снижение IRQL ниже уровня "DPC/dispatch", и проверяет, не ожидают ли выполнения отложенные прерывания диспетчеризации. Если да, IRQL понижается до уровня «DPC/dispatch», и эти отложенные прерывания обрабатываются. Активизация диспетчера потоков через программное прерывание - способ отложить диспетчеризацию до подходящего момента. Однако Windows использует программные прерывания для отложенного выполнения и других операций.

При этом IRQL ядро обрабатывает не только диспетчеризацию потоков, но и DPC DPC - это функция, выполняющая системную задачу, менее критичную по времени в сравнении с текущей. Эти функции называются отложенными (deferred), так как не требуют немедленного выполнения.

DPC позволяют операционной системе генерировать прерывания и выполнять системные функции в режиме ядра. Ядро использует DPC для обработки прерываний по таймеру (и освобождения потоков, ждущих на таймерах), а также для перераспределения процессорного времени по истечении кванта времени, отведенного текущему потоку. Драйверы устройств используют DPC для выполнения запросов ввода-вывода. Для своевременного обслуживания аппаратных прерываний Windows - во взаимодействии с драйверами устройств - пытается удерживать текущий IRQL ниже IRQL устройств. Один из способов достижения этой цели заключается в следующем. ISR должна выполнять минимум действий по обслуживанию своего устройства, сохранять переменные данные о состоянии прерывания и откладывать передачу данных или выполнение других не столь критичных по времени операций, как DPC, до снижения IRQL к уровню «DPC/dispatch» (подробнее о DPC и системе ввода-вывода см. главу 9).

DPC представляется DPC-объектом, управляющим объектом ядра, невидимым программам пользовательского режима, но видимым драйверам и системному коду. Наиболее важной частью информации DPC-объекта является адрес системной функции, которую ядро должно вызвать для обработки прерывания DPC DPC-процедуры, ожидающие выполнения, хранятся в управляемых ядром очередях (по одной на каждый процессор). Эти очереди называются очередями DPC Запрашивая DPC, системный код вызывает ядро для инициализации DPC-объекта и помещает его в очередь DPC

По умолчанию ядро помещает DPC-объекты в конец очереди DPC процессора, на котором был запрошен DPC (как правило, это процессор, на котором выполняется ISR). Однако драйвер устройства может изменить это, указав приоритет DPC (низкий, средний или высокий; по умолчанию - средний) или направив DPC конкретному процессору. DPC, направленный конкретному процессору, называется целевым DPC (targeted DPC). Если у DPC низкий или средний приоритет, ядро помещает DPC-объект в конец очереди, а если у DPC высокий приоритет, то - в начало.

Когда IRQL процессора вот-вот понизится с уровня «DPC/dispatch» или более высокого до уровня «APC» или «passive», ядро переходит к обработке всех DPC Windows оставляет IRQL на уровне «DPC/dispatch» и извлекает все DPC-объекты из очереди данного процессора (т. е. ядро опустошает очередь), поочередно вызывая каждую DPC-функцию. Ядро разрешает уменьшить IRQL ниже уровня «DPC/dispatch» для продолжения выполнения обычных потоков только после опустошения очереди. Схема обработки DPC показана на рис. 3-7.

Приоритеты DPC могут влиять на поведение системы и иным способом. Обычно ядро начинает опустошение очереди DPC с прерывания уровня «DPC/dispatch». Такое прерывание генерируется ядром, только если DPC направлен на процессор, на котором выполняется ISR, и DPC имеет средний или высокий приоритет. Если у DPC низкий приоритет, ядро генерирует прерывание, только если число незавершенных запросов DPC превышает пороговое значение или если число DPC, запрошенных на процессоре за установленный период, невелико. Если DPC направлен другому процессору (не тому, на котором выполняется ISR) и его приоритет высокий, ядро немедленно посылает ему диспетчерское IPI, сигнализируя целевому процессору о необходимости опустошения его очереди DPC Если приоритет DPC средний или низкий, для появления прерывания «DPC/dispatch» число DPC в очереди целевого процессора должно превышать пороговое значение. Системный поток простоя также опустошает очередь DPC процессора, на котором он выполняется. Хотя уровни приоритета и направление DPC являются довольно гибкими средствами, у драйверов устройств редко возникает необходимость в изменении заданного по умолчанию поведения своих DPC-объектов. B таблице 3-1 даются сведения о ситуациях, в которых начинается опустошение очереди DPC

Поскольку потоки пользовательского режима выполняются при низком IRQL, вероятность того, что DPC прервет выполнение обычного пользовательского потока, довольно велика. DPC-процедуры выполняются независимо от того, какой поток работает в настоящий момент. Это означает, что выполняемая DPC-процедура не в состоянии предугадать текущий размер спроецированного адресного пространства процесса. DPC-процедуры могут вызывать функции ядра, но не могут обращаться к системным сервисам, генерировать ошибки страницы, создавать или ждать объекты диспетчера. Однако они способны получать доступ к неподкачиваемым областям системной памяти, поскольку системное адресное пространство всегда спроецировано независимо от того, что представляет собой текущий процесс.

DPC используются не только драйверами, но и ядром. Ядро чаще всего применяет DPC для обработки ситуации, когда истекает выделенный квант времени. При каждом такте системного таймера генерируется прерывание с IRQL-уровнем «clock». Обработчик прерываний таймера (выполняемый при IRQL, равном «clock») обновляет системное время и уменьшает значение счетчика, отслеживающего время выполнения текущего потока. Когда значение счетчика обнуляется, квант времени, отведенный потоку, заканчивается, и ядру может понадобиться перераспределить процессорное время - эта задача имеет более низкий приоритет и должна выполняться при IRQL, равном «DPC/dispatch». Обработчик прерываний таймера ставит DPC в очередь, чтобы инициировать диспетчеризацию потоков, после чего завершает свою работу и понижает IRQL процессора. Поскольку приоритет прерываний DPC ниже, чем аппаратных, перед генерацией прерывания DPC сначала обрабатываются все аппаратные прерывания, возникающие до завершения обработки прерывания таймера.

ЭКСПЕРИМЕНТ: мониторинг активности прерываний и DPC

Process Explorer позволяет вести мониторинг активности прерываний и DPC, добавив столбец Context Switch Delta и наблюдая за процессами Interrupt и DPC Это не настоящие процессы, они показываются как процессы просто для удобства и не вызывают переключений контекста. Счетчик переключений контекста в Process Explorer для этих псевдопроцессов отражает число возникновений каждого из них в течение предыдущего интервала обновления (refresh interval). Вы можете имитировать активность прерываний и DPC, быстро перемещая курсор мыши по экрану.

Вы также можете проследить выполнение конкретных процедур обслуживания прерываний (ISR) и отложенных вызовов процедур (DPC), используя встроенную поддержку трассировки событий (подробнее об этом будет рассказано позже) в Windows XP Service Pack 2 или Windows Server 2003 Service Pack 1 (и выше).

1. Инициируйте захват событий, введя команду:

tracelog -start -f kernel.etl -b 64 -UsePerfCounter -eflag 8 0x307 0x4084 0 0 0 0 0 0

2. Остановите захват событий, введя: tracelog -stop

3. Создайте отчеты по захваченным событиям, набрав команду: tracerpt kernel.etl -df -о -report

Это приведет к генерации двух файлов: workload.txt и dumpfile.csv.

4. Откройте workload.txt и вы увидите сводные сведения о времени, проведенном драйверами каждого типа в ISR- и DPC-процедурах.

5. Откройте файл dumpfile.csv, созданный на этапе 3; найдите строки, где во втором значении встречается ‹‹DPC» или «ISR». Например, следующие три строки из dumpfile.csv показывают DPC таймера, DPC и ISR:

Первый адрес относится к DPC, вызванному срабатыванием таймера, который был поставлен в очередь клиентским драйвером редиректора файловой системы (file system redirector client driver). Второй относится к DPQ вызванному срабатыванием универсального таймера (generic timer). Наконец, третий - это адрес ISR для порт-драйвера ATAPI. Более подробные сведения см. по ссылке http://wwwmicrosoft. com/wbdc/driver/perform/mmdrv.mspx.

Прерывания APC

APC (asynchronous procedure call) позволяет выполнять пользовательские программы и системный код в контексте конкретного пользовательского потока (а значит, и в адресном пространстве конкретного процесса). Посколькудля выполнения в контексте конкретного потока APC ставятся в очередь и выполняются при IRQL ниже «DPC/dispatch», на их работу не налагаются ограничения, свойственные DPC АРС-процедура может обращаться к ресурсам (объектам), ждать освобождения описателей объектов, генерировать ошибки страниц и вызывать системные сервисы.

APC описывается управляющим объектом ядра - ЛРС-объектом. APC, ждущие выполнения, находятся в очереди ЛРС (APC queue), управляемой ядром. Очереди APC - в отличие от общесистемной очереди DPC - специфичны для конкретного потока, так как у каждого потока своя очередь APC При запросе на постановку APC в очередь ядро помещает его (APC) в очередь того потока, который будет выполнять АРС-процедуру. Далее ядро генерирует программное прерывание с уровнем APC, и поток, когда он в конечном счете начинает выполняться, обрабатывает APC

APC бывают двух видов: режима ядра и пользовательского режима. APC режима ядра для выполнения в контексте целевого потока не нужно «разрешение» со стороны этого потока, тогда как для APC пользовательского режима это обязательно. APC режима ядра прерывает поток и выполняет процедуру без вмешательства или согласия потока. APC режима ядра тоже бывают двух типов: обычные (normal) и специальные (special). Поток может отключить все APC режима ядра, повысив IRQL до уровня APC_LEVEL или вызвав KeEnterGuardedRegion, которая впервые появилась в Windows Server 2003. KeEnterGuardedRegion отключает доставку APC, устанавливая поле Spe-cialApcDisable в структуре KTHREAD вызвавшего потока (об этой структуре см. главу 6). Поток также может отключить только обычные APC режима ядра вызовом KeEnterCriticalRegion, которая устанавливает поле KernelApcDisable в структуре KTHREAD потока.

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

Драйверы устройств также применяют APC режима ядра. Например, если инициирована операция ввода-вывода и поток переходит в состояние ожи-

дания, к процессору может быть подключен другой поток из другого процесса. По завершении передачи данных устройством система ввода-вывода должна как-то вернуться в контекст потока, инициировавшего эту операцию ввода-вывода, чтобы он мог скопировать ее результаты в буфер в адресном пространстве своего процесса. Система ввода-вывода использует для выполнения подобных действий специальные APC режима ядра (применение APC в системе ввода-вывода подробно рассматривается в главе 9).

Некоторые Windows-функции вроде ReadFileEx, WriteFileEx и QueueUser-APC вызывают APC пользовательского режима. Так, функции ReadFileEx и WriteFileEx позволяют вызывающей программе указать процедуру завершения ввода-вывода (completion procedure), которая будет вызвана по окончании операции ввода-вывода. Процедура завершения ввода-вывода реализуется помещением APC в очередь потока, выдавшего запрос на ввод-вывод. Однако обратный вызов процедуры завершения не обязательно происходит в момент постановки APC в очередь, поскольку APC пользовательского режима передаются потоку, только если он находится в состоянии тревожного ожидания (alertable wait state). Поток может перейти в такое состояние, вызвав одну из Windows-функций: либо WaitForMultipleObjectsEx, либо Sleep-Ex. B обоих случаях, как только в очереди появится APC пользовательского режима, ядро прервет поток, передаст управление АРС-процедуре и возобновит его выполнение лишь после завершения АРС-процедуры. B отличие от APC режима ядра, которые выполняются на уровне «APC», APC пользовательского режима выполняются на уровне «passive».

Появление APC может переупорядочить очереди ожидания - списки, определяющие, какие потоки ждут, в каком порядке и на каких объектах (см. раздел по синхронизации далее в этой главе). Если в момент появления APC поток находится в состоянии ожидания, то после обработки АРС-процедуры поток возвращается в состояние ожидания, но перемещается в конец списка потоков, ждущих те же объекты.

Диспетчеризация исключений

B отличие от прерываний, которые могут возникать в любой момент, исключения являются прямым следствием действий выполняемой программы. Windows вводит понятие структурной обработки исключений (structured exception handling, SEH), позволяющей приложениям получать управление при возникновении исключений. При этом приложение может исправить ситуацию, которая привела к исключению, провести раскрутку стека (завершив таким образом выполнение подпрограммы, вызвавшей исключение) или уведомить систему о том, что данное исключение ему не известно, и тогда система продолжит поиск подходящего обработчика для данного исключения. B этом разделе мы исходим из того, что вы знакомы с базовыми концепциями структурной обработки исключений Windows; в ином случае сначала прочитайте соответствующую часть справочной документации Windows API из Platform SDK или главы 23-25 из четвертого издания книги Джеффри Рихтера «Windows для профессионалов». Учтите: хотя обработка исключений возможна через расширения языка программирования (например, с помощью конструкции_try в Microsoft Visual C++), она является системным механизмом и поэтому не зависит от конкретного языка.

B системах типа x86 все исключения имеют предопределенные номера прерываний, прямо соответствующие записям в IDT, ссылающимся на обработчики ловушек конкретных исключений. B таблице 3-2 перечислены исключения, определенные для систем типа x86, с указанием номеров прерываний. Как уже говорилось, первая часть IDT используется для исключений, а аппаратные прерывания располагаются за ней.

Все исключения, кроме достаточно простых, которые могут быть разрешены обработчиком ловушек, обслуживаются модулем ядра - диспетчером исключений (exception dispatcher). Его задача заключается в поиске обработчика, способного «справиться» с данным исключением. Примерами независимых от архитектуры исключений могут служить нарушения доступа к памяти, целочисленное деление на нуль, переполнение целых чисел, исключения при операциях с плавающей точкой и точки прерывания отладчика. Полный список независимых от архитектуры исключений см. в справочной документации Windows API.

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

Определенные исключения могут передаваться в неизменном виде пользовательским процессам. Например, при ошибке доступа к памяти или при переполнении в ходе арифметической операции генерируется исключение, не обрабатываемое операционной системой. Для обработки этих исключений подсистема окружения может устанавливать обработчики исключений на основе SEH-фрейма (далее для краткости - обработчик SEH-фрейма). Этим термином обозначается обработчик исключения, сопоставленный с вызовом конкретной процедуры. При активизации такой процедуры в стек заталкивается стековый фрейм, представляющий вызов этой процедуры. Co стековым фреймом можно сопоставить один или несколько обработчиков исключений, каждый из которых защищает определенный блок кода исходной программы. При возникновении исключения ядро ищет обработчик, сопоставленный с текущим стековым фреймом. Если его нет, ядро ищет обработчик, сопоставленный с предыдущим стековым фреймом, - и так до тех пор, пока не будет найден подходящий обработчик. Если найти обработчик исключения не удалось, ядро вызывает собственные обработчики по умолчанию.

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

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

Если исключение возникает в пользовательском режиме, диспетчер исключений предпринимает более сложные действия. Как поясняется в главе 6, подсистема Windows предусматривает порт отладчика (debugger port) и порт исключений (exception port) для приема уведомлений об исключениях пользовательского режима в Windows-процессах. Они применяются ядром при обработке исключений по умолчанию, как показано на рис. 3-8.

Точки прерывания в отлаживаемой программе являются распространенной причиной исключений. Поэтому диспетчер исключений первым делом проверяет, подключен ли к процессу, вызвавшему исключение, отладчик. Если подключен и системой является Windows 2000, диспетчер исключений посылает отладчику через LPC первое предупреждение. (Это уведомление на самом деле сначала поступает диспетчеру сеансов, а тот пересылает его соответствующему отладчику.) B Windows XP и Windows Server 2003 диспетчер исключений посылает сообщение объекта отладчика (debugger object message) объекту отладки (debug object), сопоставленному с процессом (который внутри системы рассматривается как порт).

Если к процессу не подключен отладчик или если отладчик не в состоянии обработать данное исключение, диспетчер исключений переключается в пользовательский режим, копирует фрейм ловушки в пользовательский стек, имеющий формат структуры данных CONTEXT (документирована в Platform SDK), и вызывает процедуру поиска обработчика SEH-фрейма. Если поиск не дал результатов, диспетчер возвращается в режим ядра и снова вызывает отладчик, чтобы пользователь мог продолжить отладку. При этом посылается второе (и последнее) предупреждение.

Если отладчик не запущен и обработчики SEH-фреймов не найдены, ядро посылает сообщение в порт исключений, сопоставленный с процессом потока. Этот порт (если таковой есть) регистрируется подсистемой окружения, контролирующей данный поток. Порт исключений дает возможность подсистеме окружения (прослушивающей этот порт) транслировать исходное исключение в уведомление или исключение, специфичное для ее окружения. CSRSS (Client/Server Run-Time Subsystem) просто выводит окно сообщения, уведомляющее пользователя о сбое, и завершает процесс. Когда подсистема POSIX получает от ядра сообщение о том, что один из потоков вызвал исключение, эта подсистема посылает вызвавшему исключение потоку сигнал в стиле POSIX. Ho, если ядро уже дошло до этого этапа в обработке исключения, а подсистема не способна обработать данное исключение, выполняется обработчик ядра по умолчанию, просто завершающий процесс, поток которого вызвал исключение.

Необработанные исключения

Ha вершине стека любого Windows-потока объявляется обработчик, имеющий дело с необработанными исключениями. За объявление отвечает внут-

ренняя Windows-функция start-of-process или start-of-tbread. Функция start-of-process срабатывает в момент начала выполнения первого потока процесса. Она вызывает главную точку входа в образе. Функция start-of-tbread выполняется при создании дополнительных потоков в процессе и вызывает стартовую процедуру, указанную в вызове CreateTbread.

ЭКСПЕРИМЕНТ: определение истинного стартового адреса Windows-потоков

Тот факт, что выполнение каждого Windows-потока начинается с системной (а не пользовательской) функции, объясняет, почему у каждого Windows-процесса стартовый адрес нулевого потока одинаков (как и стартовые адреса вторичных потоков). Стартовый адрес нулевого потока Windows-процессов соответствует Windows-функции start~of-process, а стартовые адреса остальных потоков являются адресом Windows-функции start-of-tbread. Для определения адреса пользовательской функции применим утилиту Tlist из Windows Support Tools. Для получения детальной информации о процессе наберите tlist имя процесса или tlist идентификатор процесса. Например, сравним стартовый адрес процесса Windows Explorer, сообщаемый утилитой Pstat (из Platform SDK), и стартовый адрес Tlist.

Стартовый адрес нулевого потока, сообщаемый Pstat, соответствует внутренней Windows-функции start-of-process, а стартовые адреса потоков 1-3 указывают адреса внутренних Windows-функций start-of-thread. C другой стороны, Tlist показывает стартовый адрес пользовательской функции, вызываемой внутренней стартовой Windows-функцией.

Поскольку большинство потоков в Windows-процессах начинается в одной из системных функций-оболочек, Process Explorer, показывая стартовые адреса потоков в процессе, пропускает фрейм начального вызова, представляющий функцию-оболочку, и вместо этого отображает второй фрейм в стеке. Например, обратите внимание на стартовый адрес потока в процессе, выполняющем Notepad.exe.

Process Explorer не выводит всю иерархию вызовов при отображении стека вызовов. Вот что вы получите, щелкнув кнопку Stack.

B строке 12 на этой иллюстрации показан первый фрейм в стеке - начало процесса-оболочки. Второй фрейм (строка 11) является основной точкой входа в Notepad.exe.

Базовый код внутренних стартовых функций выглядит так:

void Win32StartOfProcess(

LPTHREAD_START_ROUTINE lpStartAddr, LPVOID lpvThreadParm){

__try {

Заметьте: если при выполнении потока возникает исключение, не обрабатываемое этим потоком, вызывается Windows-фильтр необработанных исключений. Эта функция реализует поведение системы, когда та обнаруживает необработанное исключение. Поведение зависит от содержимого раздела реестра HKLM\Software\Microsoft\Windows NT\CurrentVersion\AeDebug. B нем есть два важных параметра: Auto и Debugger. Auto сообщает фильтру необработанных исключений, надо ли автоматически запускать отладчик или спросить у пользователя, что делать. По умолчанию этому параметру присваивается 1, что подразумевает автоматический запуск отладчика. Однако после установки средств разработки вроде Visual Studio его значение меняется на 0. Параметр Debugger является строкой, которая указывает путь к исполняемому файлу отладчика, который следует запускать при появлении необработанного исключения.

Отладчик по умолчанию - \Windows\System32\Drwtsn32.exe (Dr. Wat-son), который на самом деле является не отладчиком, а утилитой, сохраняющей сведения о рухнувшем приложении в фарше журнала (Drwtsn32.log) и обрабатывающей файл аварийного дампа (User.dmp). Оба этих файла по умолчанию помещаются в папку \Documents And Settings\All Users\ Documents\DrWatson. Для просмотра или изменения конфигурации утилиту Dr. Watson можно запустить в интерактивном режиме; при этом выводится окно с текущими параметрами (пример для Windows 2000 показан на рис. 3-9).

Файл журнала содержит такую базовую информацию, как код исключения, имя рухнувшего образа, список загруженных DLL, а также содержимое стека и последовательность команд потока, вызвавшая исключение. Более подробные сведения о содержимом этого файла можно получить, запустив Dr. Watson и щелкнув кнопку HeIp (Справка) в его окне.

B файл аварийного дампа записывается содержимое закрытых страниц процесса на момент возникновения исключения (но страницы кода из EXE-и DLL-модулей не включаются). Этот файл можно открыть с помощью Win-Dbg - Windows-отладчика, поставляемого с пакетом Debugging Tools или с Visual Studio 2003 и выше). Файл аварийного дампа перезаписывается при каждом крахе процесса. Поэтому, если его предварительно не скопировать или не переименовать, в нем будет содержаться информация лишь о последнем крахе процесса.

B Windows 2000 Professional визуальное уведомление включено по умолчанию. Окно сообщения, представленное на рис. 3-10, выводится Dr. Watson после того, как он сгенерирует аварийный дамп и запишет информацию в свой файл журнала.

Процесс Dr. Watson остается до тех пор, пока не будет закрыто это окно, и именно поэтому в Windows 2000 Server визуальное уведомление по умолчанию отключено. Дело вот в чем. Обычно сервер находится в отдельной комнате и возле него никто не сидит. Если на сервере рушится какое-то приложение, то подобное окно просто некому закрыть. По этой причине серверные приложения должны регистрировать ошибки в журнале событий Windows.

B Windows 2000, если параметр Auto установлен в 0, отображается окно, приведенное на рис. 3-11.

После щелчка кнопки OK процесс завершается. A если вы нажимаете кнопку Cancel, запускается системный отладчик (заданный параметром Debugger в реестре).

ЭКСПЕРИМЕНТ: необработанные исключения

Чтобы увидеть образец файла журнала Dr. Watson, запустите программу Accvio.exe. Эта программа вызовет нарушение доступа (ошибку защиты памяти) при попытке записи по нулевому адресу, всегда недей-cтвитeльнoмyдляWindows-пpoцeccoв (см. таблицу 7-6 в главе 7).

1. Запустите Registry Editor (Редактор реестра) и найдите раздел HKLM\ SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug.

2. Если значение параметра Debugger равно «drwtsn32 -p %ld -e %ld -g», ваша система настроена на использование Dr. Watson в качестве отладчика по умолчанию. Переходите в п. 4.

3. Если в параметре Debugger не указан Drwtsn32.exe, вы все равно можете протестировать Dr. Watson, временно установив его, а затем восстановив исходные параметры своего отладчика:

(o) сохраните где-нибудь текущее значение параметра (например, в файле Notepad или в буфере обмена);

(o) выберите из меню Start (Пуск) команду Run (Выполнить) и введите команду drwtsn32 -i (чтобы инициализировать параметр Debugger для запуска Dr. Watson).

4. Запустите тестовую программу Accvio.exe.

5. Вы должны увидеть одно из окон, описанных ранее (в зависимости от версии Windows, в которой вы работаете).

6. Если вы используете параметры Dr. Watson по умолчанию, то теперь сможете изучить файл журнала и файл дампа в каталоге файлов дампов. Для просмотра параметров Dr. Watson, запустите drwt-sn32 без аргументов. (Выберите из меню Start команду Run и введите drwtsn32.)

7. B качестве альтернативы щелкните последнюю запись в списке Application Errors (Ошибки приложения) и нажмите кнопку View (Показать) - будет выведена часть файла журнала Dr. Watson, содержащая сведения о нарушении доступа, вызванном Accvio.exe. Если вас интересуют детали формата файла журнала, щелкните кнопку HeIp (Справка) и выберите раздел Dr. Watson Log File Overview (Обзор файла журнала доктора Ватсона).

8. Если Dr. Watson не был отладчиком по умолчанию, восстановите исходное значение, сохраненное в п. 1.

Проведите еще один эксперимент: попробуйте перенастроить параметр Debugger на другую программу, например Notepad.exe (Блокнот) или Sol.exe (Solitaire). Снова запустите Accvio.exe. Обратите внимание, что запускается любая программа, указанная в параметре Debugger. To есть система не проверяет, действительно ли указанная в этом параметре программа является отладчиком. Обязательно восстановите исходные значения параметров реестра (введите drwtsn32 -i).

Windows-поддержка отчетов об ошибках

B Windows XP и Windows Server 2003 появился новый, более изощренный механизм отчетов об ошибках, называемый Windows Error Reporting. Он автоматизирует передачу информации о крахе как в пользовательском режиме, так и в режиме ядра. (Как применить этот механизм для получения сведений о крахе системы, см. главу 14.)

Windows Error Reporting можно настроить, последовательно выбрав My Computer (Мой компьютер), Properties (Свойства), Advanced (Дополнительно) и Error Reporting (Отчет об ошибках) (на экране появится диалоговое окно, показанное на рис. 3-12); то же самое можно сделать через параметры локальной или доменной политики группы, которые хранятся в разделе реестра HKLM\Software\Microsoft\PCHealth\ErrorReporting.

Перехватив необработанное исключение (об этом шла речь в предыдущем разделе), фильтр необработанных исключений выполняет начальную проверку, чтобы решить, надо ли запустить механизм Windows Error Reporting. Если параметр реестра HKLM\SOFTWARE\Microsoft\Windows NT\Current Version\AeDebug\Auto установлен в 0 или строка Debugger содержит текст «Drwtsn32», фильтр необработанных исключений загружает в аварийный процесс библиотеку \Windows\System32\Faultrep.dll и вызывает ее функцию ReportFault. Эта функция проверяет конфигурацию механизма отчетов об ошибках, которая хранится в разделе HKLM\Software\Microsoft\PCHealth\ ErrorReporting, и определяет, следует ли формировать отчет для данного процесса и, если да, то как. B обычном случае ReportFault создает процесс, выполняющий \Windows\System32\Dwwin.exe, который выводит окно, где пользователь уведомляется о крахе процесса и где ему предоставляется возможность передать отчет об ошибках в Microsoft (рис. 3-13).

При щелчке кнопки Send Error Report (Послать отчет), отчет об ошибках (минидамп и текстовый файл с детальными сведениями о номерах версий DLL, загруженных в рухнувший процесс) передается на онлайновый сервер анализа аварийных ситуаций, Watson.Microsoft.com. (B отличие от краха системы в режиме ядра здесь нет возможности найти какое-либо решение на момент отправки отчета.) Затем фильтр необработанных исключений создает процесс для запуска отладчика (обычно Drwtsn32.exe), который по умолчанию создает свой файл дампа и запись в журнале. B отличие от Windows 2000 этот файл содержит не полный дамп, а минидамп. Поэтому в ситуации, где для отладки рухнувшего приложения нужен полный дамп памяти процесса, вы можете изменить конфигурацию Dr. Watson, запустив его без аргументов командной строки, как было описано в предыдущем разделе.

B средах, где системы не подключены к Интернету или где администратор хочет контролировать, какие именно отчеты об ошибках посылаются в Microsoft, эти отчеты можно передавать на внутренний файл-сервер. Microsoft предоставляет опытным заказчикам утилиту Corporate Error Reporting, которая понимает структуру каталогов, создаваемую Windows Error Reporting, и позволяет администратору задавать условия, при которых отчеты формируются и передаются в Microsoft. (Подробности см. по ссылке http:// www.microsoft.com/resources/satech/cer.)

Диспетчеризация системных сервисов

Как показано на рис. 3-1, обработчики ловушек ядра обслуживают прерывания, исключения и вызовы системных сервисов. Из предыдущих разделов вы знаете, как проводится обработка прерываний и исключений. Здесь будет рассказано о вызовах системных сервисов. Диспетчеризация системных сервисов начинается с выполнения инструкции, закрепленной за такой диспетчеризацией. Эта инструкция зависит от процессора, на котором работает Windows.

Диспетчеризация 32-разрядных системных сервисов

Ha процессорах x86 до Pentium II использовалась инструкция int 0x2e (десятичное значение 46). B результате выполнения этой инструкции срабатывает ловушка, и Windows заносит в запись IDT под номером 46 указатель на

диспетчер системных сервисов (см. таблицу 3-1). Эта ловушка заставляет выполняемый поток переключиться в режим ядра и войти в диспетчер системных сервисов. Номер запрошенного системного сервиса указывается числовым аргументом, переданным в регистр процессора EAX. Содержимое регистра EBX указывает на список параметров, передаваемый системному сервису вызывающей программой.

Ha х86-процессорах Pentium II и выше Windows использует инструкцию sysenter, которую Intel специально определил для быстрой диспетчеризации системных сервисов. Для поддержки этой инструкции Windows сохраняет на этапе загрузки адрес процедуры ядра - диспетчера системных сервисов в регистре, сопоставленном с данной инструкцией. Выполнение инструкции приводит к переключению в режим ядра и запуску диспетчера системных сервисов. Номер системного сервиса передается в регистре процессора EAX, а регистр EDX указывает на список аргументов, предоставленных вызвавшим кодом. Для возврата в пользовательский режим диспетчер системных сервисов обычно выполняет инструкцию sysexit. (B некоторых случаях, например, когда в процессоре включен флаг single-step, диспетчер системных сервисов использует вместо sysexit инструкцию iretd.)

Ha 32-разрядных процессорах AMD Кб и выше Windows применяет специальную инструкцию syscall, которая функционирует аналогично х8б-ин-струкции sysenter; Windows записывает в регистр процессора, связанный с инструкцией syscall, адрес диспетчера системных сервисов ядра. Номер системного вызова передается в регистре EAX, а в стеке хранятся аргументы, предоставленные вызвавшим кодом. После диспетчеризации ядро выполняет инструкцию sysret.

При загрузке Windows распознает тип процессора, на котором она работает, и выбирает подходящий системный код. Этот код для NtReadFile в пользовательском режиме выглядит так:

ntdll!NtReadFile:

77f5bfa8 b8b7000000 mov eax,0xb7

77f5bfad ba0003fe7f mov edx,0x7ffe0300

77f5bfb2 ffd2 call edx

77f5bfb4 c22400 ret 0x24

Номер системного сервиса - 0xb7 (183 в десятичной форме), инструкция вызова выполняет код диспетчеризации системного сервиса, установленный ядром, который в данном примере находится по адресу 0x7ffe0300. Поскольку пример взят для Pentium M, используется sysenter.

SharedUserData!SystemCallStub: 7ffe0300 8bd4 mov edx,esp

7ffe0302 0f34 sysenter

7ffe0304 сЗ ret

Диспетчеризация 64-разрядных системных сервисов

B архитектуре x64 операционная система Windows использует инструкцию syscall, которая работает аналогично инструкции syscall на процессорах AMD Кб. Windows передает номер системного вызова в регистре EAX, первые четыре параметра в других регистрах, а остальные параметры (если они есть) в стеке:

B архитектуре IA64 для тех же целей применяется инструкция epc (Enter Privileged Mode). Первые восемь аргументов системного вызова передаются в регистрах, а остальное в стеке.

Диспетчеризация системных сервисов режима ядра

Как показано на рис. 3-14, ядро использует номер системного сервиса для поиска информации о нем в таблице диспетчеризации системных сервисов (system service dispatch table). Эта таблица похожа на описанную ранее таблицу IDT и отличается от нее тем, что каждый ее элемент содержит указатель на системный сервис, а не на процедуру обработки прерывания.

ПРИМЕЧАНИЕ Номера системных сервисов могут различаться в разных сервисных пакетах (service packs) - Microsoft время от времени добавляет или удаляет некоторые системные сервисы, а их номера генерируются автоматически при компиляции ядра.

Диспетчер системных сервисов, KiSystemService, копирует аргументы вызвавшего кода из стека потока пользовательского режима в свой стек режима ядра (поэтому вызвавший код не может изменить значения аргументов после того, как они переданы ядру) и выполняет системный сервис. Если переданные системному сервису аргументы содержат ссылки на буферы в пользовательском пространстве, код режима ядра проверяет возможность доступа к этим буферам, прежде чем копировать в них (или из них) данные.

Как будет показано в главе 6, у каждого потока есть указатель на таблицу системных сервисов. Windows располагает двумя встроенными таблицами системных сервисов, но поддерживает до четырех. Диспетчер системных сервисов определяет, в какой таблице содержится запрошенный сервис, интерпретируя 2-битное поле 32-битного номера системного сервиса как указатель на таблицу. Младшие 12 битов номера системного сервиса служат индексом внутри указанной таблицы. Эти поля показаны на рис. 3-15.

Таблицы дескрипторов сервисов

Главная таблица по умолчанию, KeServiceDescriptorTable, определяет базовые сервисы исполнительной системы, реализованные в Ntoskrnl.exe. Другая таблица, KeServiceDescriptorTableShadow, включает в себя сервисы USER и GDI, реализованные в Win32k.sys - той части подсистемы Windows, которая работает в режиме ядра. Когда Windows-поток впервые вызывает сервис USER или GDI, адрес таблицы системных сервисов потока меняется на адрес таблицы, содержащей сервисы USER и GDI. Функция KeAddSystemServiceTable позволяет Win32k.sys и другим драйверам добавлять новые таблицы системных сервисов. Если в Windows 2000 установлены службы Internet Information Services (IIS), их драйвер поддержки (Spud.sys) после загрузки определяет дополнительную таблицу сервисов. Так что после этого стороннее программное обеспечение может определить только одну дополнительную таблицу. Таблица сервисов, добавляемая KeAddSystemServiceTable (кроме таблицы Win32k.sys), копируется в таблицы KeServiceDescriptorTable и KeService-DescriptorTableSbadow. Windows поддерживает добавление лишь двух таблиц системных сервисов помимо главной и таблиц Win32.

ПРИМЕЧАНИЕ Windows Server 2003 Service Pack 1 и выше не поддерживает добавление таблиц системных сервисов, если не считать те, которые включаются Win32k.sys, так что этот способ не годится для расширения функциональности этой системы.

Инструкции для диспетчеризации сервисов исполнительной системы Windows содержатся в системной библиотеке Ntdll.dll. DLL-модули подсистем окружения вызывают функции из Ntdll.dll для реализации своих документированных функций. Исключением являются функции USER и GDI - здесь инструкции для диспетчеризации системных сервисов реализованы непосредственно в User32.dll и Gdi32.dll, а не в Ntdll.dll. Эти два случая иллюстрирует рис. 3-l6.

Как показано на рис. 3-l6, Windows-функция WriteFile в Kernel32.dll вызывает функцию NtWriteFile из Ntdll.dll. Она в свою очередь выполняет соответствующую инструкцию, вызывающую срабатывание ловушки системного сервиса и передающую номер системного сервиса NtWriteFile. Далее диспетчер системных сервисов (функция KiSystemService в Ntoskrnl.exe) вызывает истинную NtWriteFile для обработки запроса на ввод-вывод. Для функций USER и GDI диспетчер системных сервисов вызывает функции из Win32k.sys, той части подсистемы Windows, которая работает в режиме ядра.

ЭКСПЕРИМЕНТ: наблюдение за частотой вызова системных сервисов

Вы можете наблюдать за частотой вызова системных сервисов с помощью счетчика System Calls/Sec (Системных вызовов/сек) объекта System (Система). Откройте оснастку Performance (Производительность) и щелкните кнопку Add (Добавить), чтобы добавить на график счетчик. Выберите объект System и счетчик System Calls/Sec, затем щелкните кнопки Add и Close (Закрыть).

Диспетчер объектов

Как говорилось в главе 2, реализованная в Windows модель объектов позволяет получать согласованный и безопасный доступ к различным внутренним сервисам исполнительной системы. B этом разделе описывается диспетчер объектов (object manager) - компонент исполнительной системы, отвечающий за создание, удаление, защиту и отслеживание объектов.

ЭКСПЕРИМЕНТ: исследование диспетчера объектов

B этом разделе будут предлагаться эксперименты, которые покажут вам, как просмотреть базу данных диспетчера объектов. B них будут использоваться перечисленные ниже инструменты, которые вам нужно освоить (если вы их еще не освоили).

(o) Winobj можно скачать с сайта wwwsysinternals.com. Она показывает пространство имен диспетчера объектов. Другая версия этой утилиты есть в Platform SDK (\Program Files\Microsoft Platform SDK\Bin\ Winnt\Winobj.exe). Однако версия с wwwsysinternals.com сообщает более детальную информацию об объектах (например, счетчик ссылок, число открытых описателей, дескрипторы защиты и т. д.).

(o) Process Explorer и Handle. Отображают открытые описатели для процесса.

(o) Oh.exe (имеется в ресурсах Windows) выводит открытые описатели для процесса, но требует предварительной установки специального глобального флага.

(o) Команда Openfiles /query (в Windows XP и Windows Server 2003) отображает открытые описатели для процесса, но требует предварительной установки специального глобального флага.

(o) Команда !handle отладчика ядра отображает открытые описатели для процесса.

Средство просмотра объектов позволяет изучить пространство имен, поддерживаемое диспетчером объектов (имена есть не у всех объектов). Попробуйте запустить нашу версию утилиты WinObj и проанализировать полученный результат (см. иллюстрацию ниже).

Как уже отмечалось, утилита OH и команда Openfiles /query требуют установки глобального флага «поддержка списка объектов» (maintain objects list). (O глобальных флагах см. соответствующий раздел далее в этой главе.) OH установит этот флаг, если он еще не задан. Чтобы узнать, включен ли данный флаг, введите Openfiles /Local. Вы можете включить его командой Openfiles /Local ON. B любом случае нужно перезагрузить систему, чтобы новый параметр вступил в силу. Ни Process Explorer, ни Handle не требуют включения слежения за объектами, потому что для получения соответствующей информации они используют драйвер устройства.

При разработке диспетчера объекта был выдвинут ряд требований, в соответствии с которыми он должен:

(o) реализовать общий, унифицированный механизм использования системных ресурсов;

(o) изолировать защиту объектов в одном участке операционной системы для соответствия требованиям безопасности класса C2;

(o) предоставлять механизм учета использования объектов процессами, позволяющий устанавливать лимиты на выделение процессам системных ресурсов;

(o) поддерживать такую схему именования объектов, которая позволяла бы легко включать как существующие объекты (устройства, файлы и каталоги файловой системы), так и независимые наборы объектов;

(o) соответствовать требованиям различных подсистем окружения операционной системы - например, поддерживать наследование ресурсов родительских процессов дочерними (необходимо в Windows и POSIX) и имена файлов, чувствительные к регистру букв (требуется в POSIX);

(o) устанавливать единообразные правила сохранения объектов в памяти (т. е.

объект должен быть доступен, пока используется какими-либо процессами).

B Windows существует два вида внутренних объектов: объекты исполнительной системы (executive objects) и объекты ядра (kernel objects). Первые реализуются различными компонентами исполнительной системы (диспетчером процессов, диспетчером памяти, подсистемой ввода-вывода и т. д.). Вторые являются более примитивными объектами, которые реализуются ядром Windows. Эти объекты, невидимые коду пользовательского режима, создаются и используются только в исполнительной системе. Объекты ядра поддерживают фундаментальную функциональность (например, синхронизацию), на которую опираются объекты исполнительной системы. Так, многие объекты исполнительной системы содержат (инкапсулируют) один или несколько объектов ядра, как показано на рис. 3-17.

Структура объектов ядра и способы их применения для синхронизации других объектов будут рассмотрены в этой главе несколько позже. A сейчас мы сосредоточимся на принципах работы диспетчера объектов, а также на структуре объектов исполнительной системы, описателях и таблицах описателей. Вопросы, связанные с использованием этих объектов для управления доступом в Windows, здесь затрагиваются лишь вскользь - подробнее на эту тему см. главу 8.

Объекты исполнительной системы

Каждая подсистема окружения проецирует на свои приложения разные образы операционной системы. Объекты исполнительной системы и сервисы объектов - именно те примитивы, из которых подсистемы окружения конструируют собственные версии объектов и других ресурсов.

Как правило, объекты исполнительной системы создаются подсистемой окружения в интересах пользовательских приложений или компонентов операционной системы в процессе обычной работы. Так, для создания файла Windows-приложение вызывает Windows-функцию CreateFile, реализованную в DLL подсистемы Windows, Kernel32.dll. После проверки и инициализации CreateFile в свою очередь вызывает NtCreateFile, встроенный сервис Windows, для создания объекта «файл» исполнительной системы.

Набор объектов, предоставляемый приложениям подсистемой окружения, может быть больше или меньше того набора, который предоставляется исполнительной системой. Подсистема Windows использует объекты исполнительной системы для экспорта собственных объектов, многие из которых прямо соответствуют объектам исполнительной системы. Например, Windows-объекты «мьютекс и «семафор» основаны непосредственно на объектах исполнительной системы (которые в свою очередь базируются на соответствующих объектах ядра). Кроме того, подсистема Windows предоставляет именованные каналы и почтовые ящики - ресурсы, созданные на основе объектов «файл» исполнительной системы. Некоторые подсистемы вроде POSIX вообще не поддерживают объекты как таковые. Подсистема POSIX использует объекты и сервисы исполнительной системы просто как основу для POSIX-процессов, каналов и других ресурсов, которые она предоставляет своим приложениям.

B таблице 3-3 кратко описываются основные объекты, предоставляемые исполнительной системой. Подробнее об объектах исполнительной системы см. главы, в которых рассматриваются соответствующие компоненты исполнительной системы (а также справочную документацию Windows API, если речь идет об объектах исполнительной системы, напрямую экспортируемых в Windows).

ПРИМЕЧАНИЕ B Windows 2000 исполнительная система реализует в общей сложности 27 типов объектов, а в Windows XP и Windows Server 2003 - 29- (B эти более новые версии Windows добавлены объекты DebugObject и KeyedEvent.) Многие из таких объектов предназначены только для использования компонентами исполнительной системы, которая и определяет их. Эти объекты недоступны Windows API напрямую. Пример таких объектов - Driver, Device и EventPair.

ПРИМЕЧАНИЕ Мьютекс - это название объектов «мутант» (mutants) в Windows API; объект ядра, на котором основан мьютекс, имеет внутреннее имя мутант.

Структура объектов

Как показано на рис. 3-18, у каждого объекта есть заголовок и тело. Диспетчер объектов управляет заголовками объектов, а телами объектов управляют владеющие ими компоненты исполнительной системы. Кроме того, каждый заголовок объекта указывает на список процессов, которые открыли этот объект, и на специальный объект, называемый объектом типа (type object), - он содержит общую для всех экземпляров информацию.

Заголовки и тела объектов

Диспетчер объектов использует данные, хранящиеся в заголовке, для управления объектами независимо от их типа. Стандартные атрибуты заголовка кратко описываются в таблице 3-4.

Кроме заголовка у каждого объекта имеется тело, чей формат и содержимое уникальны для данного типа объектов; все объекты одного типа имеют одинаковый формат тела. Создавая тип объектов и предоставляя для него сервисы, компонент исполнительной системы может контролировать манипуляции с данными в телах всех объектов этого типа.

Диспетчер объектов предоставляет небольшой набор базовых сервисов, которые работают с атрибутами заголовка объекта и применимы к объектам любого типа (хотя некоторые базовые сервисы не имеют смысла для отдельных объектов). Эти сервисы, часть которых доступна Windows-приложениям через подсистему Windows, перечислены в таблице 3-5.

Базовые сервисы поддерживаются для всех типов объектов, но у каждого объекта есть свои сервисы для создания, открытия и запроса. Так, подсистема ввода-вывода реализует сервис создания файлов для объектов «файл», а диспетчер процессов - сервис создания процессов для объектов «процесс». Конечно, можно было бы реализовать единый сервис создания объектов, но подобная процедура оказалась бы весьма сложной, так как набор параметров, необходимых для инициализации объекта «файл», значительно отличается, скажем, от параметров инициализации объекта «процесс». A при вызове потоком сервиса объекта для определения его типа и обращении к соответствующей версии сервиса диспетчер объектов каждый раз сталкивался бы с необходимостью обработки дополнительных данных. B силу этих и иных причин сервисы, обеспечивающие создание, открытие и запросы, для каждого типа объектов реализованы отдельно.

Объекты типа

B заголовках объектов содержатся общие для всех объектов атрибуты, но их значения могут меняться у конкретных экземпляров данного объекта. Так, у каждого объекта есть уникальное имя и может быть уникальный дескриптор защиты. Однако некоторые атрибуты объектов остаются неизменными для всех объектов данного типа. Например, при открытии описателя объекта можно выбрать права доступа из набора, специфичного для объектов данного типа. Исполнительная система предоставляет в том числе атрибуты доступа «завершение» (terminate) и «приостановка» (suspend) для объектов «поток», а также «чтение» (read), «запись» (write), «дозапись» (append) и «удаление» (delete) для объектов «файл». Другой пример атрибута, специфичного для типа объектов, - синхронизация, о которой мы кратко расскажем ниже.

Чтобы сэкономить память, диспетчер объектов сохраняет статические атрибуты, специфичные для конкретного типа объектов, только при создании нового типа объектов. Для записи этих данных он использует собственный объект типа. Как показано на рис. 3-19, если установлен отладочный флаг отслеживания объектов (описываемый в разделе «Глобальные флаги Windows» далее в этой главе), все объекты одного типа (в данном случае - «процесс») связываются между собой с помощью объекта типа, что позволяет диспетчеру объектов при необходимости находить их и перечислять.

ЭКСПЕРИМЕНТ: просмотр заголовков объектов и объектов типа

Вы можете увидеть список объектов типа, объявленных диспетчеру объектов, с помощью утилиты Winobj. Далее в Winobj откройте каталог \ObjectTypes, как показано на следующей иллюстрации.

Чтобы просмотреть структуру данных типа объектов «процесс» в отладчике ядра, сначала идентифицируйте этот объект командой !process:

Этот вывод показывает, что структура типа включает имя типа объекта, счетчики активных объектов этого типа, а также счетчики пикового числа описателей и объектов данного типа. B поле TypeInfo хранится указатель на структуру данных, в которой содержатся атрибуты, общие для всех объектов этого типа, а также указатели на методы типа:

Объектами типов нельзя управлять из пользовательского режима из-за отсутствия соответствующих сервисов диспетчера объектов. Ho некоторые из определяемых ими атрибутов видимы через отдельные системные сервисы и функции Windows API. Атрибуты, хранящиеся в объектах типа, описываются в таблице 3-6.

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

Методы объекта

Атрибут «методы», последний из перечисленных в таблице 3-6, состоит из набора внутренних процедур, похожих на конструкторы и деструкторы C++, т. е. на процедуры, автоматически вызываемые при создании или уничтожении объекта. B диспетчере объектов эта идея получила дальнейшее развитие: он вызывает методы объекта и в других ситуациях, например при открытии или закрытии описателя объекта или при попытке изменения параметров защиты объекта. B некоторых типах объектов методы определяются в зависимости от того, как должен использоваться данный тип объектов.

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

Диспетчер объектов вызывает метод open всякий раз, когда создает описатель объекта (что происходит при создании или открытии объекта). Однако метод open определен только в одном типе объектов - WindowStation. Этот метод необходим таким объектам для того, чтобы Win32k.sys мог использовать часть памяти совместно с процессом, который служит в качестве пула памяти, связанного с объектом «рабочий стол».

Пример использования метода close можно найти в подсистеме ввода-вывода. Диспетчер ввода-вывода регистрирует метод close для объектов типа «файл», а диспетчер объектов вызывает метод close при закрытии каждого описателя объекта этого типа. Метод close проверяет, не осталось ли у процесса, закрывающего описатель файла, каких-либо блокировок для файла, и, если таковые есть, снимает их. Диспетчер объектов не может и не должен самостоятельно проверять наличие блокировок для файла.

Перед удалением временного объекта из памяти диспетчер объектов вызывает метод delete, если он зарегистрирован. Например, диспетчер памяти регистрирует для объектов типа «раздел» метод delete, освобождающий физические страницы, используемые данным разделом. Перед удалением объекта «раздел» этот метод также проверяет различные внутренние структуры данных, выделенные для раздела диспетчером памяти. Диспетчер объектов не мог бы сделать эту работу, поскольку он ничего не знает о внутреннем устройстве диспетчера памяти. Методы delete других объектов выполняют аналогичные функции.

Если диспетчер объектов находит существующий вне его пространства имен объект, метод parse (по аналогии с методом query name) позволяет этому диспетчеру передавать управление вторичному диспетчеру объектов. Когда диспетчер объектов анализирует имя объекта, он приостанавливает анализ, встретив объект с сопоставленным методом parse, и вызывает метод parse, передавая ему оставшуюся часть строки с именем объекта. Кроме пространства имен диспетчера объектов, в Windows есть еще два пространства имен: реестра (реализуемое диспетчером конфигурации) и файловой системы (реализуемое диспетчером ввода-вывода через драйверы файловой системы). O диспетчере конфигурации см. главу 5; о диспетчере ввода-вывода и драйверах файловой системы см. главу 9.

Например, когда процесс открывает описатель объекта с именем \Device\ Floppy0\docs\resume.doc, диспетчер объектов просматривает свое дерево имен и ищет объект с именем FloppyO. Обнаружив, что с этим объектом сопоставлен метод parse, он вызывает его, передавая ему остаток строки с именем объекта (в данном случае - строку \docs\resume.doc). Метод parse объектов «устройство» (device objects) является процедурой ввода-вывода, которая регистрируется диспетчером ввода-вывода при определении типа объекта «устройство». Процедура parse диспетчера ввода-вывода принимает строку с именем и передает ее соответствующей файловой системе, которая ищет файл на диске и открывает его.

Подсистема ввода-вывода также использует метод security, аналогичный методу parse. Он вызывается каждый раз, когда поток пытается запросить или изменить параметры защиты файла. Эта информация для файлов отличается от таковой для других объектов, поскольку хранится в самом файле, а не в памяти. Поэтому для поиска, считывания или изменения параметров защиты нужно обращаться к подсистеме ввода-вывода.

Описатели объектов и таблица описателей, принадлежащая процессу

Когда процесс создает или открывает объект по имени, он получает описатель (handle), который дает ему доступ к объекту. Ссылка на объект по описателю работает быстрее, чем по имени, так как при этом диспетчер объектов может сразу найти объект, не просматривая список имен. Процессы также могут получать описатели объектов, во-первых, путем их наследования в момент своего создания (если процесс-создатель разрешает это, указывая соответствующий флаг при вызове CreateProcess, и если описатель помечен как наследуемый либо в момент создания, либо позднее, вызовом Windows-функции SetHandleInformation), а во-вторых, за счет приема дублированного описателя от другого процесса (см. описание Windows-функции DuplicateHandle).

Чтобы потоки процесса пользовательского режима могли оперировать объектом, им нужен описатель этого объекта. Идея применения описателей для управления ресурсами сама по себе не нова. Например, стандартные библиотеки языков С, Pascal (и других) при открытии файла возвращают его описатель. Описатели служат косвенными указателями на системные ресурсы, что позволяет прикладным программам избегать прямого взаимодействия с системными структурами данных.

ПРИМЕЧАНИЕ Компоненты исполнительной системы и драйверы устройств могут обращаться к объектам напрямую, поскольку выполняются в режиме ядра и ввиду этого имеют доступ к структурам объектов в системной памяти. Однако они должны объявлять о своем использовании объектов, увеличивая значение счетчика ссылок, что гарантирует сохранность нужного объекта (см. раздел «Хранение объектов в памяти» далее в этой главе).

Описатели дают и другие преимущества. Во-первых, описатели файлов, событий или процессов совершенно одинаковы - просто ссылаются на разные объекты. Это дает возможность создать унифицированный интерфейс для ссылок на объекты любого типа. Во-вторых, только диспетчер объектов имеет право физически создавать описатели и искать их объекты, а значит, он может проанализировать любое действие с объектом в пользовательском режиме и решить, позволяет ли профиль защиты вызывающей программы выполнить над объектом запрошенную операцию.

ЭКСПЕРИМЕНТ: просмотр открытых описателей

Запустите Process Explorer и убедитесь, что в его окне нижняя секция включена и настроена на отображение открытых описателей. (Выберите View, Lower Pane View и Handles.) Затем откройте окно командной строки и просмотрите таблицу описателей для нового процесса Cmd.exe. Вы должны увидеть открытый описатель файла - текущего каталога. Например, если текущий каталог - C:\, Process Explorer выводит следующее.

Если вы теперь смените текущий каталог командой CD, то Process Explorer покажет, что описатель предыдущего каталога закрыт и открыт описатель нового текущего каталога. Предыдущий описатель ненадолго выделяется красным цветом, а новый - зеленым. Длительность подсветки настраивается щелчком кнопки Options и регулировкой параметра Difference Highlight Duration.

Такая функциональность Process Explorer упрощает наблюдение за изменениями в таблице описателей. Например, если в процессе происходит утечка описателей, просмотр таблицы описателей в Process Explorer позволяет быстро увидеть, какой описатель (или описатели) открывается, но не закрывается. Эта информация помогает программисту обнаружить утечку описателей.

Таблицу открытых описателей также можно вывести, используя командную строку утилиты Handle. Например, обратите внимание на следующий фрагмент вывода Handle, где показана таблица описателей для процесса Cmd.exe до и после смены каталога:

Описатель объекта представляет собой индекс в таблице описателей, принадлежащей процессу. Ha нее указывает блок процесса исполнительной системы (EPROCESS), рассматриваемый в главе 6. Индекс первого описателя равен 4, второго - 8 и т. д. Таблица содержит указатели на все объекты, описатели которых открыты данным процессом. Эти таблицы реализованы по трехуровневой схеме аналогично тому, как блок управления памятью в системах типа x86 реализует трансляцию виртуальных адресов в физические, поддерживая более 16 000 000 описателей на каждый процесс (см. главу 7).

При создании процесса в Windows 2000 диспетчер объектов формирует верхний уровень таблицы описателей, содержащий указатели на таблицы среднего уровня; средний уровень содержит первый массив указателей на таблицы вторичных описателей, а нижний - первую таблицу вторичных описателей. Ha рис. 3-20 показана структура таблицы описателей в Windows 2000. B этой операционной системе диспетчер объектов интерпретирует младшие 24 бита описателя объекта как три 8-битных поля, являющиеся индексами для каждого из трех уровней таблицы описателей. B Windows XP и Windows Server 2003 при создании процесса создается лишь таблица описателей нижнего уровня - остальные уровни формируются по мере необходимости. B Windows 2000 таблица вторичных описателей состоит из 255 элементов. B Windows XP и Windows Server 2003 такая же таблица включает столько элементов, сколько помещается на страницу памяти минус один элемент, который используется для аудита описателей (handle auditing). Например, на х86-системах размер страницы составляет 4096 байтов. Делим это значение на размер элемента (8 байтов), вычитаем 1 и получаем всего 511 элементов в таблице описателей нижнего уровня. Наконец, таблица описателей среднего уровня в Windows XP и Windows Server 2003 содержит полную страницу указателей на таблицы вторичных описателей, поэтому количество таблиц вторичных описателей зависит от размеров страницы и указателя на конкретной аппаратной платформе.

ЭКСПЕРИМЕНТ: создание максимального количества описателей

Тестовая программа Testlimit ( www.sysmternals.com/windowsintemals.sbtml) позволяет открывать описатели объекта до тех пор, пока не будет исчерпан их лимит. C ее помощью вы увидите, сколько описателей можно создать в одном процессе в вашей системе. Поскольку память под таблицу описателей выделяется из пула подкачиваемых страниц, этот пул может быть исчерпан до того, как вы достигнете предельного числа описателей, допустимых в одном процессе.

1. Скачайте файл Testlimit.zip по только что упомянутой ссылке и раз-архивируйте его в какой-нибудь каталог.

2. Запустите Process Explorer, щелкните View, затем System Information. Обратите внимание на текущий и максимальный размеры пула подкачиваемой памяти. (Для вывода максимальных размеров пулов Process Explorer нужно настроить на доступ к символам для образа ядра, Ntoskrnl.exe.) Пусть эта информация отображается, когда вы запустите программу Testlimit.

3. Откройте окно командной строки.

4. Запустите программу Testlimit с ключом ~h (для этого введите testlimit -h). Когда Testlimit не удастся открыть очередной описатель, она сообщит общее число созданных ею описателей. Если это значение окажется меньше, чем около 16 миллионов, то, вероятно, вы исчерпали пул подкачиваемой памяти до достижения теоретически предельного числа описателей.

5. Закройте окно командной строки; это приведет к завершению процесса Testlimit и автоматическому закрытию всех открытых описателей.

Как показано на рис. 3-21, в х8б-системах каждый элемент таблицы описателей состоит из структуры с двумя 32-битными элементами: указателем на объект (с флагами) и маской доступа. B б4-разрядных системах каждый элемент имеет размер 12 байтов и состоит из 64-битного указателя на заголовок объекта и 32-битной маски доступа (маски доступа описываются в главе 8).

B Windows 2000 первый 32-битный элемент содержит указатель на заголовок объекта и четыре флага. Поскольку заголовки объектов всегда выравниваются по границе 8 байтов, в качестве флагов используются младшие 3 бита и один старший. Старший бит является флагом блокировки (lock). Когда диспетчер объектов транслирует описатель в указатель на объект, он блокирует соответствующую запись на время трансляции. Так как все объекты находятся в системном адресном пространстве, старший бит указателя на объект устанавливается в 1. (Гарантируется, что адреса всегда будут превышать 0x80000000 даже на системах, запускаемых с ключом загрузки /3GB.) Так что, пока элемент таблицы описателей не заблокирован, диспетчер объектов может сбросить старший бит в 0. При блокировке элемента таблицы диспетчер объектов устанавливает этот бит и получает корректное значение указателя. Диспетчер блокирует всю таблицу описателей (используя специальную блокировку, сопоставляемую с каждым процессом), только если процесс создает новый описатель или закрывает существующий. B Windows XP и Windows Server 2003 флаг блокировки хранится в младшем бите указателя на объект. A флаг, который в Windows 2000 хранился в этом бите, теперь хранится в ранее зарезервированном бите маски доступа.

Первый флаг указывает, имеет ли право вызывающая программа закрывать данный описатель. Второй определяет, получат ли процессы, созданные данным процессом, копию этого описателя. (Как уже отмечалось, наследование описателя можно указать при его создании или позже, через Windows-функцию SetHandleInformation. Этот флаг тоже можно задать вызовом Set-HandleInformation) Третий задает, будет ли генерироваться сообщение аудита при закрытии объекта. (Этот флаг не предоставляется в Windows и предназначен для внутреннего использования диспетчером объектов.)

Системным компонентам и драйверам устройств зачастую нужно открывать описатели объектов, доступа к которым у приложений пользовательского режима нет. Это достигается созданием описателей в таблице описателей ядра (kernel handle table) (внутреннее имя - ObpKernelHandleTable). Описатели в этой таблице доступны только в режиме ядра в контексте любого процесса. Это значит, что функции режима ядра могут ссылаться на эти описатели из контекста любого процесса без ущерба для производительности. Диспетчер объектов распознает ссылки на описатели в таблице описателей ядра, когда старший бит в них установлен, т. е. когда в таких ссылках содержатся значения, превышающие 0x80000000. B Windows 2000 таблица описателей ядра является независимой таблицей описателей, но в Windows XP и Windows Server 2003 она служит и таблицей описателей для процесса System.

ЭКСПЕРИМЕНТ: просмотр таблицы описателей с помощью отладчика ядра

Команда !bandle отладчика ядра допускает следующие аргументы:

!handle ‹индекс_описателя› ‹флаги› ‹идентификатор_процесса›

Индекс описателя определяет элемент в таблице описателей (0 - вывод всех описателей). Индекс первого описателя равен 4, второго - 8 и т. д. Например, введя !bandle 4, вы увидите первый описатель в текущем процессе.

Вы можете указывать флаги, являющиеся битовыми масками, где бит 0 означает, что нужно вывести лишь информацию из элемента таблицы, бит 1 - показать не только используемые, но и свободные описатели, а бит 2 - сообщить информацию об объекте, на который ссылается описатель. Следующая команда выводит полную информацию о таблице описателей в процессе с идентификатором 0x408.

Защита объектов

Открывая файл, нужно указать, для чего это делается - для чтения или записи. Если вы попытаетесь записать что-нибудь в файл, открытый для чтения, то получите ошибку. Аналогичным образом действует и исполнительная система: когда процесс создает объект или открывает описатель существующего объекта, он должен указывать набор желательных прав доступа (desired access rights), сообщая тем самым, что именно он собирается делать с объектом. Процесс может запросить либо набор стандартных прав доступа (чтение, запись, выполнение), применимых ко всем объектам, либо специфические права доступа, различные для объектов разного типа. Так, в случае объекта «файл» процесс может запросить права на удаление файла или дозапись, а в случае объекта «поток» - права на приостановку потока или его завершение.

Когда процесс открывает описатель объекта, диспетчер объектов вызывает так называемый монитор состояния защиты* (security reference monitor), часть подсистемы защиты, работающую в режиме ядра, и посылает ему уведомление о наборе желательных для процесса прав доступа. Монитор состояния защиты проверяет, разрешает ли дескриптор защиты объекта запрашиваемый тип доступа. Если да, монитор состояния защиты возвращает процессу набор предоставленных прав доступа (granted access rights), информацию о которых диспетчер объектов сохраняет в создаваемом им описателе объекта. Как подсистема защиты определяет, кто и к каким объектам может получать доступ, рассматривается в главе 8.

* Ha самом деле этот компонент представляет собой нечто вроде монитора запросов к подсистеме защиты. - Прим. перев.

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

Хранение объектов в памяти

Объекты бывают двух типов: временные (temporary) и постоянные (permanent). Большинство объектов временные, т. е. они хранятся, пока используются, и освобождаются, как только необходимость в них отпадает. Постоянные объекты существуют до тех пор, пока они не освобождаются явным образом. Поскольку большинство объектов временные, остальная часть этого раздела будет посвящена тому, как диспетчер объектов реализует хранение объектов в памяти (object retention), т. е. сохранение временных объектов лишь до тех пор, пока они используются, с их последующим удалением. Так как для доступа к объекту все процессы пользовательского режима должны сначала открыть его описатель, диспетчер объектов может легко отслеживать, сколько процессов и даже какие именно из них используют объект. Учет описателей является одним из механизмов, реализующих хранение объектов в памяти. Этот механизм двухфазный. Первая фаза называется хранением имен (name retention) и контролируется числом открытых описателей объекта. Каждый раз, когда процесс открывает описатель объекта, диспетчер увеличивает значение счетчика открытых описателей в заголовке объекта. По мере того как процессы завершают использование объекта и закрывают его описатели, диспетчер уменьшает значение этого счетчика. Когда счетчик обнуляется, диспетчер удаляет имя объекта из своего глобального пространства имен. После этого новые процессы уже не смогут открывать описатели данного объекта.

Вторая фаза заключается в том, что прекращается хранение тех объектов, которые больше не используются (т. е. они удаляются). Так как код операционной системы обычно обращается к объектам по указателям, а не описателям, диспетчер объектов должен регистрировать и число указателей объектов, переданных процессам операционной системы. При каждой выдаче указателя на объект он увеличивает значение счетчика ссылок на объект. Компоненты режима ядра, прекратив использовать указатель, вызывают диспетчер объектов для уменьшения счетчика ссылок. Система также увеличивает счетчик ссылок при увеличении счетчика описателей, а при уменьшении счетчика описателей соответственно уменьшает счетчик ссылок, поскольку описатель тоже является подлежащей учету ссылкой на объект (подробнее об этих механизмах см. описание функции ObReferenceObjectByPointer или ObDere-ferenceObject в DDK).

Ha рис. 3-22 показаны два задействованных объекта-события. Процесс A открыл первый объект, а процесс B - оба объекта. Кроме того, на первый объект ссылается какая-то структура режима ядра, и его счетчик ссылок pa-

вен 3. Так что, даже если оба процесса закроют свои описатели первого объекта, он по-прежнему будет существовать, поскольку его счетчик ссылок еще не обнулится. Ho, когда процесс B закроет свой описатель второго объекта, этот объект будет удален.

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

Такой механизм позволяет хранить объект и его имя в памяти, просто не закрывая его описатель. Программистам, создающим приложения с двумя и более взаимодействующими процессами, не приходится беспокоиться о том, что один из процессов удалит объект в то время, когда он еще используется другим процессом. Кроме того, закрытие описателей объекта, принадлежащих приложению, еще не означает, что этот объект будет немедленно удален, - он может использоваться операционной системой. Например, какой-то процесс создает второй процесс для выполнения программы в фоновом режиме, после чего немедленно закрывает описатель созданного процесса. Так как второй процесс еще не закончил свою работу и выполняется операционной системой, она поддерживает ссылку на этот объект «процесс». Диспетчер сможет обнулить счетчик ссылок на второй процесс и удалить его, только когда завершится выполнение этого процесса.

Учет ресурсов

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

Bo многих операционных системах для ограничения доступа процессов к системным ресурсам применяется система квот. Однако типы устанавливаемых для процессов квот иногда весьма разнообразны, а отслеживающий квоты код распределен по всей операционной системе. Так, в некоторых операционных системах компонент ввода-вывода может регистрировать и ограничивать число файлов, которые может открыть процесс, а компонент управления памятью может накладывать ограничения на объем памяти, выделяемой потокам процесса. Компонент, отвечающий за управление процессами, способен ограничивать максимальное число новых процессов или новых потоков процесса. Каждое из этих ограничений отслеживается и реализуется в различных частях операционной системы.

Диспетчер объектов Windows, напротив, представляет собой компонент централизованного учета ресурсов. B заголовке каждого объекта содержится атрибут квоты, определяющий, насколько диспетчер объектов уменьшает квоту подкачиваемой или неподкачиваемой памяти процесса при открытии его потоком описателя этого объекта.

У каждого процесса в Windows имеется структура квот, регистрирующая лимиты и текущее количество используемой памяти из подкачиваемого и неподкачиваемого пулов, а также из страничного файла. (Введите dt nt!_ EPROCESS_QUOTA_ENTRY в отладчике ядра, чтобы увидеть формат этой структуры.) Значения данных квот по умолчанию равны 0 (ограничений нет), но их можно указать, модифицировав параметры в реестре (см. параметры NonPagedPoolQuota, PagedPoolQuota и PagingFileQuota в разделе HKLM\System\CurrentControlSet\Session Manager\Memory Management). Заметьте, что все процессы в интерактивном сеансе используют один и тот же блок квот (документированного способа создания процессов с собственными блоками квот нет).

Имена объектов

Важное условие для создания множества объектов - эффективная система учета. Для учета диспетчеру объектов нужна следующая информация:

(o) способ, которым можно было бы отличать один объект от другого;

(o) метод поиска и получения конкретного объекта.

Первое требование реализуется за счет присвоения имен объектам. Это расширение обычной для большинства операционных систем функциональности, в которых отдельным системным ресурсам, например файлам, каналам или блокам разделяемой памяти, можно присваивать имена. Исполнительная система, напротив, позволяет именовать любой объект, представляющий ресурс. Второе требование (поиск и получение объектов) также реализуется через именование объектов. Если диспетчер хранит объекты в соответствии с их именами, он может быстро найти объект по его имени.

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

Для большей эффективности диспетчер объектов не ищет имя объекта при каждой попытке его использования. Поиск по имени ведется только в двух случаях. Во-первых, при создании процессом именованного объекта: перед тем как сохранить имя объекта в глобальном пространстве имен, диспетчер проверяет, нет ли в нем такого же имени. Во-вторых, открывая описатель именованного объекта, диспетчер ищет объект по имени и возвращает его описатель, который затем используется для ссылки на объект. Диспетчер позволяет выбирать, надо ли при поиске учитывать регистр букв. Эта функциональность поддерживается POSIX и другими подсистемами окружения, в которых имена файлов чувствительны к регистру букв.

Где именно хранятся имена объектов, зависит от типа объектов. B таблице 3-8 перечислены стандартные каталоги объектов, имеющиеся на всех системах под управлением Windows. Пользовательским программам видны только каталоги \BaseNamedObjects и \GLOBAL?? (\?? в Windows 2000).

Поскольку имена базовых объектов ядра вроде мьютексов, событий, семафоров, ожидаемых таймеров и разделов хранятся в одном каталоге, они не должны совпадать, даже если относятся к объектам разных типов. Это ограничение подчеркивает, насколько осторожно надо выбирать имена, чтобы они не конфликтовали с другими (используйте, например, префиксы имен в виде названия вашей компании и программного продукта).

Имена объектов глобальны в пределах компьютера (или всех процессоров на многопроцессорной системе) и невидимы через сеть. Однако метод parse диспетчера объектов позволяет получать доступ к именованным объектам, существующим на других компьютерах. Так, диспетчер ввода-вывода, предоставляющий сервисы объектов «файл», расширяет функции диспетчера объектов для работы с файлами на удаленных компьютерах. При запросе на открытие объекта «файл» на удаленном компьютере диспетчер объектов вызывает метод parse, что позволяет диспетчеру ввода-вывода перехватить запрос и направить его сетевому редиректору - драйверу, обращающемуся к файлам через сеть. Серверный код на удаленной Windows-системе вызывает диспетчер объектов и диспетчер ввода-вывода на этой системе для поиска нужного объекта «файл» и возврата данных через сеть.

ЭКСПЕРИМЕНТ: просмотр именованных базовых объектов

Список именованных базовых объектов можно просмотреть с помощью утилиты Winobj. Запустите Winobj.exe и щелкните каталог \BaseNamedObjects, как показано ниже.

Именованные объекты отображаются справа. Тип объектов обозначается следующими значками:

(o) «stop» - мьютексы;

(o) в виде микросхем памяти - разделы (объекты «проекция файла»);

(o) в виде восклицательного знака - события;

(o) похожие на светофоры - семафоры;

(o) в виде изогнутой стрелки - символьные ссылки.

Объекты «каталоги объектов» (object directory objects)

C помощью этих объектов диспетчер объектов поддерживает иерархическую структуру пространства имен. Этот объект аналогичен каталогу файловой системы и содержит имена других объектов, а также другие каталоги объектов. Он включает информацию, достаточную для трансляции имен объектов в указатели на сами объекты. Диспетчер использует указатели для создания описателей объектов, возвращаемых программам пользовательского режима. Каталоги для хранения объектов могут создаваться как кодом режима ядра (включая компоненты исполнительной системы и драйверы устройств), так и кодом пользовательского режима (в том числе подсистемами). Например, диспетчер ввода-вывода создает каталог объектов \Device с именами объектов, представляющих устройства ввода-вывода.

Символьные ссылки (symbolic links)

B некоторых файловых системах (например, NTFS и отдельных UNIX-системах) с помощью символьной ссылки можно создать имя файла или каталога, которое при использовании будет транслироваться операционной системой в другое имя файла или каталога. Символьные ссылки - простой метод неявного разделения файлов или каталогов за счет создания перекрестных ссылок между различными каталогами в обычной иерархической структуре каталогов.

Диспетчер объектов реализует объект «символьная ссылка», который выполняет аналогичную функцию в отношении имен объектов в пространстве имен. Символьная ссылка может находиться в любом месте строки с именем объекта. Когда вызывающая программа ссылается на имя объекта «символьная ссылка», диспетчер просматривает пространство имен в поисках такого объекта. Далее он анализирует содержимое символьной ссылки и находит строку, которую надо подставить вместо ссылки. После этого начинается поиск другого объекта, соответствующего полученному имени.

Исполнительная система использует такие объекты при трансляции имен устройств в стиле MS-DOS во внутренние имена устройств Windows. Пользователь обращается к гибким и жестким дискам по именам A:, B:, С: и т. д. или к последовательным портам по именам COMl, COM2 и т. п. Подсистема Windows делает эти объекты «символьная ссылка» в защищенные глобальные данные, помещая их в каталог объектов \?? (в Windows 2000) или \GLOBAL?? (в Windows XP и Windows Server 2003).

Пространство имен сеанса

Windows NT изначально создавалась в расчете на регистрацию в системе одного интерактивного пользователя и выполнение лишь одного экземпляра любого из интерактивных приложений. Добавление Windows Terminal Services в Windows 2000 Server и поддержки быстрого переключения пользователей в Windows XP потребовало некоторых изменений в модели пространства имен диспетчера объектов для поддержки множества интерактивных пользователей одновременно. (Базовые сведения о службах терминала и сеансах см. в главе 1.)

Пользователь, зарегистрированный в консольном сеансе, получает доступ к глобальному пространству имен, которое является первым экземпляром пространства имен. Дополнительные сеансы получают свое (закрытое) представление пространства имен, называемое локальным пространством имен. Части пространства имен, локальные для каждого сеанса, включают \Dos-Devices, \Windows и \BaseNamedObjects. Формирование раздельных копий одних и тех же частей называется созданием экземпляров (instancing) пространства имен. Создание экземпляров каталога \DosDevices позволяет каждому пользователю обозначать сетевые дисковые устройства разными буквами и по-разному именовать такие объекты, как, например, последовательные порты. B Windows 2000 глобальный каталог \DosDevices называется \?? и является каталогом, на который указывает символьная ссылка \DosDevices, а локальные каталоги \DosDevices идентифицируются по идентификатору для сеанса сервера терминала. B Windows XP и более поздних операционных системах глобальный каталог \DosDevices называется \Global?? и является каталогом, на который указывает \DosDevices, а локальные каталоги \DosDevices определяются по идентификатору сеанса входа (logon session).

Win32k.sys создает в каталоге \Windows интерактивный oбъeктWindow-Station, \WinSta0. Среда Terminal Services может поддерживать несколько интерактивных пользователей, но для сохранения иллюзии доступа к предопределенному интерактивному объекту WindowStation в Windows каждому пользователю нужна собственная версия WinSta0. Наконец, в каталоге \BaseNamedObjects приложения и система создают разделяемые объекты, включая события, мьютексы и разделы. Если приложение, создающее именованный объект, запущено двумя пользователями, то в каждом сеансе нужна своя версия этого объекта, чтобы два экземпляра приложения не мешали друг другу, обращаясь к одному объекту.

Диспетчер объектов реализует локальное пространство имен, создавая закрытые версии трех каталогов, которые находятся в каталоге, сопоставленном с сеансом пользователя (\Sessions\X, где X - идентификатор сеанса). Например, когда некое Windows-приложение во время удаленного сеанса номер 2 создает именованное событие, диспетчер объектов перенаправляет имя этого объекта из \BaseNamedObjects в \Sessions\2\BaseNamedObjects.

Все функции диспетчера объектов, связанные с управлением пространством имен, знают о локальных экземплярах каталогов и участвуют в поддержании иллюзии того, что в удаленных сеансах используется то же пространство имен, что и в консольных. DLL-модули подсистемы Windows добавляют к именам, передаваемым Windows-приложениями, которые ссылаются на объекты в \DosDevices, префиксы \?? (например, C:\Windows превращается в \??\C:\Windows). Когда диспетчер объектов обнаруживает специальный префикс \??, предпринимаемые им действия зависят от версии Windows, но при этом он всегда полагается на поле DeviceMap в объекте «процесс», создаваемом исполнительной системой (executive process object) (EPROCESS, о котором пойдет речь в главе 6). Это поле указывает на структуру данных, разделяемую с другими процессами в том же сеансе. Поле DosDevicesDirectory структуры DeviceMap указывает на каталог диспетчера объектов, представляющий локальный \DosDevices процесса. Целевой каталог зависит от конкретной системы.

(o) Если системой является Windows 2000 и Terminal Services не установлены, поле DosDevicesDirectory в структуре DeviceMap процесса указывает на каталог \??, так как локальных пространств имен нет.

(o) Если системой является Windows 2000 и Terminal Services установлены, то, когда активным становится новый сеанс, система копирует все объекты из глобального каталога \?? в локальный для сеанса каталог \DosDevices, и поле DosDevicesDirectory структуры DeviceMap указывает на этот локальный каталог.

(o) B Windows XP и Windows Server 2003 система не копирует глобальные объекты в локальные каталоги DosDevices. Диспетчер объектов, встретив ссылку на \??, находит локальный для процесса каталог \DosDevices, используя поле DosDevicesDirectory структуры DeviceMap. Если нужного объекта в этом каталоге нет, он проверяет поле DeviceMap объекта «каталог» и, если это допустимо, ищет объект в каталоге, на который указывает поле GlobalDosDevicesDirectory структуры DeviceMap. Этим каталогом всегда является \Global??.

B определенных обстоятельствах приложениям, поддерживающим Terminal Services, нужен доступ к объектам в консольном сеансе, даже если сами приложения выполняются в удаленном сеансе. Это может понадобиться приложениям для синхронизации со своими экземплярами, выполняемыми в других удаленных или консольных сеансах. B таких случаях для доступа к глобальному пространству имен приложения могут использовать специальный префикс \Global, поддерживаемый диспетчером объектов. Так, объект \Global\ApplicationInitialized, открываемый приложением в сеансе номер 2, направляется вместо каталога \Sessions\2\BaseNamedObjects\Application-Initialized в каталог \BasedNamedObjects\ApplicationInitialized.

B Windows XP и Windows Server 2003 приложение, которому нужно обратиться к объекту в глобальном каталоге \DosDevices, не требуется использовать префикс \Global, если только этого объекта нет в локальном каталоге \DosDevices. Это вызвано тем, что диспетчер объектов автоматически ищет объект в глобальном каталоге, не найдя его в локальном. Однако при-

ложение, работающее в Windows 2000 с Terminal Services, должно всегда указывать префикс \Global для доступа к объектам в глобальном каталоге \Dos-Devices.

ЭКСПЕРИМЕНТ: просмотр экземпляров пространства имен

Вы можете увидеть, как диспетчер объектов создает экземпляры пространства имен, создав сеанс, отличный от консольного, и просмотрев таблицу описателей для процесса в этом сеансе. B Windows XP Home Edition или Windows XP Professional в системе, которая не входит в домен, отключите консольный сеанс [откройте меню Start (Пуск), щелкните Log Off (Выход из системы) и выберите Disconnect and Switch User (Смена пользователя) или нажмите комбинацию клавиш Win-dows+L]. Теперь войдите в систему под новой учетной записью. Если вы работаете с Windows 2000 Server, Advanced Server или Datacenter Server, запустите клиент Terminal Services, подключитесь к серверу и войдите в систему.

Войдя в систему в новом сеансе, запустите Winobj, щелкните каталог \Sessions и вы увидите подкаталог с числовым именем для каждого активного удаленного сеанса. Открыв один из таких каталогов, вы обнаружите подкаталоги \DosDevices, \Windows и \Base-NamedObjects, которые относятся к локальному пространству имен сеанса. Одно из таких локальных пространств имен показано на иллюстрации ниже.

Далее запустите Process Explorer и выберите какой-нибудь процесс в новом сеансе (вроде Explorer.exe). Просмотрите таблицу описателей, щелкнув View, Lower Pane View и Handles. Вы должны увидеть описатель \Windows\Windowstations\WinStaO под \Sessions\n, где n - идентификатор сеанса. Объекты с глобальными именами появятся в \Ses-sions\n\BaseNamedObjects.

Синхронизация

Концепция взаимоисключения (mutual exclusion) является одной из ключевых при разработке операционных систем. Ee смысл в следующем: в каждый момент к конкретному ресурсу может обращаться один - и только один - поток. Взаимоисключение необходимо, когда ресурс не предназначен для разделения или когда такое разделение может иметь непредсказуемые последствия. Например, если бы два потока одновременно копировали данные в порт принтера, отпечатанный документ представлял бы собой нечитаемую мешанину. Аналогичным образом, если бы один поток считывал какой-то участок памяти, когда другой записывал бы туда данные, первый поток получил бы непредсказуемый набор данных. B общем случае доступные для записи ресурсы нельзя разделять без ограничений. Рис. 3-23 иллюстрирует, что происходит, когда два потока, выполняемые на разных процессорах, одновременно записывают данные в циклическую очередь.

Поскольку второй поток получил значение указателя на конец очереди до того, как первый поток завершил его обновление, второй вставил свои данные в то же место, что и первый. Таким образом, данные первого потока были перезаписаны другими данными, а один участок очереди остался пустым. Хотя рис. 3-23 иллюстрирует, что могло бы случиться в многопроцессорной системе, аналогичную ошибку было бы нельзя исключить и в однопроцессорной системе - при переключении контекста на второй поток до того, как первый поток успел бы обновить указатель на конец очереди.

Разделы кода, обращающиеся к неразделяемым ресурсам, называются критическими секциями (critical sections). B критической секции единовременно может выполняться только один поток. Пока один поток записывает в файл, обновляет базу данных или модифицирует общую переменную, доступ к этому ресурсу со стороны других потоков запрещен. Псевдокод, показанный на рис. 3-23, представляет собой критическую секцию, которая некорректно обращается к разделяемой структуре данных без взаимоисключения.

Взаимоисключение, важное для всех операционных систем, особенно значимо (и запутанно) в случае операционной системы с жестко связанной симметричной мультипроцессорной обработкой (tightly-coupled symmetric multiprocessing), например в Windows, в которой один и тот же системный код, выполняемый на нескольких процессорах одновременно, разделяет некоторые структуры данных, хранящиеся в глобальной памяти. B Windows поддержка механизмов, с помощью которых системный код может предотвратить одновременное изменение двумя потоками одной и той же структуры, возлагается на ядро. Оно предоставляет специальные примитивы взаимоисключения, используемые им и остальными компонентами исполнительной системы для синхронизации доступа к глобальным структурам данных.

Так как планировщик синхронизирует доступ к своим структурам данных при IRQL уровня «DPC/dispatch», ядро и исполнительная система не могут полагаться на механизмы синхронизации, которые могли бы привести к ошибке страницы или к перераспределению процессорного времени при IRQL уровня «DPC/dispatch» или выше (эти уровни также известны под названием «высокий IRQL»). Из следующих разделов вы узнаете, как ядро и исполнительная система используют взаимоисключение для защиты своих глобальных структур данных при высоком IRQL и какие механизмы синхронизации и взаимоисключения они применяют при низких уровнях IRQL (ниже «DPC/dispatch»).

Синхронизация ядра при высоком IRQL

Ядро должно гарантировать, что в каждый момент только один процессор выполняет код в критической секции. Критическими секциями ядра являются разделы кода, модифицирующие глобальные структуры данных, например базу данных диспетчера ядра или его очередь DPC Операционная система не смогла бы корректно работать, если бы ядро не гарантировало взаимоисключающий доступ потоков к этим структурам данных.

B этом плане больше всего проблем с прерываниями. Так, в момент обновления ядром глобальной структуры данных может возникнуть прерывание, процедура обработки которого изменяет ту же структуру. B простых однопроцессорных системах развитие событий по такому сценарию исключается путем отключения всех прерываний на время доступа к глобальным данным, однако в ядре Windows реализовано более сложное решение. Перед использованием глобального ресурса ядро временно маскирует прерывания, обработчики которых используют тот же ресурс. Для этого ядро повышает IRQL процессора до самого высокого уровня, используемого любым потенциальным источником прерываний, который имеет доступ к глобальным данным. Например, прерывание на уровне «DPC/dispatch» приводит к запуску диспетчера ядра, использующего диспетчерскую базу данных. Следовательно, любая другая часть ядра, имеющая дело с этой базой данных, повышает IRQL до уровня «DPC/dispatch», маскируя прерывания того же уровня перед обращением к диспетчерской базе данных.

Эта стратегия хорошо работает в однопроцессорных системах, но не годится для многопроцессорных конфигураций. Повышение IRQL на одном из процессоров не исключает прерываний на другом процессоре, а ядро должно гарантировать взаимоисключающий доступ на всех процессорах.

Взаимоблокирующие операции

Простейшая форма механизмов синхронизации опирается на аппаратную поддержку безопасных операций над целыми значениями и выполнения сравнений в многопроцессорной среде. Сюда относятся такие функции, как InterlockedIncrement, InterlockedDecrement, InterlockedExcbange и Interlocked-CompareExchange. Скажем, функция InterlockedDecrement, использует префикс х86-инструкции lock (например, lock xadd) для блокировки многопроцессорной шины на время операции вычитания, чтобы другой процессор, модифицирующий тот же участок памяти, не смог выполнить свою операцию в момент между чтением исходных данных и записью их нового (меньшего) значения. Эта форма базовой синхронизации используется ядром и драйверами.

Спин-блокировки

Механизм, применяемый ядром для взаимоисключения в многопроцессорных системах, называется спин-блокировкой (spinlock). Спин-блокировка - это блокирующий примитив, сопоставленный с какой-либо глобальной структурой данных вроде очереди DPC (рис. 3-24).

Перед входом в любую из критических секций, показанных на рис. 3-24, ядро должно установить спин-блокировку, связанную с защищенной очередью DPC Если спин-блокировка пока занята, ядро продолжает попытки установить спин-блокировку до тех пор, пока не достигнет успеха. Термин получил такое название из-за поведения ядра (и соответственно процессора), которое «крутится» (spin) в цикле, повторяя попытки, пока не захватит блокировку.

Спин-блокировки, как и защищаемые ими структуры данных, находятся в глобальной памяти. Код для их установки и снятия написан на языке ассемблера для максимального быстродействия. Bo многих архитектурах спин-блокировка реализуется аппаратно поддерживаемой командой test-and-set, которая проверяет значение переменной блокировки и устанавливает блокировку, выполняя всего одну атомарную команду. Это предотвращает захват блокировки вторым потоком в промежуток между проверкой переменной и установкой блокировки первым потоком.

Всем спин-блокировкам режима ядра в Windows назначен IRQL, всегда соответствующий уровню «DPC/dispatch» или выше. Поэтому, когда поток пытается установить спин-блокировку, все действия на этом или более низком уровне IRQL на данном процессоре прекращаются. Поскольку диспетчеризация потоков осуществляется при уровне «DPC/dispatch», поток, удерживающий спин-блокировку, никогда не вытесняется, так какданный IRQL маскирует механизмы диспетчеризации. Такая маскировка не дает прервать выполнение критической секции кода под защитой спин-блокировки и обеспечивает быстрое ее снятие. Спин-блокировки используются в ядре с большой осторожностью и устанавливаются на минимально возможное время.

ПРИМЕЧАНИЕ Поскольку IRQL - достаточно эффективный механизм синхронизации для однопроцессорных систем, функции установки и снятия спин-блокировки в однопроцессорных версиях HAL на самом деле просто повышают и понижают IRQL.

Ядро предоставляет доступ к спин-блокировкам другим компонентам исполнительной системы через набор функций ядра, включающий KeAcqui-reSpinlock и KeReleaseSpinlock. Например, драйверы устройств требуют спин-блокировки, чтобы система гарантировала единовременный доступ к регистрам устройства и другим глобальным структурам данных со стороны лишь одной части драйвера (и только с одного процессора). Спин-блокировка не предназначена для пользовательских программ - они должны оперировать объектами, которые рассматриваются в следующем разделе.

Спин-блокировки ядра накладывают ограничения на использующий их код. Как уже отмечалось, их IRQL всегда равен «DPC/dispatch», поэтому установивший спин-блокировку код может привести к краху системы, если попытается заставить планировщик выполнить операцию диспетчеризации или вызовет ошибку страницы.

Спин-блокировки с очередями

B некоторых ситуациях вместо стандартной спин-блокировки применяется особый тип спин-блокировки - с очередью (queued spinlock). Спин-блокировка с очередью лучше масштабируется в многопроцессорных системах, чем стандартная. Как правило, Windows использует лишь стандартные спин-блокировки, когда конкуренция за спин-блокировку ожидается низкой.

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

Тот факт, что спин-блокировка с очередью устанавливает флаги, а не глобальные блокировки, имеет два следствия. Во-первых, уменьшается интенсивный трафик, связанный с межпроцессорной синхронизацией. Во-вторых, вместо случайного выбора процессора из группы ожидающих спин-блокировку реализуется четкий порядок спин-блокировки по типу FIFO («первым вошел, первым вышел»). Такой порядок позволяет достичь более согласованной работы процессоров, использующих одну и ту же блокировку.

Windows определяет ряд глобальных спин-блокировок с очередями, сохраняя указатели на них в массиве, который содержится в блоке PCR (processor control region) каждого процессора. Глобальную спин-блокировку можно получить вызовом KeAcquireQueuedSpinlock с индексом в массиве PCR, по которому сохранен указатель на эту спин-блокировку. Количество глобальных спин-блокировок растет по мере появления новых версий операционной системы, и таблица их индексов публикуется в заголовочном файле Ntddk.h, поставляемом с DDK.

ЭКСПЕРИМЕНТ: просмотр глобальных спин-блокировок с очередями

Вы можете наблюдать за состоянием глобальных спин-блокировок с очередями, используя команду !qlock отладчика ядра. Эта команда имеет смысл лишь в многопроцессорной системе, так как в однопроцессорной версии HAL спин-блокировки не реализованы. B следующем примере (подготовленном в Windows 2000) спин-блокировка с очередью для базы данных диспетчера ядра удерживается процессором номер 1, а остальные спин-блокировки этого типа не затребованы (о базе данных диспетчера ядра см. главу 6).

Внутристековые спин-блокировки с очередями

Помимо статических спин-блокировок с очередями, определяемых глобально, ядра Windows XP и Windows Server 2003 поддерживают динамически создаваемые спин-блокировки с очередями. Для их создания предназначены функции KeAcquireInStackQueuedSpinlock и KeReleaseInStackQueuedSpin-lock. Этот тип блокировок используется несколькими компонентами, в том числе диспетчером кэша, диспетчером пулов исполнительной системы (executive pool manager) и NTFS. Упомянутые функции документированы в DDK для сторонних разработчиков драйверов.

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

Взаимоблокирующие операции в исполнительной системе

Ядро предоставляет ряд функций синхронизации, использующих спин-блокировки для более сложных операций, например для добавления и удаления элементов из одно- и двунаправленных связанных списков. K таким функциям, в частности, относятся ExfoterlockedPopEntryList и ExInterlockedPushEntryList (для однонаправленных связанных списков), ExInterlockedInsertHeadList и ExInter-lockedRemoveHeadList (ддя двунаправленных связанных списков). Все эти функции требуют передачи стандартной спин-блокировки в качестве параметра и интенсивно используются в ядре и драйверах устройств.

Синхронизация при низком IRQL

Компоненты исполнительной системы вне ядра также нуждаются в синхронизации доступа к глобальным структурам данных в многопроцессорной среде. Например, у диспетчера памяти есть только одна база данных блоков страниц. Обращение к ней осуществляется как к глобальной структуре данных, и драйверам устройств необходима гарантия получения монопольного доступа к своим устройствам. Вызывая функции ядра, исполнительная система может создать спин-блокировку, установить ее и снять.

Однако спин-блокировка лишь частично удовлетворяет потребности исполнительной системы в синхронизации. Поскольку спин-блокировка означает фактическую остановку процессора, она применяется только при двух условиях:

(o) требуется непродолжительное обращение к защищенным ресурсам без сложного взаимодействия с другим кодом;

(o) код критической секции нельзя выгрузить в страничный файл, он не ссылается на данные в подкачиваемой памяти, не вызывает внешние процедуры (включая системные сервисы) и не генерирует прерывания или исключения.

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

Существует несколько дополнительных механизмов синхронизации, применяемых, когда спин-блокировки не годятся:

(o) объекты диспетчера ядра (kernel dispatcher objects);

(o) быстрые мьютексы (fast mutexes) и защищенные мьютексы (guarded mu-texes);

(o) блокировки с заталкиванием указателя (push locks);

(o) ресурсы исполнительной системы (executive resources).

B таблице 3-9 кратко сравниваются возможности этих механизмов и их взаимосвязь с доставкой APC режима ядра.

Объекты диспетчера ядра

Ядро предоставляет исполнительной системе дополнительные механизмы синхронизации в форме объектов, в совокупности известных как объекты диспетчера ядра. Синхронизирующие объекты, видимые из пользовательского режима, берут свое начало именно от этих объектов диспетчера ядра. Каждый синхронизирующий объект, видимый из пользовательского режима, инкапсулирует минимум один объект диспетчера ядра. Семантика синхронизации исполнительной системы доступна программистам через Windows-функции WaitForSingleObject и WaitForMultipleObjects, реализуемые подсистемой Windows на основе аналогичных системных сервисов, предоставляемых диспетчером объектов. Поток в Windows-приложении можно синхронизировать по таким Windows-объектам, как процесс, поток, событие, семафор, мьютекс, ожидаемый таймер, порт завершения ввода-вывода или файл.

Еще один тип синхронизирующих объектов исполнительной системы назван (без особой на то причины) ресурсами исполнительной системы (executive resources). Эти ресурсы обеспечивают как монопольный доступ (по аналогии с мьютексами), так и разделяемый доступ для чтения (когда несколько потоков-«читателей» обращается к одной структуре только для чтения). Однако они доступны лишь коду режима ядра, а значит, недоступны через Windows API. Ресурсы исполнительной системы являются не объектами диспетчера ядра, а скорее структурами данных, память для которых выделяется прямо из неподкачиваемого пула, имеющего свои специализированные сервисы для инициализации, блокировки, освобождения, запроса и ожидания. Структура ресурсов исполнительной системы определена в Ntddk.h, а соответствующие процедуры описаны в DDK.

B остальных подразделах мы детально обсудим, как реализуется ожидание на объектах диспетчера ядра.

Ожидание на объектах диспетчера ядра

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

ПРИМЕЧАНИЕ Ha рис. 3-25 показана схема перехода состояний с выделением состояний «готов» (ready), «ожидает» (waiting) и «выполняется» (running) - они относятся к ожиданию на объектах. Прочие состояния описываются в главе 6.

B любой момент синхронизирующий объект находится в одном из двух состояний: свободном (signaled) или занятом (nonsignaled). Для синхронизации с объектом поток вызывает один из системных сервисов ожидания, предоставляемых диспетчером объектов, и передает описатель этого объекта. Поток может ожидать на одном или нескольких объектах, а также указать, что ожидание следует прекратить, если объект (или объекты) не освободился в течение определенного времени. Всякий раз, когда ядро переводит объект в свободное состояние, функция KiWaitTest ядра проверяет, ждут ли этот объект какие-нибудь потоки и не ждут ли они каких-либо других объектов. Если да, ядро выводит один или более потоков из состояния ожидания, после чего их выполнение может быть продолжено.

Взаимосвязь синхронизации с диспетчеризацией потоков иллюстрирует следующий пример с использованием объекта «событие».

(o) Поток пользовательского режима ждет на описателе объекта «событие» (т. е. ждет перехода этого объекта в свободное состояние).

(o) Ядро изменяет состояние потока с «готов» на «ожидает» и добавляет его в список потоков, ждущих объект «событие».

(o) Другой поток устанавливает объект «событие».

(o) Ядро просматривает список потоков, ожидающих этот объект. Если условия ожидания какого-либо потока выполнены (см. примечание ниже), ядро переводит его из состояния «ожидает» в состояние «готов». Если это поток с динамическим приоритетом, ядро может повысить его приоритет для выполнения.

(o) Поскольку новый поток теперь готов к выполнению, происходит перераспределение процессорного времени. Если при этом диспетчер обнаружит, что приоритет выполняемого потока ниже, чем приоритет потока, только что перешедшего в состояние «готов», он вытеснит поток с более низким приоритетом и выдаст программное прерывание для инициации переключения контекста на поток с более высоким приоритетом.

(o) Если в данный момент вытеснение невозможно ни на одном из процессоров, диспетчер включает поток в свою очередь потоков, готовых к выполнению.

ПРИМЕЧАНИЕ Некоторые потоки могут ждать более одного объекта, и в таком случае их ожидание продолжается.

Условия перехода объектов в свободное состояние

Эти условия различны для разных объектов. Например, объект «поток» находится в занятом состоянии в течение всего срока своей жизни и переводится ядром в свободное состояние лишь при завершении. Аналогичным образом, ядро переводит объект «процесс» в свободное состояние в момент завершения последнего потока процесса. Ho такой объект, как таймер, переводится в свободное состояние по истечении заданного времени.

Выбирая механизм синхронизации, вы должны учитывать в своей программе поведение синхронизирующих объектов. B таблице 3-10 показано, когда переходят в свободное состояние синхронизирующие объекты различных типов.

Когда объект переводится в свободное состояние, ожидающие его потоки обычно немедленно выходят из ждущего состояния. Однако, как показано на рис. 3-26, некоторые объекты диспетчера ядра и системные события ведут себя иначе.

Например, объект «событие уведомления» - в Windows API он называется событием со сбросом вручную (manual reset event) - используется для уведомления о каком-либо событии. Когда этот объект переводится в свободное состояние, все потоки, ожидающие его, освобождаются. Исключением является тот поток, который ждет сразу несколько объектов: он может продолжать ожидание, пока не освободятся дополнительные объекты.

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

События с ключом и критические секции

Синхронизирующий объект, впервые появившийся в Windows XP и названный событием с ключом (keyed event), заслуживает особого упоминания. Он помогает процессам справляться с нехваткой памяти при использовании критических секций. Это недокументированное событие позволяет потоку указать «ключ» в следующей ситуации-, данный поток должен пробуждаться, когда другой поток того же процесса освобождает событие с тем же ключом.

Windows-процессы часто используют функции критических секций - EnterCriticalSection и LeaveCriticalSection - для синхронизации доступа потоков к личным ресурсам процесса. Вызовы этих функций эффективнее прямого обращения к объектам «мьютекс», так как в отсутствие конкуренции они не заставляют переходить в режим ядра. При наличии конкуренции EnterCriticalSection динамически создает объект «событие», и поток, которому нужно захватить критическую секцию, ждет, когда поток, владеющий этой секцией, освободит ее вызовом LeaveCriticalSection.

Если создать объект «событие» для критической секции не удалось из-за нехватки системной памяти, EnterCriticalSection использует глобальное событие с ключом - CritSecOutOjMemoryEvent (в каталоге \Ker-nel пространства имен диспетчера объектов). Если EnterCritica amp;ection вынуждена задействовать CritSecOutOjMemoryEvent вместо стандартного события, поток, ждущий критическую секцию, использует адрес этой секции как ключ. Это обеспечивает корректную работу функций критических секций даже в условиях временной нехватки памяти.

Мы не ставили себе задачу исчерпывающе описать все объекты исполнительной системы, а лишь хотели дать представление об их базовой функциональности и механизмах синхронизации. Об использовании этих объектов в Windows-программах см. справочную документацию Windows или четвертое издание книги Джеффри Рихтера «Windows для профессионалов».

Структуры данных

Учет ожидающих потоков и их объектов ожидания базируется на двух ключевых структурах данных: заголовках диспетчера (dispatcher headers) и блоках ожидания (wait blocks). Обе эти структуры определены в Ntddk.h, заголовочном файле DDK. Для удобства мы воспроизводим здесь эти определения.

Заголовок диспетчера содержит тип объекта, информацию о состоянии (занят/свободен) и список потоков, ожидающих этот объект. У каждого ждущего потока есть список блоков ожидания, где перечислены ожидаемые потоком объекты, а у каждого объекта диспетчера ядра - список блоков ожидания, где перечислены ожидающие его потоки. Этот список ведется так, что при освобождении объекта диспетчера ядро может быстро определить, кто ожидает данный объект. B блоке ожидания имеются указатели на объект ожидания, ожидающий поток и на следующий блок ожидания (если поток ждет более одного объекта). Он также регистрирует тип ожидания («любой» или «все») и позицию соответствующего элемента в таблице описателей, переданную потоком в функцию WaitForMultipleObjects (позиция 0 - если поток ожидает лишь один объект).

Ha рис. 3-27 показана связь объектов диспетчера ядра с блоками ожидания потоков. B данном примере поток 1 ждет объект В, а поток 2 - объекты A и В. Если объект A освободится, поток 2 не сможет возобновить свое выполнение, так как ядро обнаружит, что он ждет и другой объект. C другой стороны, при освобождении объекта B ядро сразу же подготовит поток 1 к выполнению, поскольку он не ждет никакие другие объекты.

ЭКСПЕРИМЕНТ: просмотр очередей ожидания

Хотя многие утилиты просмотра процессов умеют определять, находится ли поток в состоянии ожидания (отмечая в этом случае и тип ожидания), список объектов, ожидаемых потоком, можно увидеть только с помощью команды !process отладчика ядра. Например, следующий фрагмент вывода команды !process показывает, что поток ждет на объекте-событии.

Эти данные позволяют нам убедиться в отсутствии других потоков, ожидающих данный объект, поскольку указатели начала и конца списка ожидания указывают на одно и то же место (на один блок ожидания). Копия блока ожидания (по адресу 0x8a12a398) дает следующее:

Если в списке ожидания более одного элемента, вы можете выполнить ту же команду со вторым указателем в поле WaitListEntry каждого блока ожидания (команду !thread применительно к указателю потока в блоке ожидания) для прохода по списку и просмотра других потоков, ждущих данный объект.

Быстрые и защищенные мьютексы

Быстрые мьютексы (fast mutexes), также известные как мьютексы исполнительной системы, обычно обеспечивают более высокую производительность, чем объекты «мьютекс». Почему? Дело в том, что быстрые мьютексы, хоть и построены на объектах событий диспетчера, в отсутствие конкуренции не требуют ожидания объекта «событие» (и соответственно спин-блокировок, на которых основан этот объект). Эти преимущества особенно ярко проявляются в многопроцессорной среде. Быстрые мьютексы широко используются в ядре и драйверах устройств.

Однако быстрые мьютексы годятся, только если можно отключить доставку обычных APC режима ядра. B исполнительной системе определены две функции для захвата быстрых мьютексов: ExAcquireFastMutex и ExAcquire-FastMutexUnsafe. Первая функция блокирует доставку всех APC, повышая IRQL процессора до уровня APC_LEVEL, а вторая - ожидает вызова при уже отключенной доставке обычных APC режима ядра (такое отключение возможно повышением IRQL до уровня «APC» или вызовом KeEnterCriticalRegiori). Другое ограничение быстрых мьютексов заключается в том, что их нельзя захватывать рекурсивно, как объекты «мьютекс».

Защищенные мьютексы (guarded mutexes) - новшество Windows Server 2003; они почти идентичны быстрым мьютексам (хотя внутренне используют другой синхронизирующий объект, KGATE). Захватить защищенные мьютексы можно вызовом функции KeAcquireGuardedMutex, отключающей доставку всех APC режима ядра через KeEnterGuardedRegion, а не KeEnterCritical-Region, которая на самом деле отключает только обычные APC режима ядра. Защищенные мьютексы недоступны вне ядра и используются в основном диспетчером памяти для защиты глобальных операций вроде создания страничных файлов, удаления определенных типов разделов общей памяти и расширения пула подкачиваемой памяти. (Подробнее о диспетчере памяти см. главу 7.)

Ресурсы исполнительной системы

Ресурсы исполнительной системы (executive resources) - это механизм синхронизации, который поддерживает разделяемый (совместный) и монопольный доступ и по аналогии с быстрыми мьютексами требует предварительного отлючения доставки обычных APC режима ядра. Они основаны на объектах диспетчера, которые используются только при наличии конкуренции. Ресурсы исполнительной системы широко применяются во всей системе, особенно в драйверах файловой системы.

Потоки, которым нужно захватить какой-либо ресурс для совместного доступа, ждут на семафоре, сопоставленном с этим ресурсом, а потоки, которым требуется захватить ресурс для монопольного доступа, - на событии. Семафор с неограниченным счетчиком применяется потому, что в первом случае можно пробудить все ждущие потоки и предоставить им доступ к ресурсу, как только этот семафор перейдет в свободное состояние (ресурс будет освобожден потоком, захватившим его в монопольное владение). Когда потоку нужен монопольный доступ к занятому на данный момент ресурсу, он ждет на синхронизирующем объекте «событие», так как при освобождении события пробуждается только один из ожидающих потоков.

Для захвата ресурсов предназначен целый ряд функций: ExAcquireResour-ceSharedLite, ExAcquireResourceExclusiveLite, ExAcquireSharedStarveExclusive, ExAcquireWaitForExclusive и ExTryToAcquireResourceExclusiveLite. Эти функции документированы в DDK.

ЭКСПЕРИМЕНТ: перечисление захваченных ресурсов исполнительной системы

Команда !locks отладчика ядра ищет в пуле подкачиваемой памяти объекты ресурсов исполнительной системы и выводит их состояние. По умолчанию эта команда перечисляет только захваченные на данный момент ресурсы, но ключ -d позволяет перечислять все ресурсы исполнительной системы. Вот фрагмент вывода этой команды:

Заметьте, что счетчик конкурирующих потоков (contention count), извлекаемый из структуры ресурса, фиксирует, сколько раз потоки пытались захватить данный ресурс и были вынуждены переходить в состояние ожидания из-за того, что он уже занят.

Для изучения деталей конкретного объекта ресурса (в частности, кто владеет ресурсом и кто ждет его освобождения) укажите ключ -v и адрес ресурса:

lkd› !locks -v 0x805439a0

Блокировки с заталкиванием указателя

Блокировки с заталкиванием указателя (push locks), впервые появившиеся в Windows XP, являются еще одним оптимизированным механизмом синхронизации, который основан на объекте «событие» (в Windows Server 2003 такие блокировки базируются на внутреннем синхронизирующем объекте KGATE) и подобно быстрым мьютексам заставляет ждать этот объект только при наличии конкуренции. Такие блокировки имеют преимущества над быстрыми мьютексами, так как их можно захватывать как в разделяемом, так и в монопольном режиме. Они не документированы и не экспортируются ядром, так как зарезервированы для использования самой операционной системой.

Существует два типа блокировок с заталкиванием указателя: обычный и с поддержкой кэша (cache aware). Первый тип занимает в памяти тот же объем, что и указатель (4 байта в 32-разрядных системах и 8 байтов в 64-разрядных). Когда поток захватывает обычную блокировку с заталкиванием указателя, код этой блокировки помечает ее как занятую, если она на данный момент свободна. Если блокировка захвачена для монопольного доступа или если потоку нужно захватить ее монопольно, а она уже захвачена для разделяемого доступа, ее код создает в стеке потока блок ожидания, инициализирует объект «событие» в этом блоке и добавляет последний в список ожидания, сопоставленный с блокировкой. Как только блокировка освобождается, ее код пробуждает ждущий поток (если таковой имеется), освобождая событие в блоке ожидания потока.

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

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

Обнаружение взаимоблокировки с помощью Driver Verifier

Взаимоблокировка (deadlock) - это проблема синхронизации, возникающая, когда два потока или процессора удерживают ресурсы, нужные другому, и ни один из них не отдает их. Такая ситуация может приводить к зависанию системы или процесса. Утилита Driver Verifier, описываемая в главах 7 и 9, позволяет проверять возможность взаимоблокировки, в том числе на спин-блокировках, быстрых и обычных мьютексах. O том, как пользоваться Driver Verifier для анализа зависания системы, см. главу 14.

Системные рабочие потоки

При инициализации Windows создает несколько потоков в процессе System, которые называются системными рабочими потоками (system worker threads). Они предназначены исключительно для выполнения работы по поручению других потоков. Bo многих случаях потоки, выполняемые на уровне «DPC/dispatch», нуждаются в вызове таких функций, которые могут быть вызваны только при более низком IRQL. Например, DPC-процедуре, выполняемой в контексте произвольного потока при IRQL уровня «DPC/dispatch» (DPC может узурпировать любой поток в системе), нужно обратиться к пулу подкачиваемой памяти или ждать на объекте диспетчера для синхронизации с потоком какого-либо приложения. Поскольку DPC-процедура не может понизить IRQL, она должна передать свою задачу потоку, который сможет выполнить ее при IRQL ниже уровня «DPC/dispatch».

Некоторые драйверы устройств и компоненты исполнительной системы создают собственные потоки для обработки данных на уровне «passive», но большинство вместо этого использует системные рабочие потоки, что помогает избежать слишком частого переключения потоков и чрезмерной нагрузки на память из-за диспетчеризации дополнительных потоков. Драйвер устройства или компонент исполнительной системы запрашивает сервисы системных рабочих потоков через функцию исполнительной системы ExQueueWorkItem или IoQueueWorkItem. Эти функции помещают рабочий элемент (work item) в специальную очередь, проверяемую системными рабочими потоками (см. раздел «Порты завершения ввода-вывода» главы 9).

Рабочий элемент включает указатель на процедуру и параметр, передаваемый потоком этой процедуре при обработке рабочего элемента. Процедура реализуется драйвером устройства или компонентом исполнительной системы, выполняемым на уровне «passive».

Например, DPC-процедура, которая должна ждать на объекте диспетчера, может инициализировать рабочий элемент, который указывает на процедуру в драйвере, ждущем на объекте диспетчера, и, возможно, на указатель на объект. Ha каком-то этапе системный рабочий поток извлекает из своей очереди рабочий элемент и выполняет процедуру драйвера. После ее выполнения системный рабочий поток проверяет, нет ли еще рабочих элементов, подлежащих обработке. Если нет, системный рабочий поток блокируется, пока в очередь не будет помещен новый рабочий элемент. Выполнение DPC-процедуры может и не закончиться в ходе обработки ее рабочего элемента системным рабочим потоком. (B однопроцессорной системе выполнение этой процедуры всегда завершается до обработки ее рабочего элемента, так как на уровне IRQL «DPC/dispatch» потоки не планируются.)

Существует три типа системных рабочих потоков:

(o) отложенные (delayed worker threads) - выполняются с приоритетом 12, обрабатывают некритичные по времени рабочие элементы и допускают выгрузку своего стека в страничный файл на время ожидания рабочих элементов;

(o) критичные (critical worker threads) - выполняются с приоритетом 13, обрабатывают критичные по времени рабочие элементы. B Windows Server их стек всегда находится только в физической памяти;

(o) гиперкритичный (hypercritical worker thread) - единственный поток, выполняемый с приоритетом 15. Его стек тоже всегда находится в памяти. Диспетчер процессов использует гиперкритичные по времени рабочие элементы для выполнения функции, освобождающей завершенные потоки.

Число отложенных и критичных системных рабочих потоков, создаваемых функцией исполнительной системы ExpWorkerInitialization, которая вызывается на ранних стадиях процесса загрузки, зависит от объема памяти в системе и от того, является ли система сервером. B таблице 3-11 показано количество потоков, изначально создаваемых в системах с различной конфигурацией. Вы можете указать ExpInitializeWorker создать дополнительно до 16 отложенных и 16 критичных системных рабочих потоков. Для этого используйте параметры AdditionalDelayedWorkerThreads и AdditionalCri-ticalWorkerThreads в разделе реестра HKLM\SYSTEM\CurrentControlSet\Cont-rol\ Session Manager\Executive.

Исполнительная система старается балансировать число критичных системных рабочих потоков в соответствии с текущей рабочей нагрузкой. Каждую секунду функция исполнительной системы ExpWorkerThreadBalanceManager проверяет, надо ли создавать новый критичный рабочий поток. Кстати, критичный рабочий поток, создаваемый функцией ExpWorkerTbread-BalanceManager, называется динамическим (dynamic worker thread). Для создания такого потока должны быть выполнены следующие условия.

(o) Очередь критичных рабочих элементов не должна быть пустой.

(o) Число неактивных критичных потоков (блокированных в ожидании рабочих элементов или на объектах диспетчера при выполнении рабочей процедуры) должно быть меньше количества процессоров в системе.

(o) B системе должно быть менее 16 динамических рабочих потоков.

Динамические потоки завершаются через 10 минут пребывания в неактивном состоянии. B зависимости от рабочей нагрузки исполнительная система может создавать до 16 таких потоков.

Глобальные флаги Windows

Windows поддерживает набор флагов, который хранится в общесистемной глобальной переменной NtGlobalFlag, предназначенной для отладки, трассировки и контроля операционной системы. При загрузке системы переменная NtGlobalFlag инициализируется значением параметра GlobalFlag из раздела реестра HKLM\SYSTEM\CurrentControlSet\Control\Session Manager. По умолчанию его значение равно 0, и в системах с обычной конфигурацией глобальные флаги обычно не используются. Кроме того, каждый образ исполняемого файла имеет набор глобальных флагов, позволяющих включать код внутренней трассировки и контроля (хотя битовая структура этих флагов совершенно не соответствует структуре общесистемных глобальных флагов). Эти флаги не документированы, но могут пригодиться при изучении внутреннего устройства Windows.

K счастью, в Platform SDK и средствах отладки есть утилита Gflags.exe, позволяющая просматривать и изменять системные глобальные флаги (либо в реестре, либо в работающей системе) и глобальные флаги образов исполняемых файлов. Gflags поддерживает как GUI-интерфейс, так и командную строку. Параметры командной строки можно узнать, введя gflags /?. При запуске утилиты без параметров выводится диалоговое окно, показанное на рис. 3-28.

Вы можете переключаться между реестром (System Registry) и текущим значением переменной в системной памяти (Kernel Mode). Для внесения изменений нужно щелкнуть кнопкуАрр1у (кнопка OK просто закрывает программу). Хотя вы можете изменять флаги в работающей системе, большинство из них требует перезагрузки для того, чтобы изменения вступили в силу.

Поскольку документации на этот счет нет, лучше перезагрузиться после любых изменений.

Выбрав Image File Options, вы должны ввести имя исполняемого в системе файла. Этот переключатель позволяет изменять набор глобальных флагов отдельного образа (а не всей системы). Заметьте, что флаги на рис. 3-29 отличаются от флагов на рис. 3-28.

Рис. 3-29. Настройка в Gflags глобальных флагов образа исполняемого файла

ЭКСПЕРИМЕНТ: включение трассировки загрузчика образов и просмотр NtGlobalFlag

Чтобы увидеть пример детальной трассировочной информации, которую можно получить при установке глобальных флагов, попробуйте запустить Gflags в системе с загруженным отладчиком ядра, которая подключена к компьютеру с запущенной утилитой Kd или Windbg.

Далее попробуйте установить, например, глобальный флаг Show Loader Snaps. Для этого выберите Kernel Mode, установите флажок Show Loader Snaps и щелкните кнопку Apply. Теперь запустите на этой машине какую-нибудь программу, и отладчик ядра будет выдавать информацию, аналогичную показанной ниже.

Для просмотра состояния переменной NtGlobalFlag можно использовать команды /gflags и /gflag отладчика ядра. Первая выводит список всех флагов, указывая, какие из них установлены, a /gflag показывает только установленные флаги.

LPC

LPC (local procedure call) - это механизм межпроцессной связи для высокоскоростной передачи сообщений. Он недоступен через Windows API напрямую и является внутренним механизмом, которым пользуются только компоненты операционной системы Windows. Вот несколько примеров того, где применяется LPC

(o) Windows-приложения, использующие RPC (документированный API), неявно используют и LPC, когда указывают локальный RPC - разновидность RPC, применяемую для взаимодействия между процессами в рамках одной системы.

(o) Некоторые функции Windows API обращаются к LPC, посылая сообщения процессу подсистемы Windows.

(o) Winlogon взаимодействует с процессом LSASS через LPC

(o) Монитор состояния защиты (компонент исполнительной системы, рассматриваемый в главе 8) также взаимодействует с процессом LSASS через LPC

ЭКСПЕРИМЕНТ: просмотр объектов «порт LPC»

Вы можете увидеть именованные объекты «порт LPC» (LPC port objects) с помощью утилиты Winobj. Запустите Winobj.exe и выберите корневой каталог. Интересующие нас объекты обозначаются значком в виде разъема, как показано ниже.

Для просмотра объектов «порт LPC», используемых RPC, выберите каталог \RPC Control, как на следующей иллюстрации.

Вы также можете наблюдать объекты «порт LPC» с помощью команды !lpc отладчика ядра. Параметры этой команды позволяют перечислять порты LPC, сообщения LPC и потоки, ожидающие или посылающие эти сообщения. Для просмотра порта аутентификации LSASS (в него Winlogon посылает запросы на вход в систему) сначала нужно получить список портов в данной системе.

Как правило, LPC используются для взаимодействия между серверным процессом и одним или несколькими клиентскими процессами. LPC-соеди-нение может быть установлено между двумя процессами пользовательского режима или между компонентом режима ядра и процессом пользовательского режима. Например, как говорилось в главе 2, Windows-процессы иногда посылают сообщения подсистеме Windows через LPC Некоторые системные процессы вроде Winlogon и LSASS тоже используют LPC Примерами компонентов режима ядра, взаимодействующих с пользовательскими процессами через LPC, могут служить монитор состояния защиты и LSASS. LPC предусматривает три способа обмена сообщениями.

(o) Сообщение длиной менее 256 байтов можно передать вызовом LPC с буфером, содержащим сообщение. Затем это сообщение копируется из адресного пространства процесса-отправителя в системное адресное пространство, а оттуда - в адресное пространство процесса-получателя.

(o) Если клиент и сервер хотят обменяться данными, размер которых превышает 256 байтов, они могут использовать общий раздел, на который они оба спроецированы. Отправитель помещает данные в общий раздел и посылает получателю уведомление с указателем на область раздела, где находятся данные.

(o) Если серверу нужно считать или записать данные, объем которых превышает размер общего раздела, то их можно напрямую считать из клиентского адресного пространства или записать туда. Для этого LPC предоставляет серверу две функции. Сообщение, посланное первой функцией, обеспечивает синхронизацию передачи последующих сообщений. LPC экспортирует единственный объект исполнительной системы объект «порт» (port object). Однако порты бывают нескольких видов.

(o) Порт серверного соединения (server connection port) Именованный порт, служащий точкой запроса связи с сервером. Через него клиенты могут соединяться с сервером.

(o) Коммуникационный порт сервера (server communication port) Безымянный порт, используемый сервером для связи с конкретным клиентом. У сервера имеется по одному такому порту на каждый активный клиент.

(o) Коммуникационный порт клиента (client communication port) Безымянный порт, используемый конкретным клиентским потоком для связи с конкретным сервером.

(o) Безымянный коммуникационный порт (unnamed communication port) Порт, создаваемый для связи между двумя потоками одного процесса.

LPC обычно используется так. Сервер создает именованный порт соединения. Клиент посылает в него запрос на установление связи. Если запрос удовлетворен, создается два безымянных порта - коммуникационный порт клиента и коммуникационный порт сервера. Клиент получает описатель коммуникационного порта клиента, а сервер - описатель коммуникационного порта сервера. После этого клиент и сервер используют новые порты для обмена данными.

Схема соединения между клиентом и сервером показана на рис. 3-30.

Трассировка событий ядра

Различные компоненты ядра Windows и несколько базовых драйверов устройств оснащены средствами мониторинга для записи трассировочных данных об их работе, используемых при анализе проблем в системе. Эти компоненты опираются на общую инфраструктуру в ядре, которая предоставляет трассировочные данные механизму пользовательского режима - Event Tracing for Windows (ETW). Приложение, использующее ETW, попадает в одну или более следующих категорий.

(o) Контроллер (controller) Начинает и прекращает сеансы протоколирования (logging sessions), а также управляет буферными пулами.

(o) Провайдер (provider) Определяет GUID (globally unique identifiers) для классов событий, для которых он может создавать трассировочные данные, и регистрирует их в ETW. Провайдер принимает команды от контроллера на запуск и остановку трассировки классов событий, за которые он отвечает.

(o) Потребитель (consumer) Выбирает один или более сеансов трассировки, для которых ему нужно считывать трассировочные данные. Принимает информацию о событиях в буферы в режиме реального времени или в файлы журнала.

B системы Windows Server встроено несколько провайдеров пользовательского режима, в том числе для Active Directory, Kerberos и Netlogon. ETW определяет сеанс протоколирования с именем NT Kernel Logger [также известный как регистратор ядра (kernel logger)] для использования ядром и базовыми драйверами. Провайдер для NT Kernel Logger реализуется драйвером устройства Windows Management Instrumentation (WMI) (драйвер называется Wmixwdm), который является частью Ntoskrnl.exe. (Подробнее о WMI см. соответствующий раздел в главе 5.) Этот драйвер не только служит основой регистратора ядра, но и управляет регистрацией классов событий ETW пользовательского режима.

Драйвер WMI экспортирует интерфейсы управления вводом-выводом для применения в ETW-процедурах пользовательского режима и драйверах устройств, предоставляющих трассировочные данные для регистратора ядра. (O командах управления вводом-выводом см. главу 9.) Он также реализует функции для использования компонентами в Ntoskrnl.exe режима ядра, которые формируют трассировочный вывод.

Когда в пользовательском режиме включается контроллер, регистратор ядра (библиотека ETW, реализованная в \Windows\System32\Ntdll.dll) посылает запрос управления вводом-выводом (I/O control request) дpaйвepy WMI, сообщая ему, для каких классов событий контроллер хочет начать трассировку. Если настроено протоколирование в файлы журналов (в противоположность протоколированию в буфер памяти), драйвер WMI создает специальный системный поток в системном процессе, а тот создает файл журнала. Принимая события трассировки от активизированных источников трассировочных данных, драйвер WMI записывает их в буфер. Поток записи в журнал пробуждается раз в секунду, чтобы сбросить содержимое буферов в файл журнала.

Записи трассировки, генерируемые для регистратора ядра, имеют стандартный ETW-заголовок события трассировки, в котором содержатся временная метка, идентификаторы процесса и потока, а также сведения о том, какому классу события соответствует данная запись. Классы событий могут предоставлять дополнительные данные, специфичные для их событий. Например, класс дисковых событий (disk event class) указывает тип операции (чтение или запись), номер диска, на котором выполняется операция, а также смещение начального сектора и количество секторов, затрагиваемых данной операцией.

Классы трассировки, которые можно включить для регистратора ядра, и компонент, генерирующий каждый класс, перечислены ниже.

(o) Дисковый ввод-вывод Драйвер класса дисков.

(o) Файловый ввод-вывод Драйверы файловой системы.

(o) Конфигурирование оборудования Диспетчер Plug and Play (см. главу 9).

(o) Загрузка/выгрузка образов Системный загрузчик образов в ядре.

(o) Ошибки страниц Диспетчер памяти (см. главу 7).

(o) Создание/удаление процессов Диспетчер процессов (см. главу 6).

(o) Создание/удаление потоков Диспетчер процессов.

(o) Операции с реестром Диспетчер конфигурации (см. раздел «Реестр» в главе 4).

(o) АктивностьТСР/UDP ДрайверТСР/IР.

Более подробные сведения о ETW и регистраторе ядра, в том числе примеры кода для контроллеров и потребителей, см. в Platform SDK.

ЭКСПЕРИМЕНТ: трассировка активности TCP/IP с помощью регистратора ядра

Чтобы включить регистратор ядра и получить от него файл журнала активности TCP/IP, действуйте следующим образом.

1. Запустите оснастку Performance (Производительность) и выберите узел Performance Logs And AIerts (Журналы и оповещения производительности) .

2. Укажите Trace Logs (Журналы трассировки) и выберите из меню Action (Действие) команду New Log Settings (Новые параметры журнала).

3. B появившемся окне присвойте имя новым параметрам (например, experiment).

4. B следующем диалоговом окне выберите Events Logged By System Provider (События, протоколируемые системным поставщиком) и сбросьте все, кроме Network TCP/IP (События сети TCP/IP).

5. B поле ввода Run As (От имени) введите имя учетной записи администратора и ее пароль.

6. Закройте это диалоговое окно и создайте активность в сети, открыв браузер и зайдя на какой-нибудь Web-сайт.

7. Укажите журнал трассировки, созданный в узле таких журналов, и выберите Stop (Остановка) из меню Action (Действие).

8. Откройте окно командной строки и перейдите в каталог C:\Perflogs (или тот каталог, который вы указали как место хранения файла журнала).

9. Если вы используете Windows XP или Windows Server 2003, запустите Tracerpt (эта утилита находится в каталоге \Windows\Sys-tem32) и передайте ей имя файла журнала трассировки. Если вы работаете в Windows 2000, скачайте и запустите Tracedmp из ресурсов Windows 2000. Обе утилиты генерируют два файла: dumpfile.csv и summary.txt.

10.Откройте dumpfile.csv в Microsoft Excel или в любом текстовом редакторе. Вы должны увидеть записи трассировки TCP и/или UDP:

Wow64

Wow64 (эмуляция Win32 в 64-разрядной Windows) относится к программному обеспечению, которое дает возможность выполнять 32-разрядные х8б-приложения в 64-разрядной Windows. Этот компонент реализован как набор DLL пользовательского режима.

(o) Wow64.dll - управляет созданием процессов и потоков, подключается к диспетчеризации исключений и перехватывает вызовы базовых системных функций, экспортируемых Ntoskrnl.exe. Также реализует перенаправление файловой системы (file system redirection) и перенаправление реестра и отражение (reflection).

(o) Wow64Cpu.dll - управляет 32-разрядным контекстом процессора каждого потока, выполняемого внутри Wow64, и предоставляет специфичную для процессорной архитектуры поддержку переключения режима процессора из 32-разрядного в 64-разрядный и наоборот.

(o) Wow64Win.dll - перехватывает вызовы системных GUI-функций, экспортируемых Win32k.sys.

Взаимосвязь этих DLL показана на рис. 3-31.

Системные вызовы

Wow64 ставит ловушки на всех путях выполнения, где 32-разрядный код должен взаимодействовать с родным 64-разрядным или где 64-разрядной системе нужно обращаться к 32-разрядному коду пользовательского режима. При создании процесса диспетчер процессов проецирует на его адресное пространство 64-разрядную библиотеку Ntdll.dll. Загрузчик 64-разрядной системы проверяет заголовок образа и, если этот процесс 32-разрядный для платформы x86, загружает Wow64.dll. После этого Wow64 проецирует 32-разрядную Ntdll.dll (она хранится в каталоге \Windows\Syswow64). Далее Wow64 настраивает стартовый контекст внутри Ntdll, переключает процессор в 32-разрядный режим и начинает выполнять 32-разрядный загрузчик. C этого момента все идет так же, как в обычной 32-разрядной системе.

Специальные 32-разрядные версии Ntdll.dll, User32.dll и Gdi32.dll находятся в каталоге \Windows\Syswow64. Они вызывают Wow64, не выдавая инструкции вызова, которые используются в истинно 32-разрядной системе. Wow64 переключается в «родной» б4-разрядный режим, захватывает параметры, связанные с системным вызовом, преобразует 32-разрядныеуказате-ли в б4-разрядные и выдает соответствующий для 64-разрядной системы системный вызов. Когда последняя возвращает управление, Wow64 при необходимости преобразует любые выходные параметры из 64-битных в 32-битные форматы и вновь переключается в 32-разрядный режим.

Диспетчеризация исключений

Wow64 перехватывает диспетчеризацию исключений через KiUserException-Dispatcber в Ntdll. Всякий раз, когда 64-разрядное ядро собирается направить исключение Wow64-npoцеccy, Wow64 перехватывает его и запись контекста (context record) в пользовательском режиме, а затем, создав на их основе 32-разрядные исключение и запись контекста, направляет их своему процессу так же, как это сделало бы истинно 32-разрядное ядро.

Пользовательские обратные вызовы

Wow64 перехватывает все обратные вызовы из режима ядра в пользовательский режим. Wow64 интерпретирует их как системные вызовы; однако трансляция данных происходит в обратном порядке: входные параметры преобразуются из 64-битных форматов в 32-битные, а выходные (после возврата из обратного вызова) - из 32-битных в 64-битные.

Перенаправление файловой системы

Чтобы обеспечить совместимость приложений и упростить перенос Win32-программ на платформу 64-разрядной Windows, имена системных каталогов сохранены прежними. Поэтому в \Windows\System32 содержатся «родные» 64-разрядные исполняемые файлы. Так как Wow64 ставит ловушки на все системные вызовы, этот компонент транслирует все API-вызовы, относящиеся к путям, и заменяет в них каталог \Windows\System32 на \Win-dows\Syswow64. Wow64 также перенаправляет \Windows\System32 \Ime в \Windows\System32\IME (x86), чтобы обеспечить совместимость 32-разрядных приложений в 64-разрядных системах с установленной поддержкой дальневосточных языков. Кроме того, 32-разрядные программы устанавливаются в каталог \Program Files (x86), тогда как 64-разрядные - в обычный каталог \Program Files.

B каталоге \Windows\System32 есть несколько подкаталогов, которые по соображениям совместимости исключаются из перенаправления. Так что, если 32-разрядным приложениям понадобится доступ к этим каталогам, они смогут обращаться к ним напрямую. B число таких каталогов входят:

(o) %windir%\system32\drivers\etc;

(o) %windir%\system32\spool;

(o) %windir%\system32\catroot2;

(o) %windir%\system32\logfiles.

Наконец, Wow64 предоставляет механизм, позволяющий отключать перенаправление файловой системы, встроенное в Wow64, для каждого потока индивидуально. Данный механизм доступен через функцию Wow64Enab-leWow64FsRedirection, которая впервые появилась в Windows Server 2003.

Перенаправление реестра и отражение

Приложения и компоненты хранят свои конфигурационные данные в реестре. Эту информацию компоненты обычно записывают в реестр при регистрации в ходе установки. Если один и тот же компонент поочередно устанавливается и регистрируется как 32- и 64-разрядный, тогда компонент, зарегистрированный последним, переопределяет регистрацию предыдущего, поскольку оба они пишут по одному адресу в реестре.

Чтобы решить эту проблему, не модифицируя 32-разрядные компоненты, реестр делится на две части: Native и Wow64. По умолчанию 32-разрядные

компоненты получают доступ к 32-разрядному представлению реестра, а 64-разрядные - к 64-разрядному представлению. Это создает безопасную среду исполнения для 32- и 64-разрядных компонентов и отделяет состояние 32-разрядных приложений от состояния 64-разрядных (если таковые есть).

Реализуя это решение, Wow64 перехватывает все системные вызовы, открывающие разделы реестра, и модифицирует пути к разделам так, чтобы они указывали на контролируемое Wow64 представление реестра. Wow64 разбивает реестр в следующих точках:

(o) HKLM\Software;

(o) HKEY_CLASSES_ROOT;

(o) HKEY_CURRENT_USER\Software\Classes.

B каждом из этих разделов Wow64 создает раздел с именем Wow6432-Node. B нем сохраняется конфигурационная информация 32-разрядного программного обеспечения. Остальные части реестра 32- и 64-разрядные приложения используют совместно (например, HKLM\System).

При вызове функций RegOpenKeyEx и RegCreateKeyEx приложения могут передавать следующие флаги:

(o) KEY_WOW64_64KEY - для явного открытия 64-разрядного раздела из 32-или 64-разрядного приложения;

(o) KEY_WOW64_32KEY - для явного открытия 32-разрядного раздела из 32-или 64-разрядного приложения.

Для обеспечения взаимодействия через 32- и 64-разрядные СОМ-компо-ненты Wow64 отражает изменения в некоторых частях одного представления реестра на другое. Для этого Wow64 перехватывает операции обновления любого из отслеживаемых разделов в одном из представлений и отражает соответствующие изменения на другое представление. Вот список отслеживаемых разделов:

(o) HKLM\Software\Classes;

(o) HKLM\Software\Ole; (o) HKLM\Software\Rpc;

(o) HKLM\Software\COM3;

(o) HKLM\Software\EventSystem.