• из пользовательского или системного потока, выполняющего явную операцию файлового ввода-вывода;
• из подсистем записи модифицированных и спроецированных страниц, принадлежащих диспетчеру памяти;
• неявно из подсистемы отложенной записи, принадлежащей диспетчеру кэша;
• неявно из потока опережающего чтения, принадлежащего диспетчеру кэша;
• из обработчика ошибок страниц, принадлежащего диспетчеру памяти.
Явный файловый ввод-вывод
Наиболее очевидный способ доступа приложения к файлам — вызов Windows-функций ввода-вывода, например CreateFile, ReadFile и WriteFile. Приложение открывает файл с помощью CreateFile, а затем читает, записывает и удаляет его, передавая описатель файла, возвращенный CreateFile, другим Windows-функциям. CreateFile, реализованная в Kernel32.dll, вызывает встроенную функцию NtCreateFile и формирует полное имя файла, обрабатывая символы «.» и «…» и предваряя путь строкой «\??» (например, \??\C: \Daryl\Todo.txt).
Чтобы открыть файл, системный сервис NtCreateFile вызывает функцию ObOpenObjectByName, которая выполняет разбор имени, начиная с корневого каталога диспетчера объектов и первого компонента полного имени («??»). B главе 3 дано подробное описание разрешения имен диспетчером объектов, а здесь мы поясним, как происходит поиск букв диска для томов.
Первое, что делает диспетчер объектов, — транслирует \?? в каталог пространства имен, индивидуальный для сеанса, в котором выполняется данный процесс; на этот каталог ссылается поле DosDevicesDirectory в структуре карты устройств (device map structure) в объекте «процесс». B системах Windows 2000 без Terminal Services поле DosDevicesDirectory ссылается на каталог \?? а в системах Windows 2000 без Terminal Services карта устройств ссылается на индивидуальный для каждого сеанса каталог, где хранятся объекты «символьная ссылка», представляющие все действительные буквы дисков для томов. Однако в Windows XP и Windows Server 2003 в таком каталоге обычно содержатся лишь имена томов для общих сетевых ресурсов, поэтому в этих OC диспетчер объектов, не найдя имя (в данном примере — G) в индивидуальном для сеанса каталоге, переходит к поиску в каталоге, на который ссылается поле GlobalDosDevicesDirectory карты устройств, сопоставленной с индивидуальным для сеанса каталогом. GlobalDosDevicesDirectory всегда указывает на каталог \Global?? где Windows XP и Windows Server 2003 хранят буквы дисков для локальных томов. (Подробнее о пространстве имен сеанса см. одноименный раздел в главе 3-)
Символьная ссылка для буквы диска, присвоенной тому, указывает на объект тома в каталоге \Device, поэтому диспетчер объектов, распознав объект тома, передает остаток строки с именем в функцию IopParseDevice, зарегистрированную диспетчером ввода-вывода для объектов «устройство». (Ha томах динамических дисков символьная ссылка указывает на промежуточную ссылку, которая в свою очередь указывает на объект тома.) Ha рис. 12-9 показано, как происходит доступ к объектам томов через пространство имен диспетчера объектов. B данном случае (система Windows 2000 без Terminal Services) символьная ссылка \??\C: указывает на объект тома \Device\HarddiskVolumel.
Заблокировав контекст защиты вызывающего потока и получив информацию о защите из его маркера, IopParseDevice генерирует пакет запроса ввода-вывода (IRP) типа IRP_MJ_CREATE, создает объект «файл», в котором запоминается имя открываемого файла, и по ссылке в VPB объекта тома находит объект «устройство» смонтированной файловой системы тома. Далее, используя IoCallDriver, она передает IRP драйверу файловой системы, которому принадлежит данный объект «устройство».
Когда FSD получает IRP типа IRP_MJ_CREATE, он ищет указанный файл, проверяет права доступа и, если файл есть и пользователь имеет права на запрошенный вид доступа к файлу, возвращает код успешного завершения. Диспетчер объектов создает в таблице описателей, принадлежащей процессу, описатель объекта «файл», который передается назад по цепочке вызовов, в конечном счете достигая приложения в виде параметра, возвращаемого CreateFile. Если файловой системе не удается создать файл, диспетчер ввода-вывода удаляет созданный для него объект «файл».
Мы опустили здесь детали, относящиеся к тому, как FSD находит открываемый на томе файл. B выполнении ReadFile ядро участвует в той же мере, что и в выполнении CreateFile, но в этом случае системному cepвucy NtReadFiIe не приходится искать имя — он вызывает диспетчер объектов для трансляции описателя, переданного ReadFile, в указатель на объект «файл». Если описатель открытого файла указывает на наличие у вызывающего потока прав на чтение файла, NtReadFile создает IRP типа IRPMJREAD и посылает его драйверу файловой системы, в которой находится файл. NtReadFile получает объект FSD, хранящийся в объекте «файл», и вызывает IoCallDriver. Диспетчер ввода-вывода находит FSD с помощью объекта FSD и передает IRP этому драйверу файловой системы.
Если считываемый файл может быть кэширован (т. е. при открытии файла в функцию CreateFile не передан флаг FILE_FLAG_NO_BUFFERING), FSD проверяет, инициировано ли кэширование для этого объекта «файл». Если да, поле PrivateCacheMap объекта «файл» указывает на структуру закрытой карты кэша (см. главу 11). B ином случае поле PrivateCacheMap будет пустым. Кэширование объекта «файл» инициируется FSD при первой операции записи или чтения над этим объектом, для чего FSD вызывает функцию CcIni-tializeCacheMap диспетчера кэша, и диспетчер кэша создает закрытую и общую карты кэша, а также объект «раздел» (если это еще не сделано).
Убедившись, что кэширование файла разрешено, FSD копирует данные запрошенного файла из виртуальной памяти диспетчера кэша в буфер, указатель на который передан функции ReadFile вызывающим потоком. Файловая система выполняет копирование в рамках блока try/except, что позволяет перехватывать все ошибки, которые могут возникнуть, если приложение указало неверный буфер. Для копирования файловая система использует функцию CcCopyRead диспетчера кэша, которая принимает в качестве параметров объект «файл», смещение внутри файла и длину данных.
Диспетчер кэша, выполняя CcCopyRead, получает указатель на общую карту кэша, хранящуюся в объекте «файл». Вспомните из главы 11, что эта карта хранит указатели на блоки управления виртуальными адресами (VACB) и что один элемент VACB соответствует 256-килобайтному блоку файла. Если VACB-указатель для считываемой части файла пуст, CcCopyRead создает VACB, резервируя в виртуальном адресном пространстве диспетчера кэша 256-ки-лобайтное представление, и проецирует на это представление указанную порцию файла (с помощью MmMapViewInSystemCacbe). Затем CcCopyRead просто копирует данные файла из спроецированного представления в переданный ей буфер (буфер, изначально переданный в ReadFile). Если файловых данных в физической памяти нет, операция копирования вызывает ошибки страниц, обслуживаемые MmAccessFault.
Когда возникает ошибка страницы, MmAccessFault изучает виртуальный адрес, вызвавший ошибку, и находит дескриптор виртуального адреса (VAD) в дереве VAD вызвавшего ошибку процесса (подробнее о дереве VAD см. главу 7). B данном случае VAD описывает представление считываемого файла, проецируемое диспетчером кэша, поэтому для обработки ошибки страницы, вызванной действительным виртуальным адресом, MmAccessFault вызывает MiDispatcbFault, которая сначала находит область управления (на нее указывает VAD) и уже через нее отыскивает объект «файл», представляющий открытый файл. (Если файл открывался более чем один раз, возможно наличие списка объектов «файл», связанных указателями в закрытых картах кэша.)