January 24, 2019

Что такое CLR – Часть 2

Выполнение в среде CLR

Теперь, когда вы понимаете, из каких элементов состоит исполняемый файл .NET, поговорим о сервисах, предоставляемых CLR для поддержки управления и исполнения .NЕТ-сборок. В CLR имеется много потрясающих компонентов, однако для краткости мы ограничим наше обсуждение только основными из них, показанными на рис. 2.4.Рис. 2.4. Основные компоненты CLR: виртуальная исполняющая система(VES)

Основными компонентами CLR являются загрузчик классов, верификатор, JIТ-компиляторы и средства поддержки исполнения, такие как управление кодом, безопасностью, сборка мусора, управление исключениями, отладкой, маршалингом, потоками и т. д. Как видно из рис. 2.4, РЕ-файлы .NET расположены поверх CLR и исполняются в рамках виртуальной исполняющей системы (VES) CLR, содержащей основные компоненты среды времени выполнения. Ваши РЕ-файлы .NET должны пройти через загрузчик классов, верификатор типов, JIТ-компиляторы и другие компоненты поддержки исполнения перед тем, как они будут выполнены.

Загрузчик классов

Когда запускается стандартное Wiпdоws-приложение, загрузчик ОС загружает его перед тем, как сможет выполнить. На момент написания этого текста загрузчики в существующих операционных системах Windows 98/Ме/2000 распознают только стандартные РЕ-файлы Windows. В результате Мiсrоsоft пришлось предоставить обновленные загрузчики для каждой из этих операционных систем, поддерживающие среду времени исполнения .NET. Обновленные загрузчики ОС понимают формат РЕ-файлов .NET и могут обращаться с файлом должным образом.

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

Загрузчик классов (class loader) загружает .NЕТ-классы в память и готовит их для исполнения. Прежде чем он сможет это сделать, он должен найти требуемый класс. Информация, предоставляемая одним или несколькими источниками, является ключевой для поиска нужного класса. Вспомните, что класс ограничен определенным пространством имен, пространство имен ограничено определенной сборкой, а сборка ограничивается определенной версией. Поэтому два класса, оба имеющие имя Car, рассматриваются как различные классы, даже если информация о версиях их сборок совпадает.

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

Верификатор

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

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

Поговорим о месте, которое занимает верификатор внутри CLR. После того как загрузчик классов загружает класс, но перед тем, как фрагмент IL-кода будет исполнен, верификатор внедряется в проверяемый код. Верификатор отвечает за следующие проверки:

· являются ли метаданные корректными и действительными;

· безопасен ли IL-код в отношении типов, т. е. корректно ли используются сигнатуры типов.

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

JlT -компиляторы

JIТ-компиляторы играют важную роль в платформе .NET, т.к. все РЕ-файлы .NET содержат IL-код и метаданные, а не двоичный код. JIT-компиляторы преобразуют IL в двоичный код, чтобы он мог выполняться в целевой операционной системе. Для каждого метода, успешно проверенного на безопасность типов, JIТ-компилятор CLR компилирует метод и преобразует его в управляемый машинный код. Требуется именно управляемый машинный код, т. к. только им могут управлять и только его могут исполнять компоненты поддержки выполнения в целевой операционной системе.

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

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

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

Для того чтобы исключить накладные расходы, связанные с JIT-компиляцией во время выполнения, можно обратиться к специальной утилите ngen, компилирующей ваш IL-код во время установки. Применение ngen позволяет выполнить JIТ-компиляцию кода один раз и кэшировать его на компьютере, чтобы избежать JIТ-компиляции во время выполнения (этот процесс называется рге-JIТtiпg, или предварительной компиляцией). В том случае, если РЕ-файл был обновлен, необходимо снова выполнить предварительную компиляцию РЕ-файла. В противном случае CLR обнаружит обновление и динамически скомандует соответствующему JIТ-компилятору скомпилировать сборку.

Поддержка и управление исполнением

Как видите, все компоненты CLR, рассмотренные нами до сих пор, тем или иным образом используют метаданные IL для успешного осуществления поддерживаемых ими функций. Кроме предоставления метаданных и генерации управляемого кода, JIТ-компилятор должен генерировать управляемые данные, необходимые диспетчеру кода для поиска и разворачивания стековых фреймов. Диспетчер кoдa (code таnager) использует управляемые данные для управления исполнением кода, в том числе перемещения по стеку, требуемые для обработки исключений, проверок безопасности и сборки мусора. Кроме диспетчера кода CLR также предоставляет несколько важных сервисов для поддержки и управления исполнением:

Сборка мусора

В отличие от С++, где приходится вручную удалять все расположенные в куче объекты, CLR поддерживает автоматическое управление временем жизни всех объектов среды .NET. Сборщик мусора может определить, что на объекты больше нет ссылок и выполнить сборку мусора для освобождения неиспользуемой памяти.

Обработка исключений

До появления .NET не было согласованного метода обработки ошибок или исключений, что создавало много проблем в обработке ошибок и в оповещении о них. В .NET CLR поддерживает стандартный механизм обработки исключений, работающий во всех языках, позволяя всем программам использовать общий механизм обработки ошибок. Механизм обработки исключений в CLR интегрирован со структурной обработкой исключений (Windows Structured Exception Handling, SEH).

Поддержка безопасности

На этапе выполнения CLR осуществляет различные проверки безопасности, чтобы гарантировать, что код безопасен для исполнения и не нарушает никаких требований безопасности.

Поддержка отладки

CLR предоставляет богатую поддержку отладки и профилирования. Это АРI, который может применяться разработчиками компиляторов для создания отладчиков. Он содержит поддержку управления исполнением программы, точек останова, исключений, управляющей логики и т. п. Существует также АРI для поддержки профилирования работающих программ.

Поддержка взаимодействия кода

CLR поддерживает взаимодействие между управляемым (CLR) и неуправляемым (без CLR) «мирами». Средство СОМ Interop играет роль моста, соединяющего СОМ и CLR, обеспечивая СОМ-объекту возможность использовать .NЕТ-объект, и наоборот. Средство Platform 1пvoke (Р/1пvoke) позволяет вызывать функции Windows API.

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