4. МУЛЬТИЗАДАЧНОСТЬ В ПРОЦЕССОРЕ I80286
4.1.
Задача и сегмент состояния задачи 4.2.
Переключение задач 4.3.
Синхронизация задач и семафоры 4.4.
Пример мультизадачного монитора В подавляющем большинстве программы, составленные для реального режима процессора, выполняются в однозадачном режиме, полностью монополизируя все ресурсы компьютера. Однако в реальной жизни человеку, работающему с компьютером требуется одновременный доступ к двум или большему числу программ.
Подтверждением этому служит наличие огромного количества резидентных программ - от простейших часов и калькуляторов до сложных резидентных интегрированных сред, аналогичных Borland SideKick.
Резидентные программы, за редким исключением, не реализуют настоящую мультизадачность. Обычно с помощью резидентных программ вы можете только переключаться от одной запущенной программы к другой. Типичный пример "мультизадачной" резидентной программы - часы, которые работают параллельно с другими программами и постоянно показывают время в заранее определённом месте экрана. Другой пример - резидентная программа фоновой печати PRINT, входящая в состав MS-DOS.
Использование резидентных программ - не самый лучший способ организации переключения задач. Это связано с возникновением конфликтов между различными резидентными программами - например, по клавишам их активизации или по используемым прерыванием.
Учитывая необходимость реализации переключения программ, фирма Microsoft в операционной системе MS-DOS версии 5.0 реализовала переключатель программ, встроенный в диалоговую оболочку DOSSHELL. Эта оболочка позволяет запустить на выполнение несколько программ и переключаться от одной к другой. Но активна только одна задача - та, на которую переключился пользователь. Остальные находятся в "замороженном" состоянии.
Однако часто бывает необходимо, чтобы программы работали в режиме разделения времени процессора. В этих случаях нужно использовать операционную систему, работающую в мультизадачном режиме - OS/2, UNIX, XENIX, WINDOWS, DeskView.
Мультизадачность позволяет не только задействовать все ресурсы современных персональных компьютеров, но и существенно повышает производительность труда. Например, вы с помощью модема принимаете файл размером 1-2 мегабайта. Скорость передачи данных по телефонным линиям редко превышает 2400 бит в секунду, поэтому в худшем случае процесс получения файла может растянуться на часы. Без использования мультизадачности ваш компьютер всё это время будет занят только приёмом файла. Более того, в основном он будет находиться в состоянии ожидания, так как передача данных производится очень медленно.
Без мультизадачности вы обречены ждать завершения процесса и не сможете выполнять на компьютере никакую другую работу, что само по себе весьма печально. И это несмотря на то, что компьютер практически ничем не загружен!
Как же можно организовать мультизадачность? Самый простой способ заключается в использовании таймера.
Напомним, что таймер вырабатывает прерывание IRQ0 примерно 18,2 раза в секунду. Операционная система может использовать это прерывание для переключения с одной выполняющейся программы на другую, предоставляя каждой программе квант времени. При этом у пользователя компьютера возникнет иллюзия параллельной работы нескольких программ.
К сожалению, из-за ограниченного объёма книги мы не сможем подробно обсудить все проблемы, связанные с организацией мультизадачности. В конце книги есть список литературы, в которой эти вопросы рассмотрены более подробно. Однако мы тем не менее остановимся на нескольких принципиальных моментах (которые, кстати, напрямую не связаны с защищённым режимом работы процессора).
Первый момент связан с загрузкой программ. В мультизадачной среде одновременно работают несколько программ. Они должны либо все одновременно находиться в оперативной памяти, либо загружаться туда по мере необходимости с магнитного диска.
Если программы загружены в память одновременно, то для каждой из них, разумеется, должна быть выделена своя область памяти. Как для программ, так и для областей данных, принадлежащих этим программам.
Для обеспечения надёжной работы программ было бы также неплохо изолировать эти области памяти друг от друга для предотвращения случайного или преднамеренного доступа за пределы выделенной области памяти.
Второй момент - процесс переключения от выполнения одной программы к выполнению другой. Инициатором этого процесса обычно является таймер, генерирующий периодические прерывания.
При переключении необходимо полностью сохранить контекст выполняемой программы, чтобы в дальнейшем можно было бы продолжить её выполнение с прерванного места. Под контекстом программы мы здесь понимаем содержимое регистров центрального процессора (а также регистров арифметического сопроцессора, если он используется одновременно несколькими программами).
Третий момент связан с необходимостью обеспечения взаимодействия параллельно работающих программ. Программы должны иметь возможность получать доступ к аппаратным ресурсам компьютера, к сервису операционной системы. Кроме того, программы должны уметь обмениваться друг с другом данными и сигнализировать друг другу (а также операционной системе) о возникновении каких либо событий.
При монопольном использовании компьютера в однозадачном режиме все ресурсы компьютера находятся во владении запущенной программы. Но если одновременно работают несколько программ и все они хотят вывести что-нибудь на экран дисплея... В этом случае должен существовать механизм, обеспечивающий совместное использование ресурсов компьютера параллельно работающими программами.
Ситуация напоминает железнодорожный переезд - движение разрешено либо поездам, либо автомобилям, но не одновременно и тем и другим!
Решение проблемы также аналогичное - установка семафора. Только в случае с железной дорогой это настоящий семафор, а в случае мультизадачной операционной системы - это ячейка памяти, отражающая текущее состояние ресурса - свободен или занят. Если ресурс компьютера занят какой либо программой, другие программы должны ждать, пока он не освободится (пока не проедет поезд).
Нет никаких принципиальных препятствий для реализации мультизадачности в реальном режиме. Однако ограничения, присущие реальному режиму не позволяют сделать это достаточно эффективно. Например, ограниченное адресное пространство не позволяет держать в оперативной памяти одновременно несколько больших программ, а подгрузка программ с диска выполняется медленно. Отсуствие какой бы то ни было изоляции адресных пространств программ влечёт за собой ненадёжную работу всей системы в целом, так как любая программа из-за ошибки или преднамеренно может разрушить всю систему.
Поэтому все мультизадачные операционные системы используют защищённый режим работы процессора. Адресация памяти в защищённом режиме полностью удовлетворяет требованиям изолирования параллельно работающих программ.
Например, для каждой программы можно создать свою локальную таблицу дескрипторов LDT. В этом случае программа принципиально будет иметь доступ только к своей области памяти, выделенной ей операционной системой. Для организации межзадачного взаимодействия можно вызывать модули операционной системой через вентили вызова. Кроме того, операционная система может создать области памяти "общего пользования", поместив соответствующие дескрипторы в глобальную таблицу дескрипторов GDT. Таблица GDT одна на все программы и доступна всем программам.
Кроме того, процессор i80286 и более старшие модели имеют специальные средства, значительно ускоряющие переключение с одной программы на другую за счёт автоматического сохранения контекста в специально выделенной для каждой программы области памяти.
В этой главе мы рассмотрим средства мультизадачности процессора i80286 и приведём пример небольшого мультизадачного монитора, обеспечивающего параллельную работу нескольких программ.
До сих пор мы говорили о параллельной работе программ. На самом деле каждая отдельная программа может состоять из нескольких частей, работающих параллельно. Например, текстовый процессор может содержать программные модули, которые параллельно с редактированием текста выполняют нумерацию страниц, печать текста или автоматическое сохранение его на диске. Каждую такую часть программы мы будем называть задачей.
Исходя из этой терминологии в мультизадачной среде одновременно выполняется много задач, принадлежащих разным программам. Причём количество задач больше или равно количеству выполняющихся программ.
Как правило, квантование времени процессора выполняется на уровне задач, а не на уровне программ. По прерыванию таймера процессор переключается от одной задачи к другой, и таким образом осуществляется параллельное выполнение программ.
Для хранения контекста неактивной в настоящей момент задачи процессор i80286 использует специальную область памяти, называемую сегментом состояния задачи TSS (Task State Segment). Формат TSS представлен на рис. 14.
Рис. 14. Формат сегмента состояния задачи TSS.
Сегмент TSS адресуется процессором при помощи 16-битного регистра TR (Task Register), содержащего селектор дескриптора TSS, находящегося в глобальной таблице дескрипторов GDT (рис. 15).
Рис. 15. Дескриптор сегмента состояния задачи TSS.
Поле доступа содержит бит B - бит занятости. Если задача активна, этот бит устанавливается процессором в 1.
Операционная система для каждой задачи создаёт свой TSS. Перед тем как переключиться на выполнение новой задачи, процессор сохраняет контекст старой задачи в её сегменте TSS.
Что же конкретно записывается в TSS при переключении задачи?
Записывается содержимое регистров общего назначения AX, BX, CX, DX, регистров SP, BP, SI, DI, сегментных регистров ES, CS, SS, DS, содержимое указателя команд IP и регистра флажков FLAGS. Кроме того, сохраняется содержимое регистра LDTR, определяющего локальное адресное пространство задачи.
Дополнительно при переключении задачи в область TSS со смещением 44 операционная система может записать любую информацию, которая относится к данной задаче. Эта область процессором не считывается и никак не модифицируется.
Поле Link представляет собой поле обратной связи и используется для организации вложенных вызовов задач. Это поле мы рассмотрим в следующем разделе.
Поля Stack 0, Stack 1, Stack 2 хранят логические адреса (селектор:смещение) отдельных для каждого кольца защиты стеков. Эти поля используются при межсегментных вызовах через вентили вызова.
Для обеспечения защиты данных процессор назначает отдельные стеки для каждого кольца защиты. Когда задача вызывает подпрограмму из другого кольца через вентиль вызова, процессор вначале загружает указатель стека SS:SP адресом нового стека, взятого из соответствующего поля TSS.
Затем в новый стек копируется содержимое регистров SS:SP задачи (т.е. адрес вершины старого стека задачи). После этого в новый стек копируются параметры, количество которых задано в вентиле вызова и адрес возврата.
Таким образом, при вызове привилегированного модуля через вентиль вызова менее привилегированная программа не может передать в стеке больше параметров, чем это определено операционной системой для данного модуля.
Включение адресов стеков в TSS позволяет разделить стеки задач и обеспечивает их автоматическое переключение при переключении задач.
Для переключения задач имеются следующие возможности:
- переключение по команде JMP;
- переключение по команде CALL;
- переключение по прерыванию.
В первом и втором случаях для переключения задачи используются обычные команды JMP и CALL, но в качестве операнда в этих командах указывается адрес сегмента TSS задачи, на которую необходимо переключиться.
Если произошло переключение с первой задачи на вторую при помощи команды JMP, то для возврата к выполнению первой задачи необходимо вновь использовать JMP, указав в качестве операнда адрес TSS первой задачи.
Команда CALL позволяет организовать вызов вложенных задач. Переключившись из первой задачи на вторую, программа может вновь вернуться к первой задаче, если она выполнит команду IRET. В этом случае по команде IRET произойдет обратное переключение задач. Адрес TSS для возврата команда IRET возьмёт из поля обратной связи Link текущего сегмента TSS, куда он был записан командой CALL при первом переключении задач.
Кроме того, при переключении задачи командой CALL в поле FLAGS сегмента TSS вызванной задачи устанавливается в 1 бит вложенной задачи NT. Команда JMP, если она использована для переключения задачи, сбрасывает бит NT. Формат регистра флагов для процессоров i80386 и i80486 описан в приложении. Регистр флагов FLAGS процессора i80286 - это младшее слово 32-разрядного регистра EFLAGS.
Аналогично тому, как можно вызвать подпрограмму через вентиль вызова, для вызова задачи командой CALL можно использовать вентиль задачи. Формат вентиля задачи представлен на рис. 16.
Рис. 16. Вентиль задачи.
Вентили задач, вызываемых по команде CALL, могут располагаться в таблицах GDT или LDT.
Обратите внимание на одно существенное различие между вызовом подпрограммы и вызовом задачи. После возврата из подпрограммы при её повторном вызове мы войдём в процедуру в начальной точке входа. В аналогичном случае при возврате из задачи и её повторном вызове управление будет передано команде, находящейся сразу за командой IRET.
Это происходит потому, что при переключении задачи в сегменте TSS записывается содержимое регистров CS:IP на момент переключения задачи. Если задача была вызвана при помощи команды CALL и возврат (обратное переключение) было выполнено по команде IRET, в TSS записывается адрес CS:IP, указывающий на следующую после IRET команду. Вы можете поместить там команду безусловного переходаЭто происходит потому, что при переключении задачи в сегменте TSS записывается содержимое регистров CS:IP на момент переключения задачи. Если задача была вызвана при помощи команды CALL и возврат (обратное переключение) было выполнено по команде IRET, в TSS записывается адрес CS:IP, указывающий на следующую после IRET команду. Вы можете поместить там команду безусловного перехода JMP на начало задачи и таким образом зациклить задачу. После этого вызов задачи станет похож на вызов подпрограммы.
Существует ещё одна очень интересная возможность для переключения задач - переключение задач по прерыванию.
Эту возможность можно легко реализовать, если поместить вентиль задачи в дескрипторную таблицу прерываний IDT. Например, можно сделать отдельные задачи для обработки исключений или аппаратных прерываний. В последнем случае обработчикам аппаратных прерываний не нужно использовать стек прикладных задач, так как они будут иметь свой собственный стек.
Прежде чем мы приведём конкретный пример простейшей мультизадачной системы, расскажем немного о применении семафоров как средств синхронизации задач.
Как правило, любая мультизадачная операционная система содержит более или менее развитые средства синхронизации и взаимодействия задач. Семафоры как средство синхронизации задач предназначены для управления коллективным доступом со стороны задач к какому либо ресурсу. Под ресурсом мы будем понимать не только физические ресурсы компьютера (диски, клавиатуру и т.д.), но и логические ресурсы - ячейки памяти, буфера и т.п.
Программа, владеющая ресурсом или создавшая ресурс может создать семафор для управления этим ресурсом. Физически семафор реализуется в оперативной памяти и представляет из себя ячейку памяти (достаточно одного байта или иногда даже одного бита памяти).
В простейшем случае для семафора определяются операции:
- создание семафора;
- уничтожение семафора;
- сброс семафора;
- сброс семафора;
- ожидание, пока семафор не будет установлен.
Эти операции выполняются операционной системой по запросам прикладных программ. В примере мультизадачной программы, приведённой ниже в этой главе, мы должны сами реализовать все семафорные операции.
Что касается двух первых операций, то мы выбрали самое простое решение. Семафоры создаются статически на этапе трансляции программы и представляют из себы простые статические переменные. Операция уничтожения семафора в этом случае не используется, т.к. мы создаём все нужные нам семафоры при трансляции программы.
Операции сброса и установки семафора заключаются в записи, соответственно, нуля и единицы в ячейки памяти, распределённые семафорам. Единственная особенность выполнения этих операций заключается в том, что они должны быть непрерываемыми, то есть на время выполнения этих операций необходимо запретить переключение задач. Так как в нашем случае задачи переключаются по прерываниям таймера, мы на время выполнения операций сброса и установки семафора запрещаем все прерывания при помощи команды CLI.
Операция ожидания установки семафора, напротив, должна выполняться с разрешёнными прерываниями, так как семафор ожидает одна задача, а его установку выполняет другая задача. Однако в процессе ожидания необходимо считать содержимое семафора и проверить его состояние, и вот эта операция должна выполняться с запрещёнными прерываниями.
Для того, чтобы вы могли почувствовать мультизадачность, мы подготовили пример программы, реализующей параллельную работу нескольких задач в режиме разделения времени.
Эта программа состоит из нескольких модулей, составленных на языках ассемблера и Си.
Первые два файла предназначены для определения используемых констант и структур данных.
Файл tos.c содержит основную программу, которая инициализирует процессор для работы в защищённом режиме и запускает все задачи. С помощью функции с названием Init_And_Protected_Mode_Entry() мы попадаем в защищённый режим и выводим сообщение на экран о том, что в главной задаче установлен защищённый режим. Регистр TR загружается селектором главной задачи при помощи функции load_task_register().
Сразу после этого программа переключается на выполнение задачи TASK_1. Эта задача просто выводит сообщение о своём запуске на экран и возвращает управление главной задаче. Цель этой процедуры - продемонстрировать процесс переключения задач с помощью команды JMP.
После возврата в главную задачу программа размаскирует прерывания от клавиатуры и таймера. Как только начинают поступать и обрабатываться прерывания таймера, оживают остальные задачи, определённые при инициализации системы.
Начиная с этого момента главная задача разделяет процессорное время наравне с остальными задачами. Что же она делает?
Главная задача сбрасывает семафор с номером 0, вслед за чем ожидает его установку. Этот семафор устанавливается задачей, которая занимается вводом символов с клавиатуры и выводит на экран скан-коды клавиш, а также состояние переключающих клавиш. Как только окажется нажатой клавиша ESC, задача ввода символов с клавиатуры устанавливает семафор 0, что и приводит к завершению работы главной задачи.
Перед тем, как завершить работу, главная задача устанавливает реальный режим работы процессора, стирает экран и возвращает управление операционной системе.
Файл tasks.c содержит тексты программ, которые будут работать в режиме разделения времени (кроме задачи TASK_1, эта задача запускается только один раз).
Обработчик аппаратного прерывания клавиатуры мы взяли практически без изменений из программы, представленной в предыдущей главе. Исходные тексты находятся в файле keyboard.asm
Файл screen.c содержит процедуры, необходимые для вывода информации на экран дисплея. Работа этих процедур основана на непосредственной записи данных в видеопамять.
Последний файл - tossyst.asm - содержит уже знакомые вам процедуры для входа в защищённый режим и возврата обратно в реальный режим. Обратите внимание на процедуры _load_task_register и _jump_to_task, выполняющие загрузку регистра задачи TR и переключение на другую задачу соответственно.