Глава 6. Компьютер и внешние события 6.0 Начало Главы

Мы ждали его слишком долго,
Что может быть глупее, чем ждать?
Б. Гребенщиков

     Практически все функции современных вычислительных систем так или иначе сводятся к обработке внешних событий. Единственная категория приложений, для которых внешние события совершенно неактуальны - это так называемые пакетные приложения, чаще всего - вычислительные задачи. Доля таких задач в общем объеме компьютерных приложений в наше время невелика и постоянно падает. В остальных же случаях, даже если не вспоминать о специализированных управляющих компьютерах, серверы обрабатывают внешние по отношению к ним запросы клиентов, а персональный компьютер - реагирует на действия пользователя. Различие между управляющими системами (приложениями реального времени) и системами общего назначения (термин - система разделенного времени вышел из употребления и не всегда точно отражает суть дела) состоит лишь в том, что первые должны обеспечивать гарантированное время реакции на событие, в то время как вторые "всего лишь" - предоставить хорошее среднее время такой реакции и/или обработку большого количества событий в секунду.
     Единственный способ, которым фон-неймановский компьютер может отреагировать на что бы то ни было - это исполнить программу, последовательность команд. В случае внешнего события, естественным решением ка-жется предоставить команду условного перехода, условием которого является признак события. В системах команд микроконтроллеров часто реализуют именно такие переходы (см. например табл. 2.2). В качестве признака события в этом случае используется значение одного из битов специального регистра, биты которого соответствуют входам микросхемы контроллера. Бит равен единице, если на соответствующий вход подано высокое напряжение, и нулю - если низкое.
     Наличие таких команд полезно, но решает проблему не полностью: да, если событие произошло, мы можем вызвать программу и осуществить реакцию, но что мы будем делать, если его еще не происходило?

Вернуться наверх

6.1. Опрос

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

Пример 6.1. Пример использования режима опроса
; Приведенный фрагмент кода использует опрос таймера TMR0,
; работающего от "часового" кварцевого генератора с частотой 32768Гц.
; Цикл опроса в чистом виде
; TMR0 - регистр таймера,
; TimerValue - просто переменная,
; регистр 0 - аккумулятор, обозначаемый также как W.
; Такой цикл ожидает одного отсчета таймера.
	MovF TMR0, 0
	MovWF TimerValue 
G5H_Continuel
	MovF TimerValue, 0
	SubWF TMR0, 0
BNZ G5H__Continuel
; Код содержит два цикла опроса: первый цикл генерирует 
; сигнал высокого напряжения, второй - низкого.
; В результате получается периодический сигнал, называемый меандром. 
; Фрагмент определителя номера на основе микроконтроллера PIC 
; (с) 1996, Дмитрий Иртегов.

;	Запрос к АТС на выдачу номера.
;	Генератор меандра с частотой 501.5 гц. Выдает 50 периодов (100 мс).
;	Генерирует 2 периода по 16 тиков и один - по 17.
;	Получается очень похоже.

Generate500Hz
	MovLW 50
	MovWF AONByteCounter
	MovLW 3
	MovWF Tmp1
	MovF TMR0, 0
	MovWF TimerValue 
G5H_NextPeriod
	MovLW 8
	AddWF TimerValue, 1
	BSF L1NE_CTL_PORT, LINE_ANSWER 
G5H_Continue1
	MovF TimerValue, 0
	SubWF TMR0, 0
	BNZ G5H_Continue1
	MovLW 8
	AddWF TimerValue, 1
	BCF LINE_CTL_PORT, LINE_ANSWER
	DecFSZ Tmp1, 1 
	GoTo G5H_Continue0
	MovLW 3
	MovWF Tmp1
	IncF TimerValue, 1 
G5H_Continue0
	MovF TimerValue, 0
	SubWF TMR0, 0
	BNZ G5H_Continue0
	DecFSZ AONByteCounter, 1 
	GoTo G5H_NextPeriod
Return
		

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

Вернуться наверх

6.2. Канальные процессоры и прямой доступ к памяти

     Одно из решений состоит в том, чтобы завести отдельный процессор и поручить ему всю работу по опросу. Процессор, занимающийся только организацией ввода-вывода, называют периферийным или канальным (channel).
     Понятно, впрочем, что это повышает стоимость системы и не решает проблемы радикально - теперь вместо флагов, непосредственно сигнализирующих о внешних событиях, центральный процессор вынужден опрашивать флаги, выставляемые канальным процессором. В зависимости от характера событий и требуемой обработки это решение может оказаться и совсем неприемлемым, например, если на каждое событие требуется немедленная реакция именно центрального процессора.
     В противном случае, если немедленно после события требуется лишь простая обработка, а сложные вычисления можно отложить на потом, канальный процессор можно упростить и сделать существенно дешевле центрального.
     Так, при работе с контроллерами дисков, лент и других устройств массовой памяти возникает задача копирования отдельных байтов (или, в зависимости от разрядности шины контроллера, полуслов или слов) из контроллера в память и обратно. Передача одного блока (512 байт у большинства современных контроллеров) состоит из 128 операций передачи слова, идущих друг за другом с небольшими интервалами. Темп передачи данных определяется скоростью вращения диска или движения ленты. Этот темп обычно ниже скорости системной шины, поэтому передача данных должна включать в себя опрос признака готовности контроллера принять или предоставить следующее слово. Интервал между словами обычно измеряется несколькими циклами шины. Нередко бывает и так, что частоты шины и контроллера не кратны, поэтому последовательные слова надо передавать через различное число циклов.
     Дополнительная сложность состоит в том, что, не предоставив вовремя следующее слово для записи, мы испортим весь процесс - эта проблема особенно серьезна на устройствах однократной записи, например прожигателях компакт-дисков. Аналогично, не успев прочитать очередное слово, мы потеряем его и вынуждены будем отматывать ленту назад или ждать следующего оборота диска.
     Видно, что это именно та ситуация, которую мы ранее описывали как показание к использованию режима опроса: поток следующих друг за другом с небольшим интервалом событий, каждое из которых нельзя потерять, а нужно обязательно обработать.
     Обработка события, которая нужна, чтобы избежать такой неприятности, крайне проста, так что устройство, способное с ней справиться, не обязано даже быть полностью программируемым процессором.
     При передаче надо всего лишь убедиться, что блок данных не кончился, взять следующее слово из памяти, дождаться готовности устройства, скопировать слово и вернуться к началу алгоритма. Если блок данных кончился или контроллер выдал ошибку, необходимо сообщить об этом центральному процессору.
     Для реализации этого алгоритма достаточно трех регистров (указателя в памяти, значения текущего слова и счетчика переданных слов). Реализующее этот алгоритм устройство называют контроллером прямого доступа к памяти (Direct Memory Access controller, DMA controller) (рис. 6.1). Такие контроллеры часто рассчитаны на одновременную работу с несколькими устройствами - имеют несколько каналов - и, соответственно, больше регистров. Описание реальной микросхемы контроллера ПДП можно найти в [Паппас/Марри 1993].
     Обычно контроллеры ПДП не считают процессорами, однако без большой натяжки можно сказать, что это все-таки канальный процессор, хотя и очень примитивный. Контроллеры ПДП, рассчитанные на совместную работу с процессором, обладающим виртуальной памятью, часто имеют некий аналог диспетчера памяти ЦП, для того, чтобы позволить операционной системе предоставлять указатель для ПДП в виртуальном адресном пространстве, или, во всяком случае, упростить работу по преобразованию виртуального адреса в физический.
     Различают два типа реализаций ПДП:


     В качестве альтернативы ПДП можно предложить снабжение устройства буфером, который работает с частотой системной шины. Центральный процессор передает данные в буфер, и лишь когда заканчивает передачу, инициирует операцию устройства. Логика работы самого устройства с этим буфером, впрочем, ничем не отличается от ПДП, с той лишь разницей, что используется не общесистемная, а встроенная память. На практике, оба подхода часто используются совместно: ПДП позволяет минимизировать загрузку центрального процессора, а буфер - избежать потери данных, если системная шина занята другим устройством.
     Типичный современный дисковый контроллер имеет и средства ПДП, и внутренний буфер. У кэширующих (имеющих кэш-память) и RAID-контроллеров объем буфера может измеряться многими мегабайтами. Кроме того, современные жесткие диски также имеют собственные буферы.
     Периферийные процессоры находят широкое применение в современных вычислительных системах. Так, типичный современный персональный компьютер, кроме центрального процессора, обычно имеет и специализирован-ный видеопроцессор, так называемый графический ускоритель. У кэширующих дисковых контроллеров и аппаратных реализаций RAID (см. разд. 9.6.2) обычно также есть собственный процессор, в данном случае, как правило, используются полностью программируемые процессоры. Лазерные и струйные печатающие устройства имеют процессор, который интерпретирует команды языка управления принтером (PCL или Postscript), есть процессоры в модемах и во многих других периферийных устройствах. Впрочем, нередко встречаются и попытки обратить процесс децентрализации вычислений - так называемые "софтовые" или Win-модемы (называемые так потому, что программное обеспечение, способное работать с таким модемом, часто поставляется только под Windows), многие бытовые принтеры и т. д.
     В отличие от перечисленных устройств, классический полностью програм-мируемый канальный процессор подключен непосредственно к системной шине и может оперировать несколькими устройствами, в зависимости от загруженной в него канальной программы. Канальные процессоры долгое время считались отличительной особенностью больших ЭВМ. В мини- и микрокомпьютерах использование специализированных канальных процессоров, более сложных, чем контроллер ПДП, считалось неприемлемым по стоимостным показателям. Удивительно, что даже современное ради-кальное удешевление оборудования не изменило положения: предложение консорциума I2O (Intelligent Input/Output) снабжать компьютеры на основе процессоров х86 канальным процессором Intel 960, с энтузиазмом поддержанное практически всеми поставщиками операционных систем, почему-то не было столь же горячо поддержано потребителями.
     Потребители мини- и микросистем, нуждающиеся в высокой производительности, предпочитают использовать в качестве дополнительных процессоров устройства с той же архитектурой, что и центральный процессор. Это называется симметричной многопроцессорностью (SMP), и позволяет перерас-пределять между процессорами не только обработку событий, но и собственно вычислительную деятельность. Понятно, что обрабатывать все события по принципу опроса в такой архитектуре - бессмысленная, а зачастую и нетерпимая расточительность.
     К счастью, еще с 60-х годов, практически все процессоры как центральные, так и канальные, используют стратегию работы с событиями, во многих отношениях гораздо более совершенную, чем опрос.

Вернуться наверх

6.3. Прерывания

     Альтернатива опросу, применяемая практически во всех современных процессорах, называется прерываниями (interrupt), и состоит в значительном усложнении логики обработки команд процессором.
     Процессор имеет один или несколько входов, называемых сигналами или линиями запроса прерывания. При появлении сигнала на одном из входов, процессор дожидается завершения исполнения текущей команды и, вместо перехода к исполнению следующей команды, инициирует обработку прерывания.
     Обработка состоит в сохранении счетчика команд и, возможно, некоторых других регистров (практически всегда сохраняется также слово состояния процессора. В процессорах с виртуальной памятью иногда сохраняются и регистры диспетчера памяти), и в передаче управления на адрес, определяемый типом прерывания. По этому адресу размещается программа, обработчик прерывания, которая и осуществляет реакцию на соответствующее прерыванию событие. Перед завершением обработчик восстанавливает реги-стры, и исполнение основной программы возобновляется с той точки, где она была прервана.
     Как правило, адреса программ, соответствующих различным прерываниям, собраны в таблицу, называемую таблицей векторов прерываний, размещаемую в определенном месте адресного пространства. У микроконтроллеров каждому возможному сигналу прерывания обычно соответствует свой вектор. Процессоры общего назначения часто используют более сложную схему, в которой устройство, запрашивающее прерывание, передает процессору номер прерывания или сразу адрес обработчика.
     Прерывания лишены недостатков, которые мы указали выше для обработки событий при помощи опроса: ожидая события, процессор может заниматься какой-либо другой полезной работой, а когда событие произойдет, он приступит к обработке, не дожидаясь полного завершения этой работы.
     Однако этот механизм имеет и собственные недостатки. В частности, обработка прерывания сопряжена с гораздо большими накладными расходами, чем проверка флага и условный переход в режиме ожидания. У оптимизированных для обработки событий микроконтроллеров разница невелика или даже может быть в пользу механизма прерываний: приведенный в примере 6.1 цикл опроса занимает 5 циклов процессора, а обработчик прерывания у PIC вызывается в течение 3-4 циклов ([www.microchip.com PICMicro] утверждает, что средняя задержка прерывания составляет 3,75 цикла). Таким образом, среднее время реакции на событие в режиме опроса составляет 2,5 цикла (по среднему времени опрос в выигрыше), а максимальное - 5 циклов (в данном случае преимущество на стороне прерываний).
     Однако у процессоров общего назначения, которые при обработке прерывания вынуждены сохранять несколько регистров и осуществлять относительно сложный диалог с вызвавшим прерывание устройством, задержка между установкой сигнала прерывания и исполнением первой команды его обработчика - этот интервал и называется задержкой прерывания (interrupt latency) - составляет десятки тактов.
     Современные суперскалярные процессоры при обработке прерываний вынуждены сбрасывать очередь предварительной выборки команд и, по крайней мере, часть кэшей команд и данных, поэтому у них накладные расходы еще больше. Задержка прерывания у современных реализаций архитектуры х86 лишь ненамного лучше, чем у 80386 хотя по скорости исполнения последовательных программ современные процессоры превосходят 80386 на несколько порядков. Поэтому младшие модели процессоров с архитектурой х86, 8086 и даже 8085, хотя и не находят применения в персональных компьютерах, но продолжают выпускаться для использования во встраиваемых приложениях или в качестве периферийных процессоров.
     Так, например, "марсоход" Sojourner использовал в качестве управляющего процессора 8085 на сапфировой подложке (для обеспечения радиационной устойчивости).
     Это же обстоятельство является дополнительным доводом в пользу включения в систему канальных процессоров, в данном случае с целью освобождения центрального процессора не от опроса, а от обработки прерываний. Разработчики больших компьютеров часто реализовывали канальные процессоры старших моделей на основе центральных процессоров младших моделей той же серии.

Вернуться наверх

6.4. Исключения

     Многие процессоры используют механизм, родственный прерываниям, для обработки не только внешних, но и внутренних событий: мы с вами уже сталкивались с исключительными ситуациями (exception) отсутствия страницы и ошибки доступа в процессорах с виртуальной памятью, а также некоторыми другими - ошибкой шины при доступе к невыровненным словам, заполнению и очистке регистрового окна у SPARC и т. д. Большинство современных процессоров предоставляют исключения при неизвестном коде операции, делении на ноль, арифметическом переполнении или, например, выходе значения операнда за допустимый диапазон в таких операциях, как вычисление логарифма, квадратного корня или арксинуса.
     Исключительные ситуации обрабатываются аналогично внешним прерываниям: исполнение программы останавливается, и управление передается на процедуру-обработчик, адрес которой определяется природой исключения.
     Отличие состоит в том, что прерывания обрабатываются после завершения текущей команды, а возврат из обработчика приводит к исполнению команды, следующей за прерванной. Исключение же приводит к прекращению исполнения текущей команды (если в процессе исполнения команды мы уже успели создать какие-то побочные эффекты, они отменяются), и сохраненный счетчик команд указывает на прерванную инструкцию. Возврат из обработчика, таким образом, приводит к попытке повторного исполнения операции, вызвавшей исключение.
     Благодаря этому, например, обработчик страничного отказа может подка-чать с диска содержимое страницы, вызвавшей отказ, перенастроить таблицу дескрипторов и повторно исполнить операцию, которая породила отказ. Обработчик исключения по неопределенному коду операции может использоваться для эмуляции расширений системы команд.
     Например, при наличии арифметического сопроцессора операции с плавающей точкой исполняются им, а при отсутствии - пакетом эмулирующих подпрограмм. Благодаря этому может обеспечиваться полная бинарная совместимость между старшими (имеющими сопроцессор) и младшими (не имеющими его) моделями одного семейства компьютеров.
     Исключения, возникающие при исполнении привилегированных команд в пользовательском режиме, могут использоваться системой виртуальных машин. Работающее в виртуальной машине ядро ОС считает, что исполняется в системном режиме. На самом же деле оно работает в пользовательском режиме, а привилегированные команды (переключения режима процессора, настройка диспетчера памяти, команды ввода/вывода) приводят к вызову СВМ.
     При грамотной реализации обработчиков таких исключений их обработка произойдет полностью прозрачно для породившей эти исключения программы. Конечно, "подкачка" страницы с диска или программная эмуляция плавающего умножения займет гораздо больше времени, чем простое обращение к памяти или аппаратно реализованное умножение, но, наверное, потребитель вычислительной системы знал, что делал, когда устанавливал недостаточное количество памяти или приобретал машину без сопроцессора.
     Многие другие исключения, такие, как деление на ноль, обычно бессмысленно обрабатывать повторной попыткой деления на какое-то другое число. В этом случае целесообразно возвратить управление не на команду, вызвавшую исключение, а в какую-то другую точку. Вопрос, впрочем, в том, куда именно следует возвращаться. Понятно, что код, который может восстановиться в случае деления на ноль, сильно зависит от контекста, в котором произошла ошибка (пример 6.2).

Пример 6.2. Обработка исключения Floating Underflow (антипереполнение при операциях с плавающей точкой)
#include "setjmp.h
static jmp_buf fpe_retry;

void fpe_handler (int sig) { 
 	_fpreset (); 
 	longjmp(fpe_retry, -1);
}
 
int compare_pgms (Image *  img0,   Image *  img1) {
	int xsize=256,   ysize=256;
	int  i,j,   p0,   p1,   pd;
	double avg, avgsq, scale, smooth;
	
	scale= (double) xsize* ( double) ysize; 
	avg = 0.0; 
	avgsq = 0,0;
	
/* Подавить возможные антипереполнения */ 

	signal (SIGFPE, fpe_handler);
	for(i=0; i < ysize; i++) {
		smooth =  (double) (img0->picture[i*xsize]-imgl->picture[i*xsize]);
		for(j=0; j < xsize;  j++) { 
			p0=img0->picture [ j+i*xsize]; 
			p1=img1->picture[j+i*xsize]; 
			pd=(p0-p1);
			if  (setjmp(fpe_retry) == 0) { 
				smooth = smooth* (1.0-SMOOTH_FACTOR)+(double)pd*SMOOTH_FACTOR;
 				avg +=  smooth; 
 				avgsq += smooth*smooth; 
 			}   else smooth=0.0 ;
 		}
 	}
	if (setjmp(fpe_jcetry) == 0)
		dispersion = avgsq/scale-avg*avg/(scale*scale); 
		else dispersion = 0.0; 
	signal (SIGFPE, SIG__DFL);
}
		

     При программировании на ассемблере это может быть реализовано простой подменой адреса возврата в стеке. Многие языки высокого уровня (ЯВУ) реализуют те или иные средства для обработки исключений. Уровень этих средств различен в разных языках, начиная от пары функций setjmp и longjmp в С [Керниган-Ритчи 2000] (пример 6.3) и заканчивая операторами try/catch и throw C++ [Страуструп 1999] и Java [Вебер 1999].

Пример 6.3. Исходный текст функций setjmp/longjmp.
		
/   setjmp. s   (emx+gcc)   -- Copyright   (с)   1990-1996 by Eberhard Mattes

#include <emx/asra386.h> 

.globl _setjmp,  _longjmp

.text 
ALIGN

#define J_EBX 0 
#define J_ESI 4 
#define J_EDI 8 
#define J_ESP 12 
#define J_EBP 16 
#define J_EIP 20 
#define J_XCP 24

/ Слова со смещениями 28.. 44 зарезервированы / 

int setjmp (jmp_buf here)

_setjmp:
	PROFILE_NOFRAME
	movl l*4(%esp), %edx /* here */
	movl %ebx, J_EBX(ledx)
	movl %esi, J_ESI(%edx)
	movl %edi, J_EDI(%edx)
	movl %ebp, J_EBP(%edx)
	movl %esp, J_ESP(%edx)
	movl 0*4(%esp), %eax  /* Адрес возврата */
	movl %eax, J_EIP(%edx)
	cmpb $0, 	osmode /* OS/2? */
	je If /* No -> skip */
	fs
	movl 0, %eax /* handler Обработчик исключений */
	movl %eax, J_XCP(%edx) 
1: 	xorl %eax, %eax
	EPILOGUE(setjmp)
	
ALIGN 

/ void longjmp (jmp_buf there, int n}


_longjmp:
	PROFILE_NOFRAME
	cmpb $0, __osmode    /* OS/2?  */
	je 2f    /* No -> skip */
	movl l*4(%esp), %eax /* there */
	pushl J_XCP(%eax)
	call __unwind2  /*  восстановить обработчики сигналов */
	addl S4, %esp 
2: 	movl l*4(%esp), %edx  /* there */
	movl 2*4(%esp), %eax /* n */
	testl %eax, %eax
	jne 3f
	incl %eax 
3: 	movl J_EBX(%edx), %ebx
	movl J_ESI(%edx), %esi
	movl J_EDI(%edx), %edi
	movl J_EBP(%edx), %ebp
 	movl J_ESP(%edx), %esp
	movl J_EIP{%edx), %edx
	movl %edx, 0*4(%esp)  /* адрес возврата */
	EPILOGUE(longjmp) /*well, ... */
		

     Исключения в ЯВУ часто позволяют избежать использования нелюбимого структурными программистами оператора goto. В объектно-ориентированных (ОО) языках этот механизм играет еще более важную роль: в большинстве таких языков - это единственный способ сообщить о неудаче при исполнении конструктора объекта.
     Важно подчеркнуть, впрочем, что исключения в смысле ЯВУ и аппаратные исключения процессора - разные вещи. В многозадачной ОС пользовательская программа не имеет непосредственного доступа к обработке прерываний и исключений. ОС предоставляет сервис, позволяющий программисту регистрировать обработчики для тех или иных событий, как соответствующих аппаратным исключениям, так и порождаемых самой операционной системой, но вызов таких обработчиков всегда осуществляется в два этапа: сначала исполняется обработчик, зарегистрированный ядром (пример 6.4), а он, если сочтет нужным и возможным, переключается в пользовательский контекст и вызывает обработчик, зарегистрированный пользователем. Среда исполнения ЯВУ, в свою очередь, может реализовать и свои обработчики между сервисом операционной системы и средствами, доступными программисту.

Пример 6.3. Исходный текст функций setjmp/longjmp.
/*
 * Iinux/arch/i386/traps.c
 *
 * Copyright (С) 1991, 1992 Linus Torvalds
 *
 * Поддержка Pentium III FXSR, SSE
 * Gareth Hughes , May 2000
 */
void die(const char * str, struct pt_regs * regs, long err) 
{
	console_verbose();
	spin_lock_irq(&die_lock);
	printk("%s: %041x\n", str, err & Oxffff);
	show_registers(regs) ;
	
	spin_unlock_irq(&die_lock);
	do_exit(SIGSEGV);
}

static inline void die_if_kernel (const char * str, struct pt_regs * regs, long err)
{
	if (! regs->eflags & VM_MASK) && !(3 & regs->xcs) ) 
		die {str, regs, err);
}

static inline unsigned long get_cr2 {void) 
{ 
	unsigned long address;
	/* получить адрес */
	__asm _ ("movl %%cr2, %0": "=r" (address));
	return address;
}

static void inline do_trap(int trapnr, int signr, char *str, int vm86,
	struct pt_regs * regs, long error_code, siginfo_t *info) 
{ 
	if (vm86 && regs->eflags & VM__MASK)
		goto vm86_trap; 
	if (!(regs->xcs & 3) ) goto kernel_trap;

	trap_signal: {
		struct task_struct *tsk = current;
		tsk->thread.error_code = error_code;
		tsk->thread. trap_no = trapnr;
		if (info) 
			force_sig_info (signr, info, tsk);
		else 
			force_sig (signr, tsk);
		return;
	}
	kernel trap: {
 		unsigned long fixup = search_exception_table (regs->eip); 
 		if (fixup)
			regs->eip = fixup; 
		else
			die(str, regs, error__code}; 
		return;
	}
	vm86_trap: {
		int ret = handle_vm86_trap((struct kernel_vm86_regs *) regs, 
			error_code, trapnr);
		if (ret) goto trap_signal; 
		return;
	}
}

#define DO_ERROR(trapnr, signr, str, name) \
asmlinkage void do_##name (struct pt_regs * regs, long error_code) \ 
{ \ 
	do_trap (trapnr, signr, str, 0, regs, error_code, NULL); \
}

#define DO_ERROR_INFO(trapnr, signr, str, name, sicode, siaddr) \ 
asmlinkage void do_##name(struct pt_regs * regs, long error_code) \ 
{ \
	siginfo_t info; \
	info.si_signo = signr; \
	info.si_errno =0; \
	info.si_code = sicode; \
	info.si_addr = (void *}siaddr; \
	do_trap(trapnr, signr, str, 0, regs, error_code, &info} ; \ 
}

#define DO_VM86_ERROR(trapnr, signr, str, name) \
asmlinkage void do_##name(struct pt_regs * regs, long error_code) \ 
{ \
do_trap(trapnr, signr, str, 1, regs, error_code, NULL); \ 
}

#define DO_VM86_ERROR_INFO(trapnr, signr, str, name, sicode, siaddr) \
 asmlinkage void do_##name (struct pt_regs * regs, long error_code) \
{ \
	siginfo_t info; \ 
	info.si_signo = signr; \ 
	info.si_errno = 0; \ 
	info.si_code = sicode; \ 
	info.si_addr = (void *)siaddr; \ 
	do_trap(trapnr, signr, str, 1, regs, error_code, &info) ; \
}

DO_VM86_ERROR_INFO( 0, SIGFPE, "divide error", divide_error, FPE_INTDIV, regs->eip)
DO_VM86_ERROR( 3, SIGTRAP, "int3", int3) 
DO_VM86_ERROR( 4, SIGSEGV, "overflow", overflow) 
DO_VM86_ERROR( 5, SIGSEGV, "bounds", bounds)
DO_ERROR_INFO ( 6, SIGILL, "invalid operand", invalid_op, ILL_ILLOPN, regs->eip)
DO_VM86_ERROR ( 7, SIGSEGV, "device not available", device_not_available) 
DO_ERROR( 8, SIGSEGV, "double fault", double_fault)
DO_ERROR( 9, SIGFPE, "coprocessor segment overrun", coprocessor_segrnent_overrun)
DO_ERROR(10, SIGSEGV, "invalid TSS", invalid_TSS)
DO_ERROR(11, SIGBUS, "segment not present", segment_not_present)
DO_ERROR(12, SIGBUS, "stack segment", stack_segment)
DO_ERROR_INFO(17, SIGBUS, "alignment check", alignment_check, BUS_ADRALN, get_cr2 ())
		

     Исключения в ЯВУ часто позволяют избежать использования нелюбимого структурными программистами оператора goto. В объектно-ориентированных (ОО) языках этот механизм играет еще более важную роль: в большинстве таких языков - это единственный способ сообщить о неудаче при исполнении конструктора объекта.
     Важно подчеркнуть, впрочем, что исключения в смысле ЯВУ и аппаратные исключения процессора - разные вещи. В многозадачной ОС пользовательская программа не имеет непосредственного доступа к обработке прерываний и исключений. ОС предоставляет сервис, позволяющий программисту регистрировать обработчики для тех или иных событий, как соответствующих аппаратным исключениям, так и порождаемых самой операционной системой, но вызов таких обработчиков всегда осуществляется в два этапа: сначала исполняется обработчик, зарегистрированный ядром (пример 6.4), а он, если сочтет нужным и возможным, переключается в пользовательский контекст и вызывает обработчик, зарегистрированный пользователем. Среда исполнения ЯВУ, в свою очередь, может реализовать и свои обработчики между сервисом операционной системы и средствами, доступными программисту.

Пример 6.4. Обработчик арифметических исключений в ядре Linux
/*
 * Iinux/arch/i386/traps.c
 *
 * Copyright (С) 1991, 1992 Linus Torvalds
 *
 * Поддержка Pentium III FXSR, SSE
 * Gareth Hughes , May 2000
 */
 
void die(const char * str, struct pt_regs * regs, long err) 
{
	console_verbose();
	spin_lock_irq(&die_lock);
	printk("%s: %041x\n", str, err & 0xffff);
	show_registers(regs);
	
	spin unlock irq(&die lock);
	do_exit(SIGSEGV);
}

static inline void die_if_kernel (const char * str, struct pt_regs * regs, long err)
{
	if (!(regs->eflags & VM_MASK) && ! (3 & regs->xcs) ) die {str, regs, err);
}

static inline unsigned long get_cr2 {void) 
{ 
	unsigned long address;
	/* получить адрес */
	__asm _ ("movl %%cr2, %0": "=r" (address));
	return address;
}

static void inline do_trap(int trapnr, int signr, char *str, int vra86,
	struct pt_regs * regs, long error_code, siginfo_t *info) 
{ 
	if (vra86 && regs->eflags & VM__MASK)
		goto vm86_trap; 
	if (! (regs->xcs & 3)) goto kernel_trap;

	trap_signal: {
		struct task_struct *tsk = current;
		tsk->thread.error_code = error_code;
		tsk->thread.trap_no = trapnr;
		if (info) 
			force_sig_info (signr, info, tsk);
		else 
			force_sig (signr, tsk) ;
		return;
	}
	kernel trap: {
 		unsigned long fixup = search_exception_table (regs->eip); 
 		if (fixup)
			regs->eip = fixup; 
		else
			die(str, regs, error_code}; 
		return;
	}
	vm86_trap: {
		int ret = handle_vm86_trap((struct kernel_vm86_regs *) regs, 
			error_code, trapnr) ;
		if (ret) goto trap_signal; 
		return;
	}
}

#define DO_ERROR(trapnr, signr, str, name) \
asmlinkage void do_##name (struct pt_regs * regs, long error_code) \ 
{ \ 
	do_trap (trapnr, signr, str, 0, regs, error_code, NULL); \
}

#define DO_ERROR_INFO(trapnr, signr, str, name, sicode, siaddr) \ 
asmlinkage void do_##name(struct pt_regs * regs, long error_code) \ 
{ \
	siginfo_t info; \
	info.si_signo = signr; \
	info.si_errno = 0; \
	info.si_code = sicode; \
	info.si_addr = (void *}siaddr; \
	do_trap(trapnr, signr, str, 0, regs, error_code, &info} ; \ 
}

#define DO_VM86_ERROR(trapnr, signr, str, name) \
asmlinkage void do_##name(struct pt_regs * regs, long error_code) \ 
{ \
	do_trap(trapnr, signr, str, 1, regs, error_code, NULL); \ 
}

#define DO_VM86_ERROR_INFO(trapnr, signr, str, name, sicode, siaddr) \
 asmlinkage void do_##name (struct pt_regs * regs, long error_code) \
{ \
	siginfo_t info; \ 
	info.si_signo = signr; \ 
	info.si_errno = 0; \ 
	info.si_code = sicode; \ 
	info.si_addr = (void *)siaddr; \ 
	do_trap(trapnr, signr, str, 1, regs, error_code, &info) ; \
}

DO_VM86_ERROR_INFO( 0, SIGFPE, "divide error", divide_error, FPE_INTDIV, regs->eip)
DO_VM86_ERROR( 3, SIGTRAP, "int3", int3) 
DO_VM86_ERROR( 4, SIGSEGV, "overflow", overflow) 
DO_VM86_ERROR( 5, SIGSEGV, "bounds", bounds)
DO_ERROR_INFO ( 6, SIGILL, "invalid operand", invalid_op, ILL_ILLOPN, regs->eip)
DO_VM86_ERROR ( 7, SIGSEGV, "device not available", device_not_available) 
DO_ERROR( 8, SIGSEGV, "double fault", double_fault)
DO_ERROR( 9, SIGFPE, "coprocessor segment overrun", coprocessor_segrnent_overrun)
DO_ERROR(10, SIGSEGV, "invalid TSS", invalid_TSS)
DO_ERROR(11, SIGBUS, "segment not present", segment_not_present )
DO_ERROR(12, SIGBUS, "stack segment", stack_segment)
DO_ERROR_INFO(17, SIGBUS, "alignment check", alignment_check, BUS_ADRALN, get_cr2 ())
		


Вернуться наверх

6.5. Многопроцессорные архитектуры

В дверь диетической столовой
Вошел дракон семиголовый
Он хором "Здравствуйте " сказал
И, улыбаясь, заказал:
- Для этой головы, пожалуйста, халвы!
- Для этой пасти - прочие сласти!
- Для этой головки - перловки!
- Для этой глотки - селёдки!
- Для этой башки - пирожки!
- Для этой рожи - тоже!
- Для этого личика - два сдобных куличика,
Что ещё? Лимонада бутылку,
Семь салфеток, ножик и вилку.
В. Берестов

     Как уже говорилось, относительно большие накладные расходы, связанные с обработкой прерываний, нередко делают целесообразным включение в систему дополнительных процессоров. Есть и другие доводы в пользу создания многопроцессорных вычислительных систем.
     Одним из доводов является повышение надежности вычислительной системы посредством многократного резервирования. Если один из процессоров многопроцессорной системы отказывает, система может перераспределить загрузку между оставшимися. Для компьютеров первых поколений, у кото-рых наработка аппаратуры процессора на отказ была относительно невелика, повышение надежности таким способом часто оказывалось целесообразным, особенно в приложениях, требовавших круглосуточной доступности.
     Другим доводом в пользу включения в систему дополнительных процессоров является тот факт, что алгоритмы, используемые для решения многих при-кладных задач, нередко поддаются распараллеливанию: разделению работы между несколькими более или менее независимо работающими процессорами. В зависимости от алгоритма (и, косвенно, от природы решаемой задачи) уровень достижимого параллелизма может сильно различаться. Отношение производительности системы к количеству процессоров и производительности однопроцессорной машины называют коэффициентом масштабирования. Для различных задач, алгоритмов, ОС и аппаратных архитектур этот коэффициент различен, но всегда меньше единицы и всегда убывает по мере увеличения количества процессоров.
     Некоторые задачи, например, построение фотореалистичных изображений методом трассировки лучей, взлом шифров полным перебором пространства ключей [www.distributed.net] или поиск внеземных цивилизаций [www.seti.org] поддаются масштабированию очень хорошо: можно включить в работу десятки и сотни тысяч процессоров, передавая при этом между ними относительно малые объемы данных. В этих случаях часто оказывается целесообразно даже не устанавливать процессоры в одну машину, а использовать множество отдельных компьютеров, соединенных относительно низкоскоростными каналами передачи данных. Это позволяет задействовать процессоры, подключенные к сети (например, к Интернет) и не занятые в данный момент другой полезной работой.
     Другие задачи, например, работа с базами данных, поддаются распараллеливанию в гораздо меньшей степени, однако и в этом случае обработка запросов может быть распределена между несколькими параллельно работающими процессорами. Количество процессоров в серверах СУБД обычно измеряется несколькими штуками, они подключены к общей шине, совместно используют одну и ту же оперативную память и внешние устройства.
     Многопроцессорность в таких системах обычно применяется только для повышения производительности, но очевидно, что ее же можно использовать и для повышения надежности: когда функционируют все процессоры, система работает быстро, а с частью процессоров работает хоть что-то, пусть и медленнее.
     Некоторые многопроцессорные системы поддерживают исполнение на разных процессорах различных ОС - так, на IBM z90 часть процессоров может исполнять Linux, а остальные - z/OS. В такой конфигурации, работающий под управлением Linux Web-сервер может взаимодействовать с работающим под z/OS сервером транзакций через общую физическую память. Многопроцессорные серверы Sun Fire могут исполнять несколько копий Solaris.
     Промежуточное положение между этими крайностями занимают специализированные массивно-параллельные компьютеры, используемые для таких задач, как численное решение эллиптических дифференциальных уравнений и численное же моделирование методом конечных элементов в геофизических, метеорологических и некоторых других приложениях.
     Современные суперкомпьютеры этого типа (IBM SP6000, Cray Origin) состоят из десятков, сотен, а иногда и тысяч отдельных процессорных модулей (каждый модуль представляет собой относительно самостоятельную вычислительную систему, обычно многопроцессорную, с собственной памятью и, нередко, с собственной дисковой подсистемой), соединенных между собой высокоскоростными каналами. Именно к этому типу относился шахматный суперкомпьютер Deep Blue, выигравший в 1997 году матч у чемпиона мира по шахматам Гарри Каспарова [www.research.ibm.com].
     Многопроцессорные системы различного рода получают все более и более широкое распространение. Если производительность отдельного процессора удваивается в среднем каждые полтора года ("закон Мура" [www.intel.com Moore]), то производительность многопроцессорных систем удваивается каждые десять месяцев [www.sun.com 2001-05].
     На практике, даже хорошо распараллеливаемые алгоритмы практически никогда не обеспечивают линейного роста производительности с ростом числа процессоров. Это обусловлено, прежде всего, расходами вычислительных ресурсов на обмен информацией между параллельно исполняемыми потоками. На первый взгляд, проще всего осуществляется такой обмен в системах с процессорами, имеющими общую память, т. е. собственно многопроцессорных компьютерах.
     В действительности, оперативная память имеет конечную, и небольшую по сравнению с циклом центрального процессора, скорость доступа. Даже один современный процессор легко может занять все циклы доступа ОЗУ, а несколько процессоров будут непроизводительно тратить время, ожидая доступа к памяти. Многопортовое ОЗУ могло бы решить эту проблему, но такая память намного дороже обычной, однопортовои, и применяется лишь в особых случаях и в небольших объемах.
     Одно из основных решений, позволяющих согласовать скорости ЦПУ и ОЗУ, - это снабжение процессоров высокоскоростными кэшами команд и данных. Такие кэши нередко делают не только для центральных процессоров, но и для адаптеров шин внешних устройств. Это значительно уменьшает количество обращений к ОЗУ, однако мешает решению задачи, ради которой мы и объединяли процессоры в единую систему: обмена данными между потоками, исполняющимися на разных процессорах (рис. 6.2).


     Большинство контроллеров кэшей современных процессоров предоставляют средства обеспечения когерентности кэша - синхронизацию содержимого кэш-памятей нескольких процессоров без обязательной записи данных в основное ОЗУ.
     Суперскалярные процессоры, у которых порядок реального исполнения операций может не совпадать с порядком, в котором соответствующие команды следуют в программе, дополнительно усугубляют проблему.
     Другим узким местом многопроцессорных систем является системная шина. Современные компьютеры общего назначения, как правило, имеют шинную архитектуру, т. е. и процессоры, и ОЗУ, и адаптеры шин внешних устройств (PCI и т. д.) соединены общей магистралью данных, системной шиной или системной магистралью. В каждый момент магистраль может занимать только пара устройств, задатчик и ведомый (рис. 6.3). Обычно, задатчиком служит процессор - как центральный, так и канальный - или контроллер ПДП, а ведомым может быть память или периферийное устройство. При синхронизации содержимого кэшей процессорный модуль также может оказаться в роли ведомого.


     Доступ к шине регулируется арбитром шины. Практически применяются две основные стратегии арбитража - приоритетная, когда устройство, имеющее более высокий приоритет, всегда получает доступ, в том числе и при наличии запросов от низкоприоритетных устройств, и справедливая (fair), когда арбитр гарантирует всем устройствам доступ к шине в течение некоторого количества циклов.
     Системы шинной архитектуры просты в проектировании и реализации, к ним легко подключать новые устройства и типы устройств, поэтому такая архитектура получила широкое распространение. Однако, особенно в многопроцессорных системах, шина часто является одним из основных ограничителей производительности. Повышение пропускной способности шины зачастую возможно, но приводит к повышению общей стоимости системы.
     Впрочем, при большом количестве узлов проблемы возникают и у систем со столь высокоскоростной шиной, как Fire Pane. Кроме того, по мере роста физических размеров системы, становится необходимо принимать во внимание физическую скорость передачи сигналов - как сигналов самой магистрали, так и запросов к арбитру шины и его ответов. Поэтому шинная топология соединений при многих десятках и сотнях узлов оказывается неприемлема, и применяются более сложные топологии.
     Архитектура позволяет также включать в эти системы процессоры с архитектурой, отличной от х86, например RS/6000 и System/390, позволяя, таким образом, создать в пределах одной машины гетерогенную сеть со сверхвысокоскоростными каналами связи.
     При большем числе модулей применяются еще более сложные топологии, например гиперкубическая. В таких системах каждый узел обычно также со-держит несколько процессоров и собственную оперативную память (рис. 6.5).
     При гиперкубическом соединении, количество узлов N пропорционально степени двойки, а каждый узел имеет log2N соединений с другими узлами. Каждый узел способен не только обмениваться сообщениями с непосредственными соседями по топологии, но и маршрутизировать сообщения между узлами, не имеющими прямого соединения. Самый длинный путь между узлами, находящимися в противоположных вершинах куба, имеет длину log2N и не является единственным (рис. 6.6). Благодаря множественности путей, маршрутизаторы могут выбирать для каждого сообщения наименее загруженный в данный момент путь или обходить отказавшие узлы.




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


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

Вернуться наверх