Uploaded by ShestovPV

Лекция 1-1

advertisement
ФЕДЕРАЛЬНОЕ АГЕНТСТВО ЖЕЛЕЗНОДОРОЖНОГО
ТРАНСПОРТА
Федеральное государственное бюджетное образовательное
учреждение высшего образования
«МОСКОВСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ
ПУТЕЙ СООБЩЕНИЯ ИМПЕРАТОРА НИКОЛАЯ II»
Кафедра
Вычислительные системы и сети
Лекция № 1
Тема: УПРАВЛЕНИЕ .ПАМЯТЬЮ
Тема 2: Среда времени выполнения программ
по дисциплине: «Современные компьютерные архитектуры»
Москва
2018 г.
Содержание занятия и время
Введение
5 мин.
Учебные вопросы (основная часть)
1. Организация памяти ............................................................................... 25 мин.
2. Выделение памяти в стеке ...................................................................... 30 мин.
3. Доступ к нелокальным данным в стеке ................................................ 25 мин.
Заключение ..................................................................................................... 5 мин.
Литература:
Основная:
1. Ахо А.В., Лам М.С., Ульман Д.Д. Компиляторы: принципы, технологии и
инструментарий. М.: изд-во Вильямс, 2015. – С. 525-554
Дополнительная:
1. Столмен Р., Пеш Р., Шебс С. Отладка с помощью GDB. 2000. – С. 51-53
Материально-техническое обеспечение:
1. Наглядные пособия (по данным учета кафедры):
2. Технические средства обучения:
доска, мел, проектор.
3. Приложения (диафильмы, слайды):
комплект слайдов.
Методические указания:
1
1. Организация памяти вычислительного процесса
В данном учебном пособии вычислительный процесс определяется как
«экземпляр выполняемой программы» или как «контекст выполнения»
работающей программы. В традиционных вычислительных системах
вычислительный процесс выполняет одну последовательность инструкций в
адресном пространстве, представляющем собой множество адресов памяти, к
которым процессу разрешено обращаться. Реализация вычислительного
процесса заключается в выполнении заданной последовательности
арифметических операций – операторов над исходными данными –
операндами, поступившими на вход программы. Таким образом,
вычислительный процесс, представляет собой некоторое преобразование f(x),
где x – n-мерная величина, характеризующая множество исходных данных
программы, f(x) – m-мерный результат работы программы.
Инструкции (операции) и данные (операнды) – два базовых типа объектов,
находящихся в адресном пространстве вычислительного процесса. Каждый
объект обладает уникальным адресом месторасположения в адресном
пространстве вычислительного процесса. Всего существует три типа адресов:
1) логический адрес - используется в инструкциях машинного языка для
обозначения адреса операнда или оператора. Каждый логический адрес
состоит из сегмента и смещения, которое определяет расстояние от начала
сегмента до адресуемой ячейки памяти;
2) линейный адрес (также известен как виртуальный адрес) - 32-разрядное
или 64-разрядное целое без знака, которое можно использовать для
адресации до 232 (или 264 соответственно) ячеек памяти. Линейные адреса
обычно представляются в шестнадцатеричной нотации;
3) физический адрес - используется для адресации ячеек в микросхемах
памяти. Представляется электрическими сигналами, посылаемыми с
адресных контактов микропроцессора на шину памяти.
Программисту для использования в исходном коде программы доступен
исключительно логический адрес. Задачи управления логическим адресным
пространством и его организации разделяются между компилятором,
операционной системой и целевой машиной (процессорной архитектурой).
Блок управления памяти преобразует логический адрес в линейный с
помощью электронной схемы, которая называется блоком сегментации.
Затем другая электронная схема, называемая блоком управления страницами,
преобразует линейные адреса в физические.
2
Представление среды времени выполнения программы в пространстве
логических адресов состоит из областей данных программы и инструкций,
выполняемых программой, как показано на рисунке 1.1.
Код
Статические
данные
Куча
Свободная
память
Стек
Рис. 1.1. – Типичное разделение памяти времени выполнения на области кода и данных
Минимально адресуемой единицей физической памяти, не требующей
использования специальных инструкций процессора, является байт. Далее
будем полагать, что память времени выполнения имеет вид блоков смежных
байтов, где байт – наименьшая адресуемая единица памяти. Байт состоит из
восьми битов, четыре байта образуют машинное слово. Многобайтные
объекты хранятся в последовательных байтах, а их адреса определяются
адресами их первых байтов.
Элементарные типы данных, такие как символы, целые и действительные
числа, могут храниться в целом количестве байтов. Память, выделенная для
составных типов, таких как массивы или структуры, должна быть достаточно
велика для хранения всех их компонентов.
На схему размещения объектов данных в памяти сильное влияние
оказывают ограничения адресации на целевой машине. Например, некоторые
платформы требуют размещения указателей по адресам кратным 4,
несоблюдение данного требования приводит к существенному снижению
скорости работы программы. Такое требование к размещению данных
называется выравниванием и приводит к необходимости затрат
дополнительной памяти для заполнения промежутков внутри объектов
программы. Хотя массив из десяти символов требует только десяти байтов
для хранения своего содержимого, компилятор может выделить ему для
достижения корректного выравнивания 12 байт, оставляя 2 байта
неиспользуемыми. Значения неиспользуемых байтов заполняются нулями.
Рассмотрим пример, поясняющий сказанное.
Пример 1.1. – Выравнивание памяти
3
//предполагается, что sizeof(int) == 4
struct x {
char c1;
//смещение 0, размер 1 байт
//байты 1-3: 3 заполняющих байта
int i1;
//байты 4-7: 4 байта на 4 байтовой границе
char c2;
//байт 8: 1 байт
//байты 9-11: 3 заполняющих байта
};
В данном примере n==1+3+4+1=9 и m==sizeof(x)==12. В значение sizeof(x) вносят
вклад все заполнители - как внутренние, так и внешние. Внешнее заполнение может
показаться излишним, но оно необходимо, например, когда вы работаете с массивом
объектов x, располагающихся в памяти один за другим, чтобы обеспечить выравнивание
данных по 4-байтовой границе. Следующая перестановка полей структуры позволяет
получить иной результат:
//предполагается, что sizeof(int) == 4
struct x {
int i1;
//байты 0-3
char c1;
//байт 4
char c2;
//байт 5
//байты 6-7: 2 заполняющих байта
};
Здесь n==4+1+1==6, а m==sizeof(x)==8.
Размер сгенерированного целевого кода фиксируется во время
компиляции, поэтому компилятор может разместить выполнимый целевой
код в статически определенной области, обычно в нижних адресах памяти.
Аналогично в процессе компиляции может быть известен размер некоторых
объектов данных программ, таких как глобальные константы, и данных,
сгенерированных компилятором, таких как информация для поддержки
сборки мусора; такие данные также могут быть размещены в другой
статически определяемой области.
Для максимально эффективного использования памяти во время
выполнения программы две другие области, стек и куча, находятся по
разные стороны оставшегося адресного пространства. Это динамические
области – их размеры могут изменяться в процессе работы программы,
причем области при необходимости растут одна навстречу другой. Стек
используется для хранения структур данных, называющихся записями
активации. Записи активации генерируются при вызове процедур в ходе
выполнения вычислительного процесса.
Запись активации используется для хранения состояния вызывающей
процедуры на момент передачи потока управления вызываемой процедуре.
Состояние процедуры описывается текущим значением счетчика команд,
4
состоянием
локальных по отношению к процедуре
объектов
вычислительного процесса. Когда поток управления возвращается
вызывающей процедуре из вызываемой, активация вызывающей процедуры
может быть возобновлена после восстановления значений регистров и
установки счетчика команд в точку, следующую непосредственно за
вызовом. Объекты данных, время жизни которых находится в пределах
времени жизни записи активации, могут располагаться в стеке вместе с
другой информацией, связанной с активацией.
Многие языки программирования содержат инструменты, позволяющие
резервировать блоки памяти, размер которых не может быть установлен до
момента попадания потока управления в заданную точку вычислительного
процесса. Например, язык С имеет функции malloc и free, которые могут
использоваться для получения и освобождения блоков памяти требуемого
размера. Для управления размещением данных такого вида используется
куча или динамическая свободная память вычислительного процесса.
Решение о выделении памяти является статическим если оно
принимается во время компиляции исполняемого файла вычислительного
процесса, когда известен только исходный код программы. И наоборот,
решение о выделении памяти является динамическим, если оно может быть
принято исключительно в ходе выполнения программы. Динамическое
выделение памяти основано на комбинации двух следующих стратегий
хранения объектов программы:
1)
2)
хранение в стеке;
хранение в куче.
2. Выделение памяти в стеке
Почти все компиляторы языков программирования, в которых
применяются пользовательские функции, процедуры или методы (функциичлены), как минимум часть своей памяти времени выполнения используют в
качестве стека. Всякий раз при вызове процедуры в стеке выделяется
пространство для ее локальных переменных, а по завершении процедуры это
пространство снимается со стека.
Выделение памяти в стеке не имело бы столько преимуществ, если бы
вызовы, или активации, не могли бы быть вложенными во времени.
Активацию процедур в процессе выполнения всей программы можно
представить в виде дерева, называемого деревом активаций. В дереве
активаций каждая вершина соответствует одной активации, а корень
представляет собой активацию главной процедуры, которая инициирует
5
выполнение программы. В программах, написанных на языках
программирования C/C++, корнем дерева активации является функция main.
В вершине активации процедуры p дочерние вершины соответствуют
активациям процедур, вызываемых данной активацией (процедурой) p.
Активации указываются слева направо в порядке их вызовов. Активация,
соответствующая дочерней вершине, должна быть завершена до того, как
начнется активация её соседа справа. Рассмотрим пример, поясняющий
сказанное.
Пример 1.2.- Построение дерева активаций
Приведенная ниже программа реализует считывание девяти целых чисел в массив a и
его сортировку алгоритмом быстрой сортировки:
int a[11];
void readarray(){/*Считываем 9 целых чисел в a[1],...,a[9]. */
int i;
}
int partition(int m, int n) {
/*Выбор разделителя v и разделение a[m,n]
так, что a[m..p-1] меньше v, а[p]=v,
а a[p+1..n] не меньше v. Возврат p*/
}
void quicksort(int m, int n) {
int i;
if(n>m){
i=partition(m,n);
quicksort(m,i-1);
quicksort(i+1,n);
}
}
int main(int argc, char** argv) {
readarray();
a[0]=-9999;
a[10]=9999;
quicksort(1,9);
return 0;
}
Функция main решает три задачи. Она вызывает функцию (процедуру) readarray,
устанавливает ограничители, а потом вызывает функцию quicksort для всего массива
данных. Одно из возможных
деревьев активации, содержащее полную
последовательность вызовов процедур и возвратов из них для приведенного фрагмента
кода представлено на рисунке 1.2. Функции на рисунке 1.2 представлены первыми
буквами их имен.
6
Рис. 1.2 – Дерево активации вызовов быстрой сортировки
В процессе активации процедуры происходит одна из трех ситуаций:
1) активация q завершается нормально. Тогда, по сути, в любом языке программирования
управление возвращается в точку p, следующую непосредственно за точкой, в которой был
сделан вызов q;
2) активация q или некоторой процедуры, вызванной p прямо или косвенно, завершается
аварийно, то есть продолжение выполнения программы невозможно. В этом случае p завершается
одновременно с q;
3) активация q завершается из-за сгенерированного исключения, которое q обработать не в
состоянии. Процедура p может обработать исключение; в этом случае активация q завершается, в
то время как активация p продолжается, хотя и не обязательно с точки, в которой была вызвана q.
Если p не может обработать исключение, то данная активация p завершается одновременно с
активацией q и, скорее всего, исключение будет обработано некоторой другой открытой
активацией процедуры.
Возможность использования стека времени выполнения обеспечивается
некоторыми взаимосвязями между деревом активации и поведением
программы:
1) последовательность вызовов процедур соответствует обходу дерева активации в прямом
порядке;
2) последовательность возвратов из процедур соответствует обходу дерева в обратном
порядке;
3) открытыми в момент времени t активациями являются активации, соответствующие
вершине N дерева активации, где N – процедура, выполняемая в момент времени t, и её предкам.
Вызов процедур и возвраты из них управляются стеком времени
выполнения, именуемым стеком управления. Активная активация имеет
запись активации, иногда называемую кадром в стеке управления. На дне
стека находится запись активации корня дерева активации, а
последовательность записей активации в стеке соответствует пути в дереве
активации от корня до текущей активации, в которой в настоящий момент
находится управление. Запись последней по времени активации находится на
вершине стека. Содержимое записи активации варьируется в зависимости от
реализуемого языка программирования, общий вид записи активации
представлен на рисунке 1.3.
7
Фактические
параметры
Возвращаемые
значения
Связь
управления
Связь доступа
Состояние
машины
Локальные
данные
Временные
переменные
Рис. 1.3 – Общий вид записи активации
Представленные на рисунке поля записи активации имеют следующее назначение:
1) временные переменные – временные значения, появляющиеся, например, в процессе
вычисления выражений, когда эти значения не могут храниться в регистрах;
2) локальные данные – локальные данные процедуры, к которой относится данная запись
активации;
3) состояние машины - информация о состоянии машины непосредственно перед вызовом
процедуры. Эта информация обычно включает адрес возврата и содержимое регистров, которые
использовались вызывающей процедурой и должны быть восстановлены при возврате из
вызываемой процедуры;
4) связь доступа используется для обращения вызванной процедуры к данным, хранящимся в
другом месте, например, в другой записи активации;
5) связь управления указывает на запись активации вызывающей процедуры;
6) память для возвращаемого значения вызываемой функции, если таковое имеется. Не все
вызываемые процедуры возвращают значения, а кроме того, для большей эффективности
возвращаемое значение может находиться не в стеке, а в регистре;
7) фактические параметры, используемые вызываемой процедурой. Зачастую эти значения
также располагаются не в стеке, а по возможности для большей эффективности передаются в
регистрах. Однако в общем случае место для них отводится в стеке.
На рисунке 1.4 показаны снимки стека времени выполнения при
прохождении потока управления по дереву активации, представленному на
рисунке 1.2.
8
Рис. 1.4. – Стек времени выполнения быстрой сортировки
Вызовы процедур реализованы с помощью кода, который выделяет
память в стеке для записи активации и вносит информацию в ее поля.
Последовательность возврата представляет собой аналогичный код, который
восстанавливает состояние машины таким образом, что вызывающая
процедура может продолжить работу.
Код в последовательности вызова зачастую разделяется между
вызывающей и вызываемой процедурами. В общем случае, если процедура
вызывается n раз, то часть последовательности вызова в вызывающих
процедурах генерируется n раз. Однако часть последовательности вызова в
вызываемой процедуре генерируется лишь единожды. Следовательно,
желательно разместить как можно большую часть последовательности
вызова в коде вызываемой процедуры – с учетом того, что известно
вызываемой процедуре при ее вызове.
При разработке последовательностей вызовов и схемы записей активации
используются следующие принципы.
1) значения, передаваемые между вызывающей и вызываемой процедурами, в общем случае
помещаются в начале записи активации вызываемой процедуры, так что они максимально близки
к записи активации вызывающей процедуры;
2) элементы фиксированного размера обычно располагаются посредине, эти элементы
обычно включают связь управления, связь доступа и состояние машины;
3) элементы, размер которых может быть неизвестен заранее, размещаются в конце записи
активации;
4) указатель вершины стека указывает на конец полей фиксированного размера в записи
активации.
9
Пример того, как вызывающая и вызываемая процедуры
сотрудничать в плане управления стеком представлен на рисунке 1.5.
могут
Рис. 1.5. – Разделение задач между вызываемой и вызывающей процедурами
На представленном рисунке указатель top_sp, символизирует адрес
расположения записи активации текущей процедуры в стеке
вычислительного процесса.
Зачастую система управления памятью должна выделять память для
объектов, размеры которых во время компиляции неизвестны, но которые
являются локальными объектами процедуры и, таким образом, могут
размещаться в стеке. В современных языках программирования память для
объектов неизвестного во время компиляции размера выделяется из кучи.
Куча (от англ. heap - куча) - это термин, которым обозначают область памяти,
использующейся для размещения данных с неопределенным временем
жизни, пока программа (вычислительный процесс) не удалит их. Однако
выделение памяти в стеке для объектов, массивов и других структур
неизвестного размера возможно. Причина, по которой размещение объектов
в стеке предпочтительнее размещения в куче, заключается в том, что в этом
случае нет необходимости отслеживать время жизни объектов
вычислительного процесса.
Распространенная стратегия выделения памяти для массивов переменной
длины показана на рисунке 1.6.
10
Рисунок 1.6 - Доступ к динамически распределенным массивам
Данная схема работает для объекта любого типа, если он локален для
вызываемой процедуры и имеет размер, зависящий от параметров вызова.
Язык программирования C, позволяет выделять динамическую память в
стеке, чтобы выполнить выделение динамической памяти из стека,
пользуйтесь системным вызовом alloca(). Пример вызова:
#include <alloca.h>
void * alloca(size_t size)
В случае успеха данный вызов возвращает указатель на size байт в памяти.
3. Доступ к нелокальным данным в стеке
В современных языках программирования переменные (операнды) делятся
на два класса - локальные и глобальные. Глобальная переменная имеет
область видимости, которая состоит из всех функций, следующих после ее
объявления, за исключением мест, где имеется локальное определение
идентификаторов, имя которых совпадает с идентификатором глобальной
переменной. Переменные, объявленные внутри функции, имеют область
видимости, состоящую только из тела этой функции.
Для языков программирования, которые не допускают вложенные
объявления процедур, выделение памяти для переменных и доступ к ним
просты.
Глобальным переменным выделяется статическая память. Размещение
этих переменных остается фиксированным и известно во время компиляции.
Любое другое имя должно быть локально для активации на вершине стека.
Обратиться к этим переменным можно при помощи указателя стека top_sp.
11
Важное преимущество статического распределения памяти заключается в
том, что в качестве аргументов процедур могут передаваться и возвращаться
в качестве результата без существенных изменений в стратегии обращения к
данным другие объявленные процедуры. В случае правил статических
областей видимости и при отсутствии вложенных процедур любое имя,
нелокальное для одной процедуры, будет нелокальным для всех процедур,
независимо от того, как они были активированы. Аналогично, если
процедура возвращается как результат, нелокальные имена в ней ссылаются
на статически выделенную для них память.
Доступ существенно усложняется, если язык программирования разрешает
объявлениям процедур быть вложенными и при этом использует обычные
правила областей видимости. Причина этого заключается в том, что во время
компиляции знание того, что объявление p непосредственно вложено в
объявление q ничего не говорит нам об относительном размещении их
записей активации во время выполнения. В действительности, поскольку
процедуры p и q могут быть рекурсивными, в стеке могут быть несколько
записей активации для p и/или q.
Присвоим глубину вложенности 1 процедурам, которые не вложены ни в
какую иную процедуру. Однако если процедура p определена в процедуре с
глубиной вложенности i, то глубина вложенности такой процедуры p
составляет i+1.
Непосредственная реализация обычного статического правила области
видимости для вложенных функций получается путем добавления в каждой
записи активации указателя, называющегося связью доступа. Если в
исходном тексте программы процедура p непосредственно вложена в
процедуру q, то связь доступа в каждой активации p указывает на последнюю
активацию q. Связи доступа образуют цепочку от записи активации на
вершине стека к последовательности активаций с монотонно
уменьшающимися глубинами вложенности. Вдоль этой цепочки
располагаются все активации, данные и процедуры которых доступны для
текущей выполняющейся процедуры.
Предположим, что на вершине стека находятся процедура p, глубина
вложенности которой – n p и p требуется доступ к имени x, представляющему
собой элемент, определенный в некоторой процедуре q, которая окружает p и
имеет глубину вложенности nq . Понятно, что n p  nq , причем равенство
выполняется, только если p и q – одна и та же процедура. Поиск x начнем с
записи активации для p на вершине стека и проследуем по связям доступа до
12
записи активации к записи n p  nq раз. Таким образом, мы доберемся до
записи активации q, и это всегда будет самая последняя запись активации для
q из имеющихся в стеке. Эта запись активации содержит искомый элемент x.
Поскольку компилятору известна схема записи активации, x может быть
найдено с некоторым фиксированным смещением от позиции в записи
активации q, на которую указывает последняя связь доступа.
Как определяются связи доступа? Простой случай – когда вызов некоторой
процедуры осуществляется по её явному указанному имени. Более сложная
ситуация – когда происходит вызов процедуры, переданной в качестве
параметра. В этом случае конкретная вызываемая процедура становится
известна только во время выполнения программы, и глубина вложенности
вызываемой процедуры может отличаться от вызова к вызову. В случае
явного вызова процедурой q процедуры p возможны три ситуации:
1) процедура p имеет большую глубину вложенности, чем q. В таком случае p должна быть
определена непосредственно в q, иначе вызов процедурой q оказывается не в пределах области
видимости имени процедуры p. Таким образом, глубина вложенности процедуры p ровно на
единицу больше глубины вложенности q и связь доступа должна вести от p к q;
2) вызов рекурсивен, то есть q=p. В этом случае связь доступа для новой записи активации та
же, что и запись активации ниже нее в стеке;
3) глубина вложенности np процедуры p меньше глубины вложенности np процедуры q. Для
того чтобы вызов в q находился в области видимости имени p, процедура q должна быть вложена
в некоторую процедуру r, а p должна быть процедурой, определенной непосредственно в r.
Когда процедура p передается процедуре q в качестве параметра, а затем
процедура q вызывает этот параметр (тем самым вызывая p в данной
активации q), вполне возможно, что q не известен контекст, в котором p
появляется в программе. Если это так, то q не известно, как установить связь
доступа для p. Решение этой проблемы заключается в следующем: при
использовании процедуры в качестве параметра вызывающая процедура
должна передать наряду с именем процедуры корректную связь доступа.
Вызывающей процедуре всегда известна эта связь, поскольку если p
передается процедурой r как фактический параметр, p должна быть именем,
доступным для r, следовательно, r может определить связь доступа для p так
же, как если бы эта процедура была вызвана ею непосредственно.
Пример 1.3 – Передача функции в качестве аргумента
Рассмотрим фрагмент программы на языке ML, использующей передачу функции в
качестве параметра.
fun a(x) =
let
fun b(f) =
…f…;
13
fun c(y) =
let
fun d(z) = …
in
…b(d)…
end
in
…c(1)…
end;
Здесь сначала a вызывает c, так что мы размещаем запись активации для c в стеке над a.
Связь доступа для c указывает на запись для a, поскольку c определена непосредственно в
a. Затем c вызывает b(d). Последовательность вызова выстраивает запись активации b так,
как показано на рисунке 1.7 (а). В активации b находится фактический параметр d и его
связь доступа, которые вместе образуют значение формального параметра f в записи
активации для b. Функция d известна функции c, поскольку d определена в c,
следовательно, c передает в качестве связи доступа указатель на собственную запись
активации. Не имеет значения, где именно была определена d; если c находится в области
видимости этого определения, то должно быть применимо одно из трех правил вызова
процедур и c может предоставить необходимую связь доступа. Далее, в некоторой точке
функция b использует свой параметр f, применение которого состоит в вызове d. В стеке
появляется запись активации для d, как показано на рисунке 1.7 (б). Корректная связь
доступа для размещения в записи активации находится в значении параметра f, эта связь
указывает на запись активации для с, поскольку непосредственно окружает определение
d. Заметим, что d оказывается в состоянии установить корректное значение связи доступа,
несмотря на то, что b находится вне области видимости определения c.
Одна из проблем обращения к нелокальным данным при помощи связей
доступа заключается в том, что, когда глубина вложенности становится
большой, может потребоваться пройти по длинной цепочке связей доступа
для достижения необходимых данных. Более эффективная реализация
использует вспомогательный массив d, именуемый дисплеем, который
содержит по одному указателю для каждой глубины вложенности. В любой
момент времени d[i] представляет собой указатель на наивысшую запись
активации в стеке для процедуры с глубиной вложенности i.
Преимущество использования дисплея состоит в том, что при выполнении
процедуры p, которой требуется доступ к элементу x некоторой процедуры q,
нам достаточно провести поиск только в d[i], где i – глубина вложенности q;
мы следуем по указателю d[i] в запись активации для q, в которой с
известным смещением находится x. Для корректной работы дисплея
предыдущее значение записи дисплея сохраняется в записях активации.
Пример использования механизма дисплея в случае выполнения фрагмента
кода быстрой сортировки в стиле ML представлен на рисунке 1.7.
14
Рис 1.7. - Работа с дисплеем
Download