Глава 4. Управление оперативной памятью.

Пример 4.3 приведен в усеченном виде (без ф-ции free). Если кому надо, дошлю отдельно.

СОДЕРЖАНИЕ

Основной ресурс системы, распределением которого занимается ОС - это оперативная память. Поэтому организация памяти оказывает большое влияние на структуру и возможности ОС. В настоящее время сложилась даже более интересная ситуация - переносимая операционная система UNIX, рассчитанная на машины со страничным диспетчером памяти, произвела жесткий отбор, и теперь практически все машины общего назначения, начиная от х86 и заканчивая суперкомпьютерами или, скажем процессором Alpha, имеют именно такую организацию адресного пространства.

4.1. Абсолютная загрузка.

Самый простой вариант управления памятью - отсутствие диспетчера памяти и возможность загружать в системе только один процесс. Именно так работают СР/М и RT-11 SJ (Single-Job, однозадачная). В этих системах программы загружаются с фиксированного адреса prog_start. В СР/М это 0х100; в RT-11 - 01000. По адресам от 0 до начала программы находятся векторы прерываний, а в RT-11 - также и стек программы. Операционная система размещается в старших адресах памяти. Адрес sys_start, с которого она начинается, зависит от количества памяти у машины и от конфигурации ОС (рис. 4.1).

В этом случае управление памятью со стороны системы состоит в том, что загрузчик проверяет, поместится ли загружаемый модуль в пространство от prog_start до sys_start. Если объем памяти, который использует программа, не будет меняться во время ее исполнения, то на этом все управление и заканчивается.

Однако программа может использовать динамическое управление памятью, например функцию malloc о или что-то в этом роде. В этом случае уже код malloc() должен следить за тем, чтобы не залезть в системные адреса. Как правило, динамическая память начинает размещаться с адреса prog_end = prog_start + prog_size. prog_size в данном случае обозначает полный размер программы, т. е. размер ее кода, статических данных и области, выделенной под стек.

Функция malloc () поддерживает некоторую структуру данных, следящую за тем, какие блоки памяти из уже выделенных были освобождены. При каждом новом запросе она сначала ищет блок подходящего размера в своей структуре данных и, только когда этот поиск завершится неудачей, просит новый участок памяти у системы. Для этого используется переменная, которая в библиотеке языка С называется brk_addr (рис. 4.2, пример 4.1).

Изначально эта переменная равна prog__end, ее значение увеличивается при выделении новых блоков, но в некоторых случаях может и уменьшаться. Это происходит, когда программа освобождает блок, который заканчивается на текущем значении brkievel.

Пример 4.1. Выделение дополнительной памяти в GNU LibC для Linux
morecore.с:
/* Copyright (С) 1991, 1992 Free Software Foundation, Inc. 
Этот файл является частью библиотеки С проекта GNU (GNU С Library).

GNU С Library является свободным программным обеспечением;
вы можете передавать ее и/или модифицировать ее в соответствии
с положениями GNU General Public License версии 2 или (по вашему выбору)
любой более поздней версии.

Библиотека GNU С распространяется в надежде, что она будет полезна, 
но БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ; даже без неявно предполагаемых гарантий 
КОММЕРЧЕСКОЙ ЦЕННОСТИ или ПРИГОДНОСТИ ДЛЯ КОНКРЕТНОЙ ЦЕЛИ. 
Подробнее см. GNU General Public License. Вы должны были получить копию 
GNU General Public License вместе с GNU С Library; см. файл COPYING. 
Если вы ее не получили, напишите по адресу: 
Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#ifndef	_MALLOC_INTERNAL
#define	_MALLOC_INTERNAL 
#include 
#include 
#include  
#include 
#include 
#endif

#ifndef	__GNU_LIBRARY__
#define	__sbrk	sbrk 
#endif

extern void * __brk addr;
extern __ptr_t __sbrk__P ((int increment));
extern int   init _brlk __P ((voiq));

#ifndef NULL 
#define NULL 0 
#endif

/* Выделить еще INCREMENT байтов пространства данных
и возвратить начало пространства данных или NULL при 
ошибках. Если INCREMENT отрицателен, сжать пространство 
данных. */

__ptr_t
__default_morecore (ptrdiff_t increment) 
{
	__ptr_t result = __sbrk ((int) increment);
	if (result == (__ptr_t) -1) 
		return NULL;
	return result;
}

/* Эта функция почти полностью аналогична __default_morecore ().
Но она вызывается только однажды через __morecore. */

__ptr_t
__default_morecore_init (ptrdiff_t increment)
{
	__ptr_t result;
	if ( __init_brk() != 0) 
		return NULL;
	if ( __morecore == __default_morecore_init) 
		_morecore = default morecore;
	result = __sbrk ((int) increment);
	if (result == (__ptr_t) -1) 
		return NULL;
	return result;
}
	

4.2. Алгоритмы динамического управлени памятью.

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

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

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

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

В стандартных библиотечных функциях языков высокого уровня, таких как malloc/free/realloc в С, new/dispose в Pascal и т. д., как правило, используются алгоритмы, рассчитанные на наиболее общин случай: программа запрашивает блоки случайного размера в случайном порядке и освобождает их также случайным образом.

Впрочем, случайные запросы - далеко не худший вариант. Даже не зная деталей стратегии управления кучей, довольно легко построить программу, которая "испортит жизнь" многим распространенным алгоритмам (пример 4.2).

Пример 4.2. Пример последовательности запросов памяти
while(TRUE) 
{ 
	void * b1 = malloc(random(10));

	/* Случайный размер от 0 до 10 байт */

	void * b2 = malloc (random (10)+10);

	/* ........ от 10 до 20 байт */

	if (bl == NULL && b2 == NULL) /* Если памяти нет */ 
		break;   				 /* выйти из цикла */
	free(b1) ;
}

void * b3 = malloc(150);

/* Скорее всего, память не будет выделена */
	

В результате исполнения такой программы вся доступная память будет "порезана на лапшу": между любыми двумя свободными блоками будет размещен занятый блок меньшего размера (рис. 4.4).

К счастью, пример 4.2 имеет искусственный характер. В реальных программах такая ситуация встречается редко, и часто оказывается проще исправить программу, чем вносить изменения в универсальный алгоритм управления кучей.

Приведенный пример построен на том предположении, что система выделяет нам блоки памяти, размер которых соответствует запрошенному с точностью до байта. Если же минимальная единица выделения равна 32 байтам, никакой внешней фрагментации наш пример не вызовет: на каждый запрос будет выделяться один блок. Но при этом мы столкнемся с обратной проблемой, которая называется внутренней фрагментацией, если система умеет выделять только блоки, кратные 32 байтам, а нам реально нужно 15 или 47 байт, то 17 байт на блок окажутся потеряны (рис. 4.5).

Чем больше размер единицы выделения, тем меньше нам грозит фрагментация внешняя, и тем большие потери обеспечивает фрагментация внутренняя. Величина потерь зависит от среднего размера запрашиваемого блока. Грубая оценка свидетельствует о том, что в каждом блоке в среднем теряется половина единицы выделения, т. е. отношение занятой памяти к потерянной будет:

,где N - количество выделенных блоков, s - размер единицы выделения, а l - средний размер блока. Упростив эту формулу, мы получим выражение для величины потерь:

,т. е. потери линейно растут с увеличением размера единицы выделения.

Если средний размер блока сравним с единицей выделения, наша формула теряет точность, но все равно дает хорошую оценку порядка величины потерь. Так, если s=l, наша формула дает 50% потерь, что вполне согласуется со здравым смыслом: если запрашиваемый блок чуть короче минимально возможного, теряется только это "чуть"; зато если он чуть длиннее, то для него отводится два минимальных блока, один из которых теряется почти весь. Точная величина потерь определяется распределением запрашиваемых блоков по длине, но мы предпочитаем оставить вывод точной формулы любопытному читателю.

Варианты алгоритмов распределения памяти исследовались еще в 50-е годы. Итоги многолетнего изучения этой проблемы приведены в [Кнут 2000] и многих других учебниках.

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

Поиск в списке может вестись тремя способами: до нахождения первого подходящего (first fit) блока, до блока, размер которого ближе всего к заданному - наиболее подходящего (best fit), и, наконец, до нахождения самого большого блока, наименее подходящего (worst fit).

Использование стратегии worst fit имеет смысл разве что в сочетании с сортировкой списка по убыванию размера. Это может ускорить выделение памяти (всегда берется первый блок, а если он недостаточно велик, мы с чистой совестью можем сообщить, что свободной памяти нет), но создает проблемы при освобождении блоков: время вставки в отсортированный список пропорционально 0(п), где п - размер списка. Помещать блоки в отсортированный массив еще хуже - время вставки становится 0(п + log(n)) и появляется ограничение на количество блоков. Использование хэш-таблиц или двоичных деревьев требует накладных расходов и усложнений программы, которые себя в итоге не оправдывают. На практике стратегия worst fit используется при размещении пространства в файловых системах, например в HPFS, но ни одного примера ее использования для распределения оперативной памяти автору неизвестно.

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

В общем случае best fit увеличивает фрагментацию памяти. Действительно, если мы нашли блок с размером больше заданного, мы должны отделить "хвост" и пометить его как новый свободный блок. Понятно, что в случае best fit средний размер этого "хвоста" будет маленьким, и мы в итоге получим большое количество мелких блоков, которые невозможно объединить, так как пространство между ними занято.

В тех ситуациях, когда мы размещаем блоки нескольких фиксированных размеров, этот недостаток роли не играет и стратегия best fit может оказаться оправданной. Однако библиотеки распределения памяти рассчитывают на общий случай, и в них обычно используются алгоритмы first fit.

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

Разработчик программы динамического распределения памяти обязан решить еще одну важную проблему, а именно - объединение свободных блоков. Действительно, обидно, если мы имеем сто свободных блоков по одному килобайту и не можем сделать из них один блок в 100 килобайт. Но если все эти блоки расположены в памяти один за другим, а мы не можем их при этом объединить - это просто унизительно.

Кроме того, если мы умеем объединять блоки и видим, что объединенный блок ограничен сверху значением brklevel, то мы можем, вместо помещения этого блока в список, просто уменьшить значение brklevel и, таким образом, вернуть ненужную память системе.

Представим себе для начала, что все, что мы знаем о блоке, - это его начальный адрес и размер. Легко понять, что это очень плохая ситуация. Действительно, для объединения блока с соседями мы должны найти их в списке свободных, или же убедиться, что там их нет. Для этого мы должны просмотреть весь список. Как одну из идей мозгового штурма можно выдвинуть предложение сортировать список свободных блоков по адресу.

Гораздо проще запоминать в дескрипторе блока указатели на дескрипторы соседних блоков. Немного развив эту идею, мы приходим к методу, который называется алгоритмом парных меток и состоит в том, что мы добавляем к каждому блоку по два слова памяти- Именно слова, а не байта. Дело в том, что требуется добавить достаточно места, чтобы хранить в нем размер блока в байтах или словах. Обычно такое число занимает столько же места, сколько и адрес, а размер слова обычно равен размеру адреса. На х8б в реальном режиме это не так, но это вообще довольно странный процессор.

Итак, мы добавляем к блоку два слова - одно перед ним, другое после него. В оба слова мы записываем размер блока. Получается своеобразный дескриптор, который окружает блок. При этом мы устанавливаем, что значения длин будут положительными, если блок свободен, и отрицательными, если блок занят. Можно сказать и наоборот, важно только потом соблюдать это соглашение (рис. 4.7).

Представим, что мы освобождаем блок с адресом addr. Считаем, что addr имеет тип word *, и при добавлении к нему целых чисел результирующий адрес будет отсчитываться в словах, как в языке С. Для того чтобы проверить, свободен ли сосед перед ним, мы должны посмотреть слово с адресом addr - 2. Если оно отрицательно, то сосед занят, и мы должны оставить его в покое (рис. 4.8). Если же оно положительно, то мы можем легко определить адрес начала этого блока как addr - addr[-2].

Определив адрес начала блока, мы можем легко объединить этот блок с блоком addr, нам нужно только сложить значения меток-дескрипторов и записать их в дескрипторы нового большого блока. Нам даже не нужно будет добавлять освобождаемый блок в список и извлекать оттуда его соседа!

Похожим образом присоединяется и сосед, стоящий после него. Единственное отличие состоит в том, что этого соседа все-таки нужно извлекать из списка свободных блоков.

Фактически, парные метки можно рассматривать как способ реализации решения, предложенного нами как одна из идей мозгового штурма: двунаправленного списка, включающего в себя как занятые, так и свободные блоки, и отсортированного по адресу. Дополнительное преимущество приведенного алгоритма состоит в том, что мы можем отслеживать такие ошибки, как многократное освобождение одного блока, запись в память за границей блока и иногда даже обращение к уже освобожденному блоку. Действительно, мы в любой момент можем проверить всю цепочку блоков памяти и убедиться в том, что все свободные блоки стоят в списке, что в нем стоят только свободные блоки, что сами цепочка и список не испорчены, и т. д.

Примечание

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

Алгоритм парных меток был предложен Дональдом Кнутом в начале 60-х. В третьем издании классической книги [Кнут, 2000], этот алгоритм приводится под названием "освобождения с дескрипторами границ". В современных системах используются и более сложные структуры дескрипторов, но всегда ставится задача обеспечить поиск соседей блока по адресному пространству за фиксированное время. В этом смысле, практически все современные подпрограммы динамического выделения памяти (в частности, реализации стандартной библиотеки языка С) используют аналоги алгоритма парных меток. Другие известные подходы либо просто хуже, чем этот, либо проявляют свои преимущества только в специальных случаях.

Реализация maiioc в библиотеке GNU LibC (реализация стандартной библиотеки языка С в рамках freeware проекта GNU Not Unix) (пример 4.3) использует смешанную стратегию: блоки размером более 4096 байт выделяются стратегией first fit из двусвязного кольцевого списка с использованием циклического просмотра, а освобождаются при помощи метода, который в указанном ранее смысле похож на алгоритм парных меток. Все выделяемые таким образом блоки будут иметь размер, кратный 4096 байтам.

Блоки меньшего размера объединяются в очереди с размерами, пропорциональными степеням двойки, как в описанном далее алгоритме близнецов. Элементы этих очередей называются фрагментами (рис. 4.9). В отличие от алгоритма близнецов, мы не объединяем при освобождении парные фрагменты. Вместо этого, мы разбиваем 4-килобайтовый блок на фрагменты одинакового размера. Если, например, наша программа сделает запросы на 514 и 296 байт памяти, ей будут переданы фрагменты в 1024 и 512 байт соответственно. Под эти фрагменты будут выделены полные блоки в 4 килобайта, и внутри них будет выделено по одному фрагменту. При последующих запросах на фрагменты такого же размера будут использоваться свободные фрагменты этих блоков. Пока хотя бы один фрагмент блока занят, весь блок считается занятым. Когда же освобождается последний фрагмент, блок возвращается в пул.

Описатели блоков хранятся не вместе с самими блоками, а в отдельном динамическом массиве _heapinfo. Описатель заводится не на непрерывную последовательность свободных байтов, а на каждые 4096 байт памяти (в примере 4.3 именно это значение принимает константа blocksize). Благодаря этому мы можем вычислить индекс описателя в _heapinfo, просто разделив на 4096 смешение освобождаемого блока от начала пула.

Для нефрагментированных блоков описатель хранит состояние (занят-свободен) и размер непрерывного участка, к которому принадлежит блок. Благодаря этому, как и в алгоритме парных меток, мы легко можем найти соседей освобождаемого участка памяти и объединить их в большой непрерывный участок.

Для фрагментированных блоков описатель хранит размер фрагмента, счетчик занятых фрагментов и список свободных. Кроме того, все свободные фрагменты одного размера объединены в общий список - заголовки этих списков собраны в массив _fraghead.

Используемая структура данных занимает больше места, чем применяемая в классическом алгоритме парных меток, но сокращает объем списка свободных блоков и поэтому имеет более высокую производительность. Средний объем блока, выделяемого современными программами для ОС общего назначения, измеряется многими килобайтами, поэтому в большинстве случаев повышение накладных расходов памяти оказывается терпимо.

Пример 4.3. Реализация malloc/free в GNU LibC. Функция __default_morecore приведена в примере 4.1
malloc.с /* Распределитель памяти 'malloc'.

Copyright 1990, 1991, 1992 Free Software Foundation Написана в мае 1969 Mike Hertel.

GNU С Library является свободным программным обеспечением;
вы можете передавать другим лицам и/или модифицировать ее в соответствии с 
положениями GNU General Public License версии 2 или (по вашему выбору) любой 
более поздней версии.

Библиотека GNU С распространяется в надежде, что она будет полезна, но БЕЗ 
КАКИХ-ЛИБО ГАРАНТИЙ; даже без неявно предполагаемых гарантий 
КОММЕРЧЕСКОЙ ЦЕННОСТИ или ПРИГОДНОСТИ ДЛЯ КОНКРЕТНОЙ ЦЕЛИ. 
Подробнее см. GNU General Public License.

Вы должны были получить копию GNU General Public License вместе с
GNU С Library; см. файл COPYING. Если вы ее не получили, напишите по адресу: 
Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.

С автором можно связаться по электронной почте по адресу mike@ai.mit.edu, 
или Mike Haertel с/о Free Software Foundation.  */

#ifndef    	_MALLOC_INTERNAL
#define		_MALLOC_INTERNAL 
#include 
#endif

#ifdef _ELF_ 
#pragma weak malloc =   __libc_malloc
#endif

/* Как действительно получить дополнительную память. */ 

ptr_t (* __morecore)  __P ((ptrdiff_t  __size)) =  __default_morecore_init;

/* Предоставляемая пользователем отладочная функция (hook) для 'malloc’ */

void (*__malloc_initialize_hook) __P ((void)) ;
__ptr_t (*__malloc_hook)  __P ( (size_t __size));

/* Указатель на основание первого блока. */ 
char *_heapbase;

/* Таблица информационных записей о блоках.  Размещается 
	через align/_free (не malloc/free).  */ 

malloc_info *_heapinfo;

/* Количество информационных записей. */

static size_t heapsize;

/* Индекс поиска в информационной таблице.  */ 

size_t  _heapindex;

/* Ограничитель допустимых индексов в информационной таблице */ 

size_t _heaplimit;

#if 1        /* Adapted from Mike */

/* Счетчик больших блоков, размещенных для каждого из размеров
фрагментов. */

int _fragblocks[BLOCKLOG];
#endif

/ * Списки свободных фрагментов по размерам.  */ 

struct list _fraghead[BLOCKLOG];

/* Инструментальные переменные.  */ 
size_t  _chunks_used;
size_t  _bytes_used;
size_t  _chunks_free;
size_t _bytes_free;

/* Имеем ли мы опыт? */
/* Are you experienced?  */

int  __malloc_initialized;

/* Выровненное размещение.  */ 

static __ptr_t align __P ((size_t)) ;
static __ptr_t 
align (size) 
	size_t size;
{
	__ptr_t result;
	unsigned long int adj;
	result = (*  morecore) (size);
	adj = (unsigned long int) ((unsigned long int)  ((char *) result -
		(char *) NULL)) % BLOCKSIZE;
	if (adj != ) {
	adj = BLOCKSIZE - adj;
	(void)  (*__morecore) (adj);
	result = (char *) result + adj;
		}
   	return result;
}

/* Настроить все и запомнить, что у нас есть. */ 
/* Set everything up and remember that we have.  */ 

static int initialize __P ((void));
static int 
initialize () 
{
	if ( __malloc_initialize_hook)
		(*__malloc_initialize_hook)  ();
	heapsize - HEAP / BLOCKSIZE;
	_heapinfo = (malloc_info *) align (heapsize * sizeof (malloc_info));
	if (_heapinfo = = NULL) 
	return 0;
	memset (_heapinfo, 0, heapsize * sizeof (malloc_info));
	_heapinfo[0].free.size = 0;
	_heapinfo[0].free.next =  _heapinfo[0].free.prev = 0;
	_heapindex = 0;
	_heapbase = (char *)  _heapinfo;
	__malloc_initialized = 1;
	return 1;
}


/* Получить выровненную память, инициализируя
	или наращивая таблицу описателей кучи по мере необходимости. */ 

static   __ptr_t morecore __P ((size t));
static  __ptr_t 
morecore (size)
	size_t size;
{
	__ptr_t result;
	malloc_info *newinfo, *oldinfo;
	size_t newsize;
	result = align (size);
	if (result = = NULL) return NULL;

/* Проверить, нужно ли нам увеличить таблицу описателей. */ 
	if ((size_t) BLOCK ((char *) result + size) > heapsize) 
	{ 
		newsize = heapsize;
		while ((size_t) BLOCK ((char *) result + size) > newsize) 
			newsize *= 2
		newinfo = (malloc_info *) align (newsize * sizeof (malloc_info))
		if (newinfo = = NULL) 
		{
			(*_morecore) (-size);
			return NULL;
		}
		memset (newinfo, 0, newsize * sizeof (malloc info) ) ;
		memсру (newinfo, heapinfo, heapsize * sizeof (malloc info) ) ;
		oldinfo = _heapinfo;
		newinfo[BLOCK (oldinfo)].busy.type = 0;
		newinfo[BLOCK (oldinfo)].busy.info.size = 
			BLOCKIFY (heapsize * sizeof (malloc_info)) ;
		_heapinfo = newinfo;
		_free_internal (oldinfo) ;
		heapsize = newsize;
	}
	_heaplimit = BLOCK ((char *) result + size);
	 return result;
}

/* Выделить память из кучи. */ __ptr_t

__libc_malloc (size) 
	size_t size;
{
	_ptr_t result;
	size_t block, blocks, lastblocks, start;
	register size_t i;
	struct list *next;

	/* Некоторые программы вызывают malloc (0). Мы это допускаем. */ 

#if 0
	if (size = = 0) return NULL;
#endif
	if (!__malloc_initialized) 
		if (!_initialize ()) return NULL;
	if (__malloc_hook != NULL)
		return (*__malloc_hook) (size);
	if (size < sizeof (struct list)) 
size = sizeof (struct list);

/* Определить политику размещения на основании размера запроса. */ 
	if (size <= BLOCKSIZE / 2) 
	{
/* Маленькие запросы получают фрагмент блока -
Определяем двоичный логарифм размера фрагмента, */ 
		register size_t log = 1;
		-size;
		while ( (size /= 2) != 0) 
			++log;
/* Просмотреть списки фрагментов на предмет свободного
фрагмента желаемого размера. */ 
next =  _fraghead[log].next;
if (next != NULL)
{

/* Найдены свободные фрагменты этого размера.
Вытолкнуть фрагмент из списка фрагментов и вернуть его. 
Обновить счетчики блока nfree и first. */ 

	result = (__ptr_t) next; 
	next->prev->next = next->next;
	if (next->next != NULL)
	next->next->prev = next->prev;
	block = BLOCK (result);
	if [--_heapinfo[block].busy.info.frag.nfree != 0)
	_heapinfo[block].busy.info.frag.first = (unsigned long int) 
	((unsigned long int) ((char *) next->next - (char *) NULL) 
		% BLOCKSIZE) >> log;

/* Обновить статистику, */ 

	++ _chunks_used;
	_bytes_used += 1 << log;
	-- _chunks_free;
	_bytes_free -= 1 << log;
}
else 
{

/* Нет свободных фрагментов желаемого размера. Следует взять
новый блок, поделить его на фрагменты и вернуть первый. */ 

	result = __libc_malloc (BLOCKSIZE);
	if (result == NULL) 
		return NULL;
#if 1        /* Adapted from Mike */ 
	++_fragblocks[log];
#endif

/* Связать все фрагменты, кроме первого, в список свободных. */ 
	for (i = 1; i < (size_t) (BLOCKSIZE >> log); ++i) 
	{ 
	next = (struct list *) ((char *) result + (i << log)) ;
	next->next =  _fraghead[log].next;
	next->prev = &_fraghead[log] ;
	next->prev->next = next;
		if (next->next != NULL) 
			next->next->prev = next;

/* Инициализировать счетчики nfree и first для этого блока. */

		block = BLOCK (result);
		_heapinfo[block].busy.type = log;
		_heapinfo[block].busy.info.frag.nfree = i - 1;
		_heapinfo[block].busy.info.frag.first = i - 1;

		_chunks_free += (BLOCKSIZE >> log) - 1;
		_bytes_free += BLOCKSIZE - (1 << log);
		_bytes_used -= BLOCKSIZE - (1 << log) ;

/* Большие запросы получают один или больше блоков.
Просмотреть список свободных циклически, начиная с точки, 
где мы были в последний раз.  Если мы пройдем полный круг, не о
бнаружив достаточно большого блока, мы должны будем запросить 
еще память у системы. */ 

		blocks = BLOCKIPY (size);
		start = block =  _heapindex;
		while (_heapinfo[block]-free.size < blocks) 
		{ 
			block = _heapinfo[block].free.next;
			if (block == start) 
			{

/* Необходимо взять больше [памяти] у системы.
Проверить, не будет ли новая память продолжением последнего 
свободного блока; если это так, нам не надо будет запрашивать 
так много.  */
 
			block =  _heapinfo[0].free.prev;
				lastblocks =  _heapinfo[block].free.size;
				if ( _heapiimit != О && block + lastblocks == _heaplimit && 
					(*__morecore) (0) == ADDRESS (block + lastblocks) && 
					(morecore ((blocks - lastblocks) * BLOCKSIZE)) != NULL) 
				{
/* Adapted from Mike */ 
#if 1
/* Обратите внимание, что morecore() может изменить положение 
последнего блока, если она перемещает таблицу дескрипторов и 
старая копия таблицы сливается с последним блоком. */ 

					block = _heapinfo[0].free.prev;
					_heapinfo[block].free.size += blocks - lastblocks;

#else
					_heapinfо[block].free.size = blocks;

#endif

					_bytes_free += (blocks - lastblocks) * BLOCKSIZE;
					continue;
				}
 
				result = morecore (blocks * BLOCKSIZE);
				if (result == NULL)
				return NULL;
				block = BLOCK (result);
				_heapinfo[block].busy.type = 0;
				_heapinfo[block].busy.info.size = blocks;
				++_chunks_used;
				_bytes_used += blocks * BLOCKSIZE;
				return result;
			}
		}

/* В этой точке мы [так или иначе] нашли подходящую запись
в списке свободных.  Понять, как удалить то, что нам нужно,
из списка. */ 

		result = ADDRESS (block);
		if (_heapinfo[block].free.size > blocks)

/* Блок, который мы нашли, имеет небольшой остаток,
так что присоединить его задний конец к списку свободных. */ 

		_heapinfo[block + blocks].free.size
		= _heapinfo[block].free.size - blocks;
	_heapinfo[block + blocks].free.next 
	= _heapinfo[block].free.next;
	_heapinfo[block + blocks].free.prev 
	= _heapinfo[block].free.prev;
		_heapinfo[ _heapinfo[block].free.prev].free.next 
			=  _heapinfo[ _heapinfo[block].free.next].free.prev
				= _heapindex = block + blocks;
		else 
		{

/* Блок точно соответствует нашему запросу, поэтому
просто удалить его из списка. */ 

			_heapinfo[_heapinfo[block].free.next].free.prev 
				= _heapinfo[block].free.prev;
			_heapinfo[_heapinfo[block].free.prev].free.next 
				= _heapindex = _heapinfo[block].free.next;
			--_chunks_free;
		}
		_heapinfo[block].busy.type = 0;
		_heapinfo[block].busy.info.size = blocks;
		++_chunks_used;
		_bytes_used += blocks * BLOCKSIZE;
		_bytes_free -= blocks * BLOCKSIZE;
	}
 	return result;
}
	

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

Проще всего решить эту задачу, если нам требуются блоки нескольких фиксированных размеров (рис. 4.10). Мы объединяем блоки каждого размера в свой список. Если в списке блоков требуемого размера ничего нет, мы смотрим в список блоков большего размера. Если там что-то есть, мы разрезаем этот блок на части, одну отдаем запрашивающей программе, а вторую... Правда, если размеры требуемых блоков не кратны друг другу, что мы будем делать с остатком?

Для решения этой проблемы нам необходимо ввести какое-либо ограничение на размеры выделяемых блоков. Например, можно потребовать, чтобы эти размеры равнялись числам Фибоначчи (последовательность целых чисел, в которойFi+1 = Fi + Fi-1). В этом случае, если нам нужно Fi байт, а в наличии есть только блок размера Fi+1, мы легко можем получить два блока - один требуемого размера, а другой - Fi-1, который тоже не пропадет. Да, любое ограничение на размер приведет к внутренней фрагментации, но так ли велика эта плата за гарантированное время поиска блока?

На практике, числа Фибоначчи не используются. Одной из причин, по-видимому, является относительная сложность вычисления такого Fi, которое не меньше требуемого размера блока. Другая причина - сложность объединения свободных блоков со смежными адресами в блок большего размера. Зато широкое применение нашел алгоритм, который ограничивает последовательные размеры блоков более простой зависимостью - степенями числа 2: 512 байт, 1 Кбайт, 2 Кбайт и т. д. Такая стратегия называется алгоритмом близнецов (рис. 4.11).

Одно из преимуществ этого метода состоит в простоте объединения блоков при их освобождении. Адрес блока-близнеца получается простым инвертированием соответствующего бита в адресе нашего блока. Нужно только проверить, свободен ли этот близнец. Если он свободен, то мы объединяем братьев в блок вдвое большего размера, и т. д. Даже в наихудшем случае время поиска не превышает О (LOG(Smax) - LOG (Smin)) ,где Smах и Smin обозначают, соответственно, максимальный и минимальный размеры используемых блоков. Это делает алгоритм близнецов трудно заменимым для ситуаций, в которых необходимо гарантированное время реакции - например, для задач реального времени. Часто этот алгоритм или его варианты используются для выделения памяти внутри ядра ОС.

Существуют и более сложные варианты применения описанного выше подхода. Например, пул свободной памяти Novell Netware состоит из 4 очередей с шагом 16 байт (для блоков размерами 16, 32, 48, 64 байта), 3 очередей с шагом 64 байта (для блоков размерами 128, 192, 256 байт) и пятнадцати очередей с шагом 256 байт (от 512 байт до 4 Кбайт). При запросах большего размера выделяется целиком страница. Любопытно, что возможности работы в режиме реального времени, присущие этой изощренной стратегии. в Netware практически не используются.

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

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


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

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

Некоторые системы программирования используют специальный метод освобождения динамической памяти, называемый сборкой мусора (garbage collection). Этот метод состоит в том, что ненужные блоки памяти не освобождаются явным образом. Вместо этого используется некоторый более или менее изощренный алгоритм, следящий за тем, какие блоки еще нужны, а какие - уже нет.

Самый простой метод отличать используемые блоки от ненужных - считать, что блок, на который есть ссылка, нужен, а блок, на который ни одной ссылки не осталось, - не нужен. Для этого к каждому блоку присоединяют дескриптор, в котором подсчитывают количество ссылок на него. Каждая передача указателя на этот блок приводит к увеличению счетчика ссылок на 1, а каждое уничтожение объекта, содержавшего указатель, - к уменьшению.

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

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

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

4.4. Открытая память (продолжение).

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

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

Управление памятью в OS/360

В данном случае проблема фрагментации особенно остра, так как типичный образ процесса занимает значительную часть всего доступного ОЗУ. Если при выделении небольших блоков мы еще можем рассчитывать на "закон больших чисел" и прочие статистические закономерности, то самый простой сценарий загрузки трех процессов различного размера может привести нас к неразрешимой ситуации (см. рис. 4.13).

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

Управление памятью в MS DOS

В системах с открытой памятью невозможны эффективные средства разделения доступа. Любая программная проверка прав доступа может был легко обойдена прямым вызовом "защищаемых" модулей ядра. Даже криптографические средства не обеспечивают достаточно эффективной защиты, потому что можно "посадить" в память троянскую программу. которая будет анализировать код программы шифрования и считывать значение ключа.

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

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

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

В системе MacOS был предложен достаточно оригинальный метод борьбы с фрагментацией, заслуживающий отдельного обсуждения.


4.4.1 Управление памятью в MacOS и Win16.

В этих системах предполагается, что пользовательские программы не сохраняют указателей на динамически выделенные блоки памяти. Вместо этого каждый такой блок идентифицируется целочисленным дескриптором или "ручкой" (handle) (рис. 4.16). Когда программа непосредственно обращается к данным в блоке, она выполняет системный вызов GlobalLock (запереть). Этот вызов возвращает текущий адрес блока. Пока программа не исполнит вызов Globaiunlock (отпереть), система не пытается изменить адрес блока. Если же блок не заперт, система считает себя вправе передвигать его по памяти или даже сбрасывать на диск (рис. 4.17).

"Ручки" представляют собой попытку создать программный аналог аппаратных диспетчеров памяти. Они позволяют решить проблему фрагментации и даже организовать некое подобие виртуальной памяти. Можно рассматривать их как средство организации оверлейных данных - поочередного отображения разных блоков данных на одни и те же адреса. Однако за это приходится платить очень дорогой ценой.

Использование "ручек" сильно усложняет программирование вообще и в особенности перенос ПО из систем, использующих линейное адресное пространство. Все указатели на динамические структуры данных в программе нужно заменить на "ручки", а каждое обращение к таким структурам Необходимо ОКружить вызовами GlobalLock/GlobalUnlock.

ВЫЗОВЫ GlobalLock/GlobalUnlock:

в современных суперскалярных процессорах это может приводить к падению производительности во много раз.

Попытки уменьшить число блокировок требуют определенных интеллектуальных усилий. Фактически, к обычному циклу разработки ПО: проектирование, выбор алгоритма, написание кода и его отладка - добавляются еще две фазы: микрооптимизация использования "ручек" и отладка оптимизированного кода. Последняя фаза оказывается, пожалуй, самой сложной и ответственной.

Наиболее опасной ошибкой, возникающей на фазе микрооптимизации, является вынос указателя на динамическую структуру за пределы скобок GlobalLock/GlobalUnlock. Эту Ошибку ОЧЕНЬ СЛОЖНО обнаружить при тестировании, так как она проявляется, только если система пыталась передвигать блоки в промежутках между обращениями. Иными словами, ошибка может проявлять или не проявлять себя в зависимости от набора приложений, исполняющихся в системе, и от характера деятельности этих приложений. В результате мы получаем то, чего больше всего боятся эксплуатационщики - систему, которая работает иногда. При переходе от Windows З.х к Windows 95 наработка на отказ - даже при исполнении той же самой смеси приложений - резке возросла, так что система из работающей иногда превратилась в работающую как правило. По-видимому, это означает, что большая часть ошибок в приложениях Win 16 действительно относилась к ошибкам работы с "ручками".

Не случайно фирма Microsoft полностью отказалась от управления памятью с помощью "ручек" в следующей версии MS Windows - Windows 95, в которой реализована почти полноценная виртуальная память.

Mac OS версии 10, построенная на ядре BSD Unix, также имеет страничную виртуальную память и никогда не перемещает блоки памяти, адресуемые "ручками".

4.5 Системы с базовой виртуальной адресацией.

Как уже говорилось, в системах с открытой памятью возникают большие сложности при организации многозадачной работы. Самым простым способом разрешения этих проблем оказалось предоставление каждому процессу своего виртуального адресного пространства. Простейшим методом организации различных адресных пространств является так называемая базовая адресация. По-видимому, это наиболее ранний способ виртуальной адресации (рис. 4.18).

Вы можете заметить, что термин базовая адресация уже занят - мы называли таким образом адресацию по схеме reg [offset]. Метод, о котором сейчас идет речь, состоит в формировании адреса по той же схеме. Отличие состоит в том, что регистр, относительно которого происходит адресация, не доступен прикладной программе. Кроме того, его значение прибавляется ко всем адресам, в том числе к "абсолютным" адресным ссылкам или переменным типа указатель. По существу, такая адресация является способом организации виртуального адресного пространства.

Как правило, машины, использующие базовую адресацию, имеют два регистра. Один из регистров задает базу для адресов, второй устанавливает верхний предел. В системе ICL1900/Одренок эти регистры называются соответственно base и datum. Если адрес выходит за границу, установленную значением datum, возникает исключительная ситуация (exception) ошибочной адресации. Как правило, это приводит к тому, что система принудительно завершает работу программы.

При помощи этих двух регистров мы сразу решаем две важные проблемы.

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

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

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

Решив перечисленные выше проблемы, мы создаем другие, довольно неожиданные. Мы оговорили, что базовый регистр недоступен прикладным задачам. Но какой-то задаче он должен быть доступен! Каким же образом процессор узнает, исполняет ли он системную или прикладную задачу, и не сможет ли злонамеренная прикладная программа его убедить в том, что является системной?

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

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

Стандартное решение этого комплекса проблем состоит в следующем. Мы снабжаем процессор флагом, который указывает, исполняется системный или пользовательский процесс. Код пользовательского процесса не может манипулировать этим флагом, однако ему доступна специальная команда. В различных архитектурах эти специальные команды имеют разные мнемонические обозначения, далее мы будем называть эту команду syscall. syscall одновременно переключает флаг в положение "системный" и передает управление на определенный адрес в системном адресном пространстве. Процедура, находящаяся по этому адресу, называется диспетчером системных вызовов (рис. 4.20).

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

Протокол общения прикладной программы с системой состоит в следующем: программа помещает параметры вызова в оговоренное место - обычно в регистры общего назначения или в стек - и исполняет syscall. Одним из параметров передается и код системного вызова. Диспетчер вызовов анализирует допустимость параметров и передает управление соответствующей процедуре ядра, которая и выполняет требуемую операцию (или не выполняет, если у пользователя не хватает полномочий). Затем процедура помещает в оговоренное место (чаще всего опять-таки в регистры или в пользовательский стек) возвращаемые значения и передает управление диспетчеру, или вызывает sysret самостоятельно.

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

В системах с базовой адресацией эту проблему обычно решают просто:

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

В архитектурах с более сложной адресацией нередко предоставляются специальные инструкции для передачи данных между пользовательским и системным адресными пространствами. Однако и в этом случае ядро ОС обычно имеет полный доступ к адресным пространствам пользовательских задач.

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