Учебное пособие: Операционные системы "тонких" клиентов
Переносимость в Java
достигается за счет того, что Java-программа компилируется не непосредственно в
команды какой-либо конкретной ЭВМ, а в, так называемый, байт-код Java - команды
некоторой абстрактной машины, называемой виртуальной машиной Java (Java VM),
как показано на рисунке 13.1. Конечным результатом (исполняемым модулем)
является файл класса - программа в байт-коде Java. На целевой платформе (на той
машине, на которой программа выполняется) должна быть запущена программная Java
VM, которая эмулирует ЭВМ, способную выполнять команды байт-кода Java. Сама
Java VM платформенно-зависимая, то есть, предназначена для выполнения на
конкретной платформе и в конкретной операционной системе. Java VM читает
команды байт-кода Java и моделирует их выполнение на той аппаратной платформе и
в той операционной среде, в которой она работает. При этом она использует
библиотеки Java, также платформенно-зависимые. Стержнем технологии являются
спецификации байт-кода Java, файла класса и Java VM. Компиляторы Java могут быть
созданы (и создаются) разными разработчиками, но все генерируемые ими
исполняемые модули должны соответствовать спецификациям байт-кода Java. Более
того, существуют и компиляторы других языков программирования, которые
генерируют байт-код Java. Также различными разработчиками могут разрабатываться
(и разрабатываются) и Java VM, но все Java VM должны выполнять стандартный
байт-код Java.
Рисунок 13.1 Выполнение
приложения в платформе Java
Итак, Java-программа
выполняется в режиме интерпретации. Хотя фирма Sun Microsystems декларирует
эффективность в числе основных свойств Java-программ, в отношении быстродействия
это утверждение, мягко говоря, сомнительно. Интерпретируемая программа в
принципе не может выполняться так же быстро, как программа в целевых кодах.
Эффективность работы Java-программ зависит от эффективности работы Java VM, и
Java VM разных производителей существенно различаются по этому показателю
(лидером является фирма IBM). В составе средств разработки Java имеются также
"своевременные" (just-in-time) компиляторы (JIT), которые транслируют
байт-код Java в коды целевой платформы, результатом чего является исполняемый
модуль в формате целевой платформы и системы. Такой модуль выполняется без
участия Java VM, и его выполнение происходит эффективнее, чем выполнение
интерпретируемого байт-кода, но это уже выходит за пределы платформы Java.
Таким образом,
независимость Java-программ от конкретной аппаратной платформы и ОС достигается
за счет того, что Java-платформа является дополнительной "прослойкой"
между приложением и ОС и вместо специфических системных вызовов API конкретной
ОС приложение использует API JRE или базовые конструкции языка
Ниже мы рассматриваем
некоторые особенности виртуального "процессора" Java VM, как той
платформы, на которой выполняются Java-программы.
13.2 Виртуальная машина
Java
Типы данных, с которыми
работает Java VM, подразделяются на примитивные и ссылочные. Большинство
примитивных типов данных Java VM являются также примитивными типами в языке
Java. К ним относятся:
byte - 1-байтное целое со
знаком;
short - 2-байтное целое
со знаком;
int - 4-байтное целое со
знаком;
long - 8-байтное целое со
знаком;
float - 4-байтное число с
плавающей точкой;
double - 8-байтное число
с плавающей точкой;
char - 2-байтный символ
Unicode.
В отличие от других
языков программирования, размеры типов в языке Java и в Java VM являются
постоянными, не зависящими от платформы.
Java VM не оперирует
типом boolean, являющимся примитивным типом языка Java. Для выражений языка,
оперирующих этим типом, компилятор Java генерирует коды, оперирующие типом int.
Примитивный тип
returnAddress в Java VM не имеет соответствия в языке Java. Тип returnAddress
представляет собой указатель на команду байт-кода Java и используется в
качестве операнда команд передачи управления.
Ссылочные типы в Java VM
и в языке Java являются ссылками (указателями) на объекты - экземпляры классов,
массивы и интерфейсы (экземпляры классов, реализующих интерфейсы). Спецификации
Java VM не определяют внутренней структуры объектов, в большинстве современных
Java VM ссылка на объект является указателем на дескриптор объекта, в котором,
в свою очередь содержатся два указателя:
на объект типа Class,
представляющий информацию типа, в том числе методы и статические данные класса;
на память, выделенную для
локальных данных объекта в куче.
Все указатели, с которыми
работает Java VM, являются указателями в плоском 32-разрядном адресном
пространстве, хотя в реализациях Java VM для 64-разрядных платформ могут
использоваться и 64-разрядные указатели.
Основные области памяти,
с которыми работает Java VM, показаны на рисунке 13.2.
Рисунок 13.2 Основные
области памяти Java VM
Область памяти,
называемая кучей, разделяется на две части: область классов и область
динамически распределяемой памяти (иногда кучей называют только эту часть
памяти). Куча создается при запуске Java VM. Конкретные реализации Java VM
могут обеспечивать управление начальным размером кучи и расширение кучи при
необходимости.
Класс является основной
программной единицей платформы Java, объединяющей в себе данные и методы их
обработки. При загрузке класса для него выделяется память в области классов.
Каждый класс представляется двумя структурами памяти: областью методов и пулом
констант. Область методов содержит исполняемую часть класса - байт-коды методов
класса, а также таблицу символических ссылок на внешние методы и переменные.
Пул констант содержит литералы класса.
В области динамического
распределения выделяется память для размещения объектов. Управление этой
областью памяти мы рассматриваем в отдельном разделе.
Java VM поддерживает
параллельное выполнение нескольких нитей. Для каждой нити при ее создании Java
VM создает набор регистров и стек нити.
Набор регистров включает
в себя четыре 32-разрядных регистра:
pc - регистр-указатель на
команду;
optop - регистр-указатель
на вершину стека операндов текущего кадра;
var - регистр-указатель
на массив локальных переменных текущего кадра;
frame - регистр-указатель
на среду выполнения текущего метода.
Стек нити представляет
собой стек в традиционном понимании, то есть, списковую структуру данных,
обслуживаемую по дисциплине "последним пришел - первым ушел".
Элементами стека являются кадры (frame) методов. В традиционных блочных языках
программирования при помощи стека обеспечиваются вложенные вызовы процедур.
Аналогичным образом Java VM через стек нити обеспечивает вложенные вызовы
методов, представляя каждый метод кадром в стеке. Новый кадр создается и
помещается в вершину стека при вызове метода. Кадр, расположенный в вершине
стека является текущим, он соответствует методу, выполняемому в нити в текущий
момент. При возврате из метода его кадр удаляется из стека. Управление
начальным размером стека нити и возможность его динамического расширения
зависит от реализации Java VM. Спецификации Java VM не требуют размещения стека
нити в непрерывной области памяти.
Кадр, как было сказано,
создается динамически и содержит три основных области.
набор локальных
переменных экземпляра класса, на который ссылается регистр var;
стек операндов, на
который ссылается регистр optop;
структуры среды
выполнения, на которую ссылается регистр frame.
Эти области показаны на
рисунке 13.3.
Рисунок 13.3 Структура и
связи кадра
Набор локальных переменных
представляет собой массив 32-разрядных слов. Данные двойной точности (типы long
и double) занимают по два смежных слова в этом массиве. Размер этого массива
фиксирован для метода, так как число локальных переменных метода становится
известным уже на этапе компиляции. Операнды команд байт-кода, которые оперируют
локальными переменными, представляются индексами в этом массиве.
Java VM является стековой
машиной. Это означает, что в ней нет регистров общего назначения, и операции
производятся над данными, находящимися в стеке. Этой цели служит стек
операндов, выделяемый в составе каждого кадра. При выполнении команд байт-кода
Java, изменяющих данные, операнды таких команд выбираются из стека операндов, в
тот же стек помещаются и результаты выполнения команд.
Среда выполнения метода
содержит информацию, необходимую для динамического связывания, возврата из
метода и обработки исключений. Код класса (размещенный в области класса)
обращается к внешним методам и переменным, используя символические ссылки. Динамическая
компоновка переводит символические ссылки в фактические. Среда выполнения
содержит ссылки на таблицу символов метода, через которую производятся
обращения к внешним методам и переменным.
В среде выполнения
содержится также информация, необходимая для возврата из метода: указатель на
кадр вызывающего метода, значение регистра pc для возврата, содержимое
регистров вызывающего метода и указатель на область для записи возвращаемого
значения.
Информация обработки
исключений содержит ссылки на секции обработки исключений в методе класса.
Через среду выполнения
также происходят обращения к данным, содержащимся в области класса, в том
числе, к константам и к переменным класса.
Команды Java VM состоят
из однобитного кода операции, а также могут содержать операнды. Число и размер
операндов определяются кодом операции, некоторые команды не имеют операндов.
Основной алгоритм работы Java VM сводится к простейшему циклу, приведенному на
рисунке 13.4.
Рисунок 13.4 Основной
цикл работы Java VM
Каждый из типов данных
Java VM обрабатывается своими командами. Основные типы команд Java VM:
Команды загрузки и
сохранения, в том числе:
загрузка в стек локальной
переменной;
сохранение значения из
стека в локальной переменной;
загрузка в стек константы
(из пула констант).
Команды манипулирования
значениями (большинство этих операций работают с операндами из стека и помещают
результат в стек), в том числе:
арифметические операции;
побитовые логические
операции;
сдвиг;
инкремент (операция
работает с операндом - локальной переменной).
Команды преобразования
типов.
Команды создания
ссылочных данных и доступа к ним, в том числе:
создания экземпляров
класса;
доступа к полям класса;
создания массивов;
чтения в стек и
сохранения элементов массивов;
получения свойств
массивов и объектов.
Команды прямого
манипулирования со стеком.
Команды передачи
управления, в том числе:
безусловный переход;
условный переход;
переход по множественному
выбору.
Команды вызова методов и
возврата (включая специальные команды вызова синхронизированных методов).
Команды генерации и
обработки исключений.
Принятые в спецификациях
Java VM структуры данных и алгоритмы таковы, что позволяют реализовать
виртуальную машину с минимальными затратами памяти и сделать ее работу
максимально эффективной.
Другим ключевым элементом
спецификаций Java является файл класса. Каждый файл класса описывает один класс
или интерфейс. Файл класса содержит поток байт, структурированный определенным
образом. Все реализации компилятора Java должны генерировать файлы классов,
структура которых соответствует определенной в спецификациях. Все реализации
Java VM должны "понимать" структуру файлы класса, соответствующую
определенной в спецификациях.
Основные компоненты файла
класса следующие:
Некоторая верификационная
информация: "магическое число" - сигнатура файла класса, номер
версии.
Флаг доступа,
отображающий модификаторы, заданные в определении класса (public, final,
abstract и т.д.), а также признак класса или интерфейса.
Пул констант - таблица
структур, представляющих различные строковые константы - имена классов и
интерфейсов, полей, методов и другие константы, на которые есть ссылки в файле
класса.
Ссылки на имена
this-класса и суперкласса в пуле констант.
Перечень интерфейсов,
реализуемых классом (в виде ссылок в пул констант).
Описание полей класса с
указанием их имен, типов, модификаторов и т.д.
Методы класса - каждый
метод представляется в виде определенной структуры, в которой содержится
описание метода (имя, модификаторы, и т.д.), одним из атрибутов этой структуры
является массив байт-кодов метода.
Многие компоненты файла
класса (пул констант, перечень интерфейсов и др.) имеют нефиксированную длину,
такие компоненты предваряются 2-байтным полем, содержащим их длину.
13.3 Многопоточность и
синхронизация
Java, по-видимому,
является единственным универсальным языком программирования, в котором
механизмы создания нитей поддерживаются встроенными средствами языка. В традиционных
языках программирования (например, C) создание нитей обеспечивается
системно-зависимыми библиотеками, обеспечивающими API ОС. В Java средства
создания нитей системно-независимые.
В Java-программе нить
представляет отдельный класс, который может быть создан
либо как подкласс
(наследник) суперкласса Tread;
либо как класс,
реализующий интерфейс Runnable, внутри этого класса должна быть переменная
экземпляра класса - ссылка на объект класса Tread.
Суперкласс Tread и
интерфейс Runnable определены в базовой библиотеке языка Java - пакете
java.lang. При любом варианте создания в классе-нити должен быть реализован
метод run(). Выполнение метода start() для экземпляра такого класса вызывает
выполнения метода run() в отдельном потоке вычисления.
Как мы увидели в
предыдущем разделе, Java VM обеспечивает для каждой нити собственную среду
вычисления - собственный набор регистров и стек (в некоторых реализациях Java
VM обеспечивает для нити также и собственную кучу).
Но Java VM не выполняет
действий по планированию нитей на выполнение. Для этого библиотечные методы
Java обращаются к ОС, используя API той ОС, в среде которой работает Java VM.
Для нитей в Java
предусмотрено управление приоритетами (методы getPriority(), setPriority()),
однако, и здесь Java использует механизмы управления приоритетами ОС. Так, уже
классическим для учебников по Java является пример аплета, в котором по экрану
"наперегонки" движутся несколько объектов, движение каждого
осуществляется в отдельной нити и с собственным приоритетом. Этот пример весьма
наглядно демонстрируется в средах, например, OS/2 и Linux, но выглядит не очень
убедительным в среде Windows 95/98, так как приоритет нити не слишком влияет на
скорость движения объекта - примерно так, как показано на рисунке 13.5.
Рисунок 13.5
"Гонки" в разных операционных средах (движение справа налево).
Любая нить может быть
сделана нитью-"демоном". Нить-"демон" продолжает
выполняться даже после окончания той нити, в которой она была создана. Нить
становится "демоном" при ее создании только в том случае, если она
создается из нити-"демона". Программа может изменить состояния нити
при помощи метода setDaemon() класса Thread. Выполнение любой программы Java VM
начинает с единственной нити (в которой вызывается метод main()), и эта нить
запускается не как "демон". Java VM продолжает существовать, пока не
завершатся все нити не-"демоны".
В языке Java имеется
также класс ThreadGroup - группа нитей, которая может управляться совместно.
Если в Java предусмотрены
нити, то, естественно, должны быть предусмотрены и средства синхронизации и
взаимного исключения при параллельной работе нитей. Основным средством
синхронизации и взаимного исключения в Java является ключевое слово synchronized,
которое может употребляться перед каким-либо программным блоком. Ключевое слово
synchronized определяет невозможность использования программного блока двумя
или более нитей одновременно. В Java synchronized-блоки называются мониторами и
их фактическая тождественность мониторам Хоара, описанным в разделе 8.8 части
I, очевидна. В зависимости от деталей способа употребления synchronized может
работать как:
защищенная (guard - см.
раздел 8.8 части I) процедура - в том случае, если synchronized-блок
представляет собой целый метод, однако, в отличие от описанных нами
guard-процедур одновременное вхождение в разные synchronized-методы возможно;
анонимные скобки
критической секции - в том случае, если synchronized-блок является просто
программным блоком;
скобки критической секции
с защитой выбранного ресурса - в том случае, если после ключевого слова
synchronized указывается в скобках ссылка на объект.
В первоначальной версии
языка Java для класса Thread предусмотрены методы:
resume() - приостановить
выполнение нити;
suspend() - возобновить
выполнение нити;
yeld() - сделать паузу в
выполнении нити, чтобы дать возможность выполниться другой нити;
join() - ожидать
завершения нити.
Эти средства позволяют
синхронизировать работу нитей, но в следующих версиях был (наряду со старыми
средствами) введен новый, более стройный аппарат синхронизации и взаимного
исключения.
Класс Object имеет три
метода:
wait() - ожидать
уведомления об этом объекте;
notify() - послать
уведомление одной из нитей, ждущих уведомления об этом объекте;
notifyAll() - послать
уведомление всем из нитям, ждущим уведомления об этом объекте.
Поскольку класс Object
является корнем иерархии классов, объекты всех - стандартных и пользовательских
- классов являются его подклассами и наследуют эти методы. Эти методы
аналогичны операциям wait и signal, описанным нами в разделе 8.7 части I.
Реализация, например, общего (с возможным значением, большим 1) семафора с
использованием этих средств будет выглядеть следующим образом:
//** семафор реализуется
в виде класса Semaphore
public class Semaphore
{
// значение семафора
private int
Semaphore_value;
//** пустой конструктор семафора, по
умолчанию начальное значение семафора - 0
public Semaphore()
{
this(0);
}
//** конструктор с параметром
- начальным значением семафора,
// если задано
отрицательное значение, устанавливается начальное значение 0
public Semaphore(int val)
{
if (val <
0) Semaphore_value = 0
else
Semaphore_value = val;
}
//** V-операция. V- и P-операции объявлены synchronized,
// чтобы исключить
одновременное выполнение их двумя или более нитями
public synchronized void
V()
{
// возможно пробуждает
нить, ожидающую у семафора
if (Semaphore_value == 0)
this.notify();
// увеличивает значение семафора
Semaphore_value++;
}
//** P-операция
public
synchronized void P() throws InterruptedException
// исключение может выбрасываться в
wait
{
// если значение
семафора равно 0, блокирует нить
while (counter == 0)
this.wait();
// уменьшает значение
семафора
Semaphore_value--;
}
}
В Java VM с каждым
объектом связывается замок (lock) и список ожидания (wait set).
В спецификациях байт-кода
Java имеются специальные команды monitorenter и monitorexit, устанавливающие и
снимающие замок. Java VM, входя в synchronized-блок, пытается выполнить
операцию установки замка и не продолжает выполнения нити, пока операция не
будет выполнена. При выходе из synchronized-блока выполняется операция снятия
замка.
Список ожидания
используется методами wait(), notify(), notifyAll(). Он представляет собой
список нитей, ожидающих уведомления о данном объекте. Названные операции
работают с этим списком очевидным образом.
Следует отметить, что
многопоточность, заложенная в языке Java, имеет большие перспективы. Изначально
сама идея нитей (а ее первая коммерческая реализация была сделана именно фирмой
Sun Microsystems) возникла как решение, призванное обеспечить эффективную
загрузку оборудования в многопроцессорных системах, но теперь свойства
многопоточности, закладываемые в программное обеспечение, оказывают (наряду с
другими факторами) обратное влияние на процессорные архитектуры, стимулируя
развитие в них средств распараллеливания вычислительного процесса.
Так, новый проект
компьютерной архитектуры фирмы Sun Microsystems носит название MAJC
(Microprocessor Architecture for Java Computing - архитектура микропроцессора
для Java-вычислений), которое говорит само за себя. В концепцию этой
архитектуры (как, впрочем, и ряда других новых архитектур) входит
многопроцессорная обработка с несколькими "потоковыми устройствами" в
каждом процессоре. Такая архитектура призвана обеспечить более эффективную
обработку на сетевом сервере современных потоков данных, которые
характеризуются возрастанием удельного веса в них мультимедийной информации.
Для получения лучшей
производительности, кроме многопоточных микропроцессоров, разработчики MAJC
применяют технологию, называемую "пространственно-временными
вычислениями" или "предположительной многопоточности". В этой
технологии прикладной программист вообще не будет беспокоиться о
распараллеливании своих программ, потому что эта работа будет сделана за него
Java VM.
Смысл
пространственно-временных вычислений сводится к следующему. Java VM проверяет
программу и предполагает, что два метода могут выполняться одновременно на двух
процессорах. Она посылает метод A на первый процессор, а метод B - на второй
для предположительного вычисления. Поскольку B - предположительный метод, он
выполняется в отдельном адресном пространстве, называемом предположительной
памятью. Если все идет хорошо, и никакие зависимости в данных не нарушены, то
предположительная память сливается с основной памятью и программа обрабатывает
следующую пару методов. Если произошло нарушение, то второй метод отменяется и
предположительная память "выбрасывается".
Идея
пространственно-временных вычислений не является кардинально новой, но она не
применялась в обычных многопроцессорных системах без многопоточности, так как
позволяла увеличить эффективность выполнения программ в 2-процессорной
конфигурации не более, чем на 5%. Разработчики MAJC рассчитывают, что в их
архитектуре производительность последовательных приложений на двух процессорах
должна возрасти в 1.6 раза.
13.4 Управление памятью в
куче
Управление памятью
относится к числу тех свойств языка Java, которые заложены в само его ядро и
непосредственно обеспечиваются Java VM. Особенностью управления памятью в Java
является то, что с точки зрения прикладного программиста его практически нет.
Память для данных примитивных типов выделяется в области локальных переменных
кадра. Кадр для метода выделяется только на время выполнения метода, при
завершении выполнения память кадра освобождается, а следовательно, и
освобождаются и все локальные переменные. Этот механизм подобен размещению
локальных переменных в стеке в традиционных блочных языках программирования
(C/C++, PL/1 и т.д.). Ссылочные типы состоят из двух частей: ссылки на объект и
собственно тела объекта. Массивы в Java также являются ссылочным типом, и все,
что далее говорится про объекты, справедливо и для массивов. Ссылка
представляет собой адрес памяти, указатель на объект в терминах языка C/C++, но
в отличие от C/C++, адресная арифметика в Java не разрешена. Объект в программе
доступен только через переменную, являющуюся ссылкой на него.
Итак, память, выделяемая
для ссылок, управляется автоматически, как и память для примитивных типов.
Иначе обстоит дело с памятью, выделяемой для тела объекта. В языке Java имеется
операция new, которая явным образом выделяет память для тела объекта и
возвращает ссылку на созданный объект. Память для каждого объекта выделяется
явным образом, при помощи этой операции. Создание новых объектов возможно также
неявным образом - некоторые библиотечные методы создают (при помощи той же
операции new) новый объект и возвращают ссылку на созданный объект. На этом
"заботы" прикладной Java-программы об управлении памятью
заканчиваются. Программа не освобождает выделенную память, это делает за нее
Java VM. Автоматическое освобождение памяти, занимаемой уже ненужными
(неиспользуемыми) объектами, - одна из наиболее интересных особенностей
платформы Java. Это освобождение выполняется в Java VM программным механизмом,
который называется сборщиком мусора (garbage collector). Но что такое
неиспользуемый объект? Программа может "оставить объект в покое" на
долгое время, а потом вдруг вновь вернуться к нему. Время обращения к объекту
(как это делается в дисциплине управления памятью LRU) не может служить
показателем ненужности объекта. Сборщик мусора считает неиспользуемыми те
объекты, на которые нет ссылок. Если в программе нет ссылки на объект, то
программа принципиально не может обратиться к объекту, следовательно, объект
представляет собой мусор. Обратите внимание на то обстоятельство, что при
выходе из блока, в котором был создан объект, освобождается память, занимаемая
ссылкой на объект, но это еще не значит, что объект сразу же становиться
мусором. Ссылка на созданный объект может быть присвоена внешней по отношению к
данному блоку переменной или быть возвращаемым значением метода. Если же этого
не происходит, то объект действительно становится мусором. При выполнении
Java-программы такой мусор в памяти накапливается. Многие методы библиотечных
классов Java (например, класса String) построены таким образом, что их
использование способствует интенсивному накоплению мусора в памяти.
Когда накопление мусора
приводит к нехватке памяти, вступает в действие сборщик мусора. Для обеспечения
работы сборщика мусора в дескрипторе каждого объекта имеется "признак
мусора". При создании объекта "признак мусора" устанавливается
во взведенное состояние. Алгоритм работы сборщика мусора (один из его
вариантов) состоит из двух фаз:
Фаза маркировки. Сборщик
мусора просматривает области локальных переменных всех активных в настоящий
момент методов, а также поля всех доступных объектов. В дескрипторах тех
объектов, на которые есть ссылки в просмотренных областях "признак
мусора" сбрасывается
Фаза очистки.
Просматривается область кучи, дескрипторы всех объектов. Те объекты,
"признак мусора" которых оказывается взведенным (не был сброшен в
фазе маркировки), являются мусором, занимаемая ими память освобождается. У тех
же объектов, "признак мусора" которых сброшен, этот признак взводится
- для подготовки к следующей сборке мусора.
Затраты на выполнение
сборки мусора практически не зависят от количества мусора - в любом случае
требуется полный просмотр и областей локальных переменных, и кучи.
Следовательно, сборку мусора выгоднее производить только в те моменты, когда
мусора накопится много: в этом случает при тех же затратах будет получен
больший результат. Поэтому операция сборки мусора может создавать некоторую
проблему при выполнении Java-программ. Проблема состоит в том, что момент
активизации сборщика мусора непредказуем, а когда такая активизация произойдет,
она вызовет задержку в вычислениях. В новых реализациях Java VM эту проблему
стараются если не решить кардинально, то несколько сгладить, запуская сборщик
мусора в отдельной низкоприоритетной нити.
Хотя область методов тоже
формально принадлежит куче, в большинстве современных Java VM сборщик мусора в
этой области не работает.
13.5 Защита ресурсов
Поскольку одной из
основных сфер применения технологии Java является Internet, вопросы
безопасности для этой технологии приобретают особое значение. Безопасность в
сетевой среде представляет собой целый комплекс сложных вопросов,
рассматриваемых в отдельном курсе. Здесь же мы уделим основное внимание защите
локальных ресурсов - анализу возможности Java-программы получить несанкционированный
доступ к ресурсам на том компьютере, на котором она выполняется.
Прежде всего, в самих
языковых средствах Java отсутствуют некоторые возможности языка C/C++, которые
наиболее часто приводят к неправильному использованию ресурсов - случайному или
намеренному. Главная черта языка Java в этом отношении - отсутствие указателей.
Хотя доступ к объектам в Java осуществляется по ссылкам, и физический смысл
ссылки и указателя C/C++ одинаков - адрес памяти, ссылка не есть указатель.
Различие состоит в том, что, во-первых, ссылка не может быть преобразована в
число или какое-либо иное представление физического адреса, во-вторых, над
ссылками недопустимы арифметические операции. Именно адресная арифметика в
C/C++ является средством, использование которого может привести к доступу
процесса за пределы той области памяти, к которой он имеет право обращаться.
Другой
"лазейкой" для выполнения несанкционированных действий в языке C/C++
является слабая защита типов. C/C++ позволяют использовать типы данных в
операциях, этому типу не свойственных - путем неявного преобразования типов или
путем приравнивания разнотипных указателей (в том числе, и для интегрированных
типов). В Java осуществляется строгий контроль типов и в большинстве случаев
требуется явное преобразование типов.
Автоматическое
освобождение памяти в Java также является свойством, повышающим защищенность.
Можно говорить также и о том, что более последовательное воплощение в Java
парадигмы объектно-ориентированного программирования также является выигрышным
обстоятельством с точки зрения защиты.
Следует оговорить, что
указанные различия между языками C/C++ и Java обусловлены прежде всего тем, что
языки ориентированы на разные сферы применения. Те "недостатки" языка
C/C++, на которые мы указываем, превращаются в уникальные достоинства при
применении С/С++ в качестве языка системного программирования, а именно таково
первоначальное предназначение этого языка. При разработке же приложений (а Java
- язык именно для разработки приложений) эти возможности становятся ненужными и
даже опасными.
Однако сами свойства
языка Java еще не являются гарантией защищенности. Они обеспечиваются
компилятором Java, но не предохраняют от модификации исполняемый модуль.
Поскольку спецификации байт-кода Java и файла класса открыты, программы,
осуществляющие несанкционированный доступ, могут писаться непосредственно в
байт-кодах или на других языках с компиляцией в байт-код Java. Чтобы перекрыть
этот канал несанкционированного доступа, в платформе Java выполняется
верификация байт-кода.
Процесс верификации
состоит из четырех шагов.
Шаг 1 выполняется при
загрузке класса. При этом Java VM проверяет базовый формат файла класса -
"магическое число" и номер версии, соответствие размера файла
суммарному размеру его составляющих, формальное соответствие отдельных структур
спецификациям.
Шаг2 выполняется при
связывании, он включает в себя верификацию без анализа байт-кодов. На этом шаге
проверяется:
отсутствие нарушений в
использовании классов и методов, объявленных с модификатором final;
наличие у каждого класса
(кроме класса Object) суперкласса;
соответствие
спецификациям содержимого пула констант;
правильность имен классов
и интерфейсов и дескрипторов всех полей и методов, ссылающихся на пул констант.
Проверки правильности
элементов файла класса, выполняемые на этом шаге, - только формальные, не
семантические. Более подробные проверки выполняются на следующих шагах.
Шаг 3 также выполняется
на этапе связывания. На этом шаге верификатор проверяет массив байт-кодов
каждого метода. При этом анализируется поток данных, обрабатывающийся при
выполнении метода. Верификатор исходит из того, что в любой точке программы,
независимо от того, каким образом управление попало на эту точку, должны
соблюдаться определенные ограничения целостности данных, которые сводятся в
основном к следующим:
размер стека операндов
неизменен и стек содержит операнды одного типа;
не выполняется доступ к
локальным переменным неизвестного типа;
доступ к локальным
переменным осуществляется только в пределах массива локальных переменных;
все обращения к пулу
констант производятся к элементам соответствующего типа;
полям класса назначаются
значения соответствующего типа;
все команды байт-кода
используются с операндами (в стеке или в массиве локальных переменных) типа, соответствующего
типу команды;
методы вызываются с
правильными аргументами;
команды перехода передают
управление только внутри байт-кода метода и передача управления всегда
происходит только на первый байт команды байт-кода.
Шаг 4 выполняется при
первом вызове кода любого метода. Это "виртуальный шаг", он
выполняется не в виде отдельного шага проверки всего байт-кода, а при
выполнении каждой отдельной команды.
Для команды, которая
ссылается на тип, при этом:
загружается определение
типа (если оно еще не загружено);
проверяется, может ли
текущий выполняемый метод ссылаться на этот тип;
выполняется инициализация
класса (если он еще не инициализирован).
Для команды, которая
вызывает метод или осуществляет доступ к полю класса, при этом:
проверяется, существует
ли поле или метод в данном классе;
проверяется правильность
дескриптора вызванного метода или поля;
проверяется, имеет ли
текущий выполняемый метод права доступа к этому методу или полю.
В конкретных реализациях
Java VM допускается после выполнения шага 4 заменять проверенную команду
байт-кода альтернативной "быстрой" формой. Например, в Sun Java VM
команда байт-кода new может быть заменена командой new_quick.
"Быстрая" команда выполняется так же, как и исходная, но при ее выполнении
исключается повторная верификация команды. В файле класса "быстрые"
команды не допускаются, они выявляются на предыдущих шагах верификации и
вызывают отказ. "Быстрые" формы не являются спецификациями Java, они
реализуются в конкретной Java VM.
Аплеты являются наиболее
критическими с точки зрения безопасности Java-программами, поскольку аплет
загружается из Internet, возможно, из непроверенного источника. Естественно,
недопустимым является предоставление программе, пришедшей "неизвестно
откуда" доступа к ресурсам локального компьютера. Поэтому для аплетов
введены весьма жесткие ограничения на выполнение. Аплету запрещается:
получать сведения о
пользователе или его домашней директории;
определять свои системные
переменные;
работать с файлами и
директориями на локальном компьютере (читать, изменять, создавать и т.д. и даже
проверять существование и параметры файла);
осуществлять доступ по
сети к удаленному компьютеру, получать список сетевых сеансов связи, которые
устанавливает локальный компьютер с другими компьютерами;
открывать без уведомления
новые окна, запускать локальные программы и загружать локальные библиотеки,
создавать новые нити, получать доступ к группам нитей другого аплета;
получать доступ к любому
нестандартному пакету, определять классы, входящие в локальный пакет.
Модель безопасности Java
еще далека от совершенства, и в ее реализациях иногда обнаруживаются
"лазейки" для несанкционированного проникновения. Следует отметить,
что в сетевых публикациях довольно часто можно встретить критику безопасности в
Java и предупреждение о принципиальной возможности "взлома" защиты
Java тем или иным способам. Вместе с тем, сетевые публикации не дают оснований
говорить о том, что реальные информационные системы, в которых применяется
технология Java, чаще подвергаются взлому, чем системы, эту технологию не
применяющие.
13.6 JavaOS и Java для
тонких клиентов
В конце 90-х годов фирма
Sun Microsystems предприняла разработку новой ОС, базирующейся на технологии
Java - JavaOS. Для доводки этой ОС фирма Sun привлекла фирму IBM, и конечный
продукт JavaOS является собственностью обеих этих фирм.
JavaOS является
операционной системой для широкого спектра вычислительных средств, включая
сетевые и встроенные компьютеры. Целью разработки этой ОС являлось
предоставление среды для выполнения Java-приложений без использования базовой
универсальной ОС.
JavaOS строится по
принципу многослойной архитектуры, показанной на рисунке 13.6, включающей в
себя платформенно-зависимую и платформенно-не
Рисунок 13.6 Архитектура
JavaOS
Платформенно-зависимая
часть состоит из загрузчика, микроядра, виртуальной машины Java и частично -
среды выполнения Java (JavaOS Runtime Environment).
Функции загрузчика
вытекают из его названия. JavaOS ориентирована прежде всего на клиент/серверную
модель вычислений. Это означает, что необходимое программное обеспечение,
постоянно хранящееся на клиентской стороне - минимальное, загрузчик и
составляет тот необходимый и достаточный минимум программного обеспечения,
который обеспечивает загрузку всего остального программного обеспечения с
сервера.
Микроядро JavaOS очень
похоже на микроядра ОС, рассмотренных нами выше (QNX, AMX RTOS и др.). Оно
выполняет функции:
обработки прерываний и
исключений;
поддержки множественных
нитей;
поддержки
многопроцессорных конфигураций;
управления реальной
памятью;
управления реальными
устройствами и каналом ПДП.
JVM обеспечивает:
интерпретацию байт-кода;
управление выполнением;
управление памятью;
нити;
загрузку классов;
верификацию байт-кода.
Программное обеспечение
среды выполнения Java частично создается в кодах Java, частично - в
"родных" (native) кодах целевой платформы. В состав среды выполнения
входит JVM и ряд системных менеджеров, в том числе:
Менеджер Конфигурации,
представляющий собой первый класс, Java-кода, выполняемый JVM, он обеспечивает
запуск компонентов Менеджера Платформы и дополнительных сервисов JavaOS;
Менеджер Платформы,
обеспечивающий запуск и поддержку компонентов, обслуживающих
платформенно-зависимые устройства и шину ввода-вывода платформы;
Менеджер Сервисов, пакет,
обеспечивающий поиск и запуск сервисных утилит JavaOS;
Менеджер Устройств,
компонент, обеспечивающий архитектуру Java-интерфейса устройств (JDI);
Классы Java-интерфейса
платформы (JPI), инкапсулирующие драйверы JDI и решение платформенно-зависимых
вопросов, включая управление временем, памятью и прерываниями.
Дополнительные
(опционные) компоненты среды выполнения включают в себя компоненты конфигурации
(персональной, сетевой, встроенной), наборы драйверов и средства отладки.
Вся среда выполнения
(включая JVM) работает как один процесс в виртуальном адресном пространстве.
Соответствие виртуального адресного пространства физической памяти
обеспечивается микроядром. Также микроядро обеспечивает использование
многопроцессорной архитектуры вычислительной системы для функционирования среды
выполнения и приложений. Вся специфика управления процессорами и памятью
инкапсулирована в JPI.
Большая часть драйверов
устройств JavaOS пишется на языке Java. Платформенная независимость драйверов
поддерживается компонентом JDI, который состоит из:
Менеджера Событий,
обеспечивающего взаимодействие с устройствами по событийной модели;
Системной Базы Данных,
обеспечивающей хранение и получение конфигурационной информации (относящейся к
ОС, устройствам и приложениям) в едином репозитории;
платформенно-зависимых
блоков драйверов;
Менеджера Шины.
Следующий, полностью
платформенно-независимый уровень составляют сервисы JavaOS, такие как: классы,
обеспечивающие базовую графику, ввод-вывод и сетевые коммуникации для
платформы.
Более высокие уровни
составляют стандартные пакеты Java, пакет расширенного графического интерфейса
Swing и, наконец, пользовательские приложения.
К сожалению, JavaOS
"не успела" на рынок тонких клиентов, к тому моменту, когда эта ОС
поступила в продажу, рынок мобильных клиентов, на который она могла
претендовать, был уже занят, в основном, Windows CE, также сложились уже и
операционные среды для сетевых компьютеров, например, IBM Workspace on Demand
для OS/2 и Windows. Поэтому фирмы-производители "законсервировали"
проект и его конечный продукт - JavaOS - не представлен на рынке.
Опыт разработки JavaOS
фирма Sun Microsystems использовала для создания концепции EmbeddedJava [17].
Технология EmbeddedJava является надстройкой над ОС (любой ОС) тонкого клиента
и включает в себя JVM и библиотеку классов Java. Отличие от базовой технологии
Java состоят в том, что и JVM, и библиотека классов являются конфигурируемыми,
то есть, их объем минимизируется таким образом, чтобы в них включались только
те свойства, которые необходимы и достаточны для выполнения Java-приложений
конкретного тонкого клиента. Фирма Sun обеспечивает набор инструментальных
средств для создания такой компактной прикладной среды, в состав которых
входят:
JavaFilter - инструмент
для выявления тех классов, полей и методов, которые необходимы для
функционирования приложения;
JavaCodeCompact -
инструмент для оптимизации кода приложения для экономии RAM- и ROM-памяти;
JavaDataCompact -
инструмент для компактного представления внешних по отношению к приложению
данных.
После определения
необходимых компонент и создания компактных кодов и данных приложение совместно
с компонентами среды выполнения компилируется в коды целевой платформы
(native-коды), которые могут быть помещены в RAM- или ROM-память
"тонкого" устройства. Общий ход процесса разработки приложения
EmbeddedJava показан на рисунке 13.7.
Рисунок 13.7 Процесс
разработки приложения EmbeddedJava
13.7 Перспективы
технологий Java
Технологии Java были на
подъеме несколько последних лет, можно предполагать, что их интенсивное
развитие и влияние на другие информационные технологии - явление
долговременное. В настоящее время стандарты технологий Java открыты, в их
развитии, наряду с Sun Microsystems, активно участвуют ведущие производители
рынка информационных технологий (IBM, Hewlett-Packard, Oracle и другие). Фирма
Microsoft, хотя и производит конкурирующие технологии, также вынуждена
считаться с технологиями и стандартами Java. В условиях распространения среды
сетевых вычислений технология Java является одним из главных средств
обеспечения совместной работы в глобальном информационном пространстве
аппаратных и программных средств от разных производителей. Другим таким
средством, обеспечивающим совместимость по данным, является язык XML, с которым
Java сейчас интегрируется (стандарт Java Standard Extension for XML).
Первый успех технологий
Java был обеспечен прежде всего аплетами, то есть, программами, выполняющимися
на удаленном клиенте. Нынешнее же развитие и перспективы этих технологий
связаны в основном с серверным программным обеспечением. Основные стандарты
этого направления: Enterprise JavaBeans (EJB) - компонентная архитектура
построения расширяемых, многоуровневых, распределённых приложений для серверов
и Java 2 Platform, Enterprise Edition (J2EE), расширение возможностей
межплатформенной переносимости EJB. Важной является также достигнутая
совместимость Java с реляционными базами данных (стандарты SQLJ и JDBC,
развиваемые под эгидой ISO).
Развитие
"тонких" клиентов сети заставляет технологии Java вновь обратиться и
"истокам" (проект Oak) - построению программного обеспечения для
неполнофункциональных клиентских устройств. Поскольку "тонкие"
клиенты отличаются большим разнообразием аппаратных и программных платформ,
именно Java может стать той технологией, которая позволит таким клиентам
интегрироваться в глобальное информационное пространство. Технология Jini,
которая базируется на Java-технологии, позволяет работать с любыми устройствами
и отказаться от традиционного использования разнообразных драйверов,
громоздкого системного программного обеспечения, привязанного к аппаратным
платформам и не позволяющего устройствам взаимодействовать в гетерогенной сети.
Jini - это сетевая инфраструктура, набор соглашений, специфицирующих методы
автоматического взаимодействия и регистрации устройств, подключаемых к сети.
Таким образом, не исключено,
что ближайшие годы развития информационных технологий пройдут "под
знаменем" технологий Java.
|