Добавлен: 29.10.2018
Просмотров: 48081
Скачиваний: 190
826
Глава 10. Изучение конкретных примеров: Unix, Linux и Android
в результате которой оболочка создала дочерний процесс, исполняющий программу cp.
Процесс оболочки заблокирован в ожидании завершения дочернего процесса, после
чего оболочка снова выведет приглашение к вводу и будет ждать ввода с клавиатуры.
Если бы пользователь на терминале 2 вместо cp ввел cc, то запустилась бы главная
программа компилятора языка C, который, в свою очередь, запустил бы несколько
дочерних процессов для выполнения различных проходов компилятора.
10.4. Управление памятью в Linux
Используемая в Linux модель памяти довольно проста, что должно обеспечить пере-
носимость программ, а также реализацию операционной системы Linux на машинах
с сильно различающимися блоками управления памятью, варьирующимися от элемен-
тарных (например, оригинальная IBM PC) до сложного оборудования со страничной
организацией. Эта область практически не изменилась за последние несколько десят-
ков лет. Разработанные решения хорошо себя зарекомендовали и не требовали серьез-
ной переработки. Мы рассмотрим модель управления памятью и методы ее реализации.
10.4.1. Фундаментальные концепции
У каждого процесса в системе Linux есть адресное пространство, состоящее из трех
логических сегментов: текста, данных и стека. Пример адресного пространства процесса
изображен на рис. 10.6 (процесс А). Текстовый сегмент (text segment) содержит машин-
ные команды, образующие исполняемый код программы. Он создается компилятором
и ассемблером при трансляции программы (написанной на языке высокого уровня,
например C или C++) в машинный код. Как правило, текстовый сегмент доступен
только для чтения. Самомодифицирующиеся программы вышли из моды примерно
в 1950 году, так как их было слишком сложно понимать и отлаживать. Таким образом,
не изменяются ни размеры, ни содержание текстового сегмента.
Сегмент данных
(data segment) содержит переменные, строки, массивы и другие
данные программы. Он состоит из двух частей: инициализированных и неинициали-
зированных данных. По историческим причинам вторая часть называется BSS (Block
Started by Symbol). Инициализированная часть сегмента данных содержит переменные
и константы компилятора, значения которых должны быть заданы при запуске про-
граммы. Все переменные в BSS должны быть инициализированы в нуль после загрузки.
Например, на языке C можно объявить символьную строку и в то же время проини-
циализировать ее. Если программа запускается, она предполагает, что эта строка уже
имеет свое начальное значение. Чтобы реализовать это, компилятор назначает строке
определенное место в адресном пространстве и гарантирует, что в момент запуска
программы по этому адресу будет располагаться необходимая строка. С точки зрения
операционной системы инициализированные данные не отличаются от текста про-
граммы — и тот и другой сегменты содержат сформированные компилятором последо-
вательности битов, которые должны быть загружены в память при запуске программы.
Неинициализированные данные необходимы лишь с точки зрения оптимизации. Если
глобальная переменная не инициализирована явным образом, то, согласно семантике
языка C, ее начальное значение устанавливается равным 0. На практике большинство
глобальных переменных не инициализируются, таким образом, их начальное значение
10.4. Управление памятью в Linux
827
Рис. 10.6. а — виртуальное адресное пространство процесса A; б — физическая память;
в — виртуальное адресное пространство процесса B
равно 0. Это можно реализовать следующим образом: создать область исполняемого
двоичного файла, точно равную по размеру числу байтов данных, и проинициализи-
ровать всю эту область нулями.
Однако (из экономии места в исполняемых файлах) так не делается. Вместо этого файл
содержит все явно инициализированные переменные прямо за текстом программы. Все
неинициализированные переменные собираются вместе после инициализированных,
так что компилятору нужно только записать в заголовок слово, содержащее количество
подлежащих выделению байтов.
Рассмотрим это еще раз на нашем примере (см. рис. 10.6, а). Здесь текст программы
занимает 8 Кбайт, инициализированные данные — также 8 Кбайт. Размер неиници-
ализированных данных (BSS) равен 4 Кбайт. Исполняемый файл содержит только
16 Кбайт (текст + инициализированные данные) плюс короткий заголовок, в котором
операционной системе дается указание выделить программе дополнительно 4 Кбайт
(после инициализированных данных) и обнулить их перед выполнением программы.
Этот трюк позволяет сэкономить 4 Кбайт нулей в исполняемом файле.
Для того чтобы избежать выделения полной нулей физической страницы, во время
инициализации Linux выделяет статическую нулевую страницу (защищенную от запи-
си страницу, заполненную нулями). Когда процесс загружается, указатель на область
его неинициализированных данных устанавливается на эту нулевую страницу. Когда
процесс пытается писать в эту область, то вмешивается механизм копирования при
записи и процессу выделяется настоящая страница.
В отличие от текстового сегмента, который не может изменяться, сегмент данных из-
меняться может. Программы все время модифицируют свои переменные. Более того,
многим программам требуется динамическое выделение памяти во время выполнения.
Для этого операционная система Linux разрешает сегменту данных расти при выделе-
нии памяти и уменьшаться при освобождении памяти. Программа может установить
размер своего сегмента данных при помощи системного вызова brk. Таким образом,
828
Глава 10. Изучение конкретных примеров: Unix, Linux и Android
чтобы выделить больше памяти, программа может увеличить размер своего сегмента
данных. Этим системным вызовом активно пользуется библиотечная процедура malloc
языка С, используемая для выделения памяти. Дескриптор адресного пространства
процесса содержит информацию о диапазоне динамически выделенных областей па-
мяти процесса (который обычно называется кучей — heap).
Третий сегмент — это сегмент стека (stack segment). На большинстве компьютеров
он начинается около старших адресов виртуального адресного пространства и растет
вниз к 0. Например, на 32-битной платформе х86 стек начинается с адреса 0xC0000000,
который соответствует предельному виртуальному адресу, видимому процессам
пользовательского режима. Если указатель стека оказывается ниже нижней границы
сегмента стека, то происходит аппаратное прерывание, при котором операционная
система понижает границу сегмента стека на одну страницу. Программы не управляют
явно размером сегмента стека.
Когда программа запускается, ее стек не пуст. Напротив, он содержит все переменные
окружения (оболочки), а также командную строку, введенную в оболочке для вызова
этой программы. Таким образом, программа может узнать параметры, с которыми она
была запущена. Например, когда вводится команда
cp src dest
то запускается программа cp со строкой «cp src dest» в стеке, что позволяет ей опреде-
лить имена файлов, с которыми ей предстоит работать. Строка представляется в виде
массива указателей на символы строки, что облегчает ее разбор.
Когда два пользователя запускают одну и ту же программу (например, текстовый
редактор), то в памяти можно было бы хранить две копии программы редактора.
Однако такой подход неэффективен. Вместо этого большинством систем Linux под-
держиваются текстовые сегменты совместного использования (shared text segemts) .
На рис. 10.6, а и в мы видим два процесса, A и B, совместно использующие общий тек-
стовый сегмент. На рис. 10.6, б мы видим возможную компоновку физической памяти,
где оба процесса совместно используют один и тот же фрагмент текста. Отображение
выполняется аппаратным обеспечением виртуальной памяти.
Сегменты данных и стека никогда не бывают общими, кроме как после выполнения
системного вызова fork, и то только те страницы, которые не модифицируются. Если
размер одного из сегментов должен быть увеличен, то отсутствие свободного места
в соседних страницах памяти не является проблемой, поскольку соседние виртуальные
страницы памяти не обязаны отображаться на соседние физические страницы.
На некоторых компьютерах аппаратное обеспечение поддерживает раздельные адрес-
ные пространства для команд и данных. Если такая возможность есть, то система
Linux может ее использовать. Например, на компьютере с 32-разрядными адресами
(при наличии возможности использования раздельных адресных пространств) можно
получить 2
32
бита адресного пространства для команд и дополнительно 2
32
бита адрес-
ного пространства для сегментов данных и стека. Условная или безусловная передача
управления по адресу 0 будет восприниматься как передача управления по адресу 0
в текстовом пространстве, тогда как при обращении к данным по адресу 0 будет исполь-
зоваться адрес 0 в пространстве данных. Таким образом, эта возможность удваивает
доступное адресное пространство.
В дополнение к динамическому выделению памяти процессы в Linux могут обращаться
к данным файлов при помощи отображения файлов на адресное пространство памяти
10.4. Управление памятью в Linux
829
(memory-mapped files) . Эта функция позволяет отображать файл на часть адресного
пространства процесса, чтобы можно было читать из файла и писать в файл так, как
если бы это был массив байтов, хранящийся в памяти. Отображение файла на адресное
пространство памяти делает произвольный доступ к нему существенно более легким,
нежели при использовании таких системных вызовов, как read и write. Совместный до-
ступ к библиотекам предоставляется именно при помощи этого механизма. На рис. 10.7
показан файл, одновременно отображенный на адресные пространства двух процессов
по различным виртуальным адресам.
Рис. 10.7. Два процесса совместно используют один отображенный на память файл
Дополнительное преимущество отображения файла на память заключается в том, что
два или более процесса могут одновременно отобразить на свое адресное пространство
один и тот же файл. Запись в этот файл одним из процессов мгновенно становится
видимой всем остальным. Таким образом, отображение на адресное пространство па-
мяти временного файла (который будет удален после завершения работы процессов)
представляет собой механизм реализации общей памяти (с высокой пропускной спо-
собностью) для нескольких процессов. В предельном случае два или более процесса
могут отобразить на память файл, покрывающий все адресное пространство, получая
тем самым такую форму совместного использования памяти, которая является чем-то
средним между процессами и потоками. В этом случае (как и у потоков) все адресное
пространство используется совместно, но каждый процесс обслуживает, например,
свои собственные открытые файлы и сигналы, что отличает этот вариант от потоков.
Однако на практике такой способ никогда не применяется.
10.4.2. Системные вызовы управления памятью в Linux
Стандарт POSIX не определяет системные вызовы для управления памятью . Эту об-
ласть посчитали слишком машинно зависимой, чтобы ее стандартизировать. Вместо
этого просто сделали вид, что проблемы не существует, и заявили, что программы, кото-
830
Глава 10. Изучение конкретных примеров: Unix, Linux и Android
рым требуется динамическое управление памятью, могут использовать библиотечную
процедуру malloc (определенную стандартом ANSI C). Таким образом, вопрос реализа-
ции процедуры malloc был вынесен за пределы стандарта POSIX. В некоторых кругах
такой подход считают перекладыванием бремени решения проблемы на чужие плечи.
На практике в большинстве систем Linux есть системные вызовы для управления
памятью. Наиболее распространенные системные вызовы перечислены в табл. 10.5.
Системный вызов brk указывает размер сегмента данных, задавая адрес первого байта
за его пределами. Если новое значение больше старого, то сегмент данных увеличива-
ется, в противном случае он уменьшается.
Таблица 10.5. Некоторые системные вызовы для управления памятью.
При возникновении ошибки код возврата s равен –1; a и addr — адреса памяти;
len — это длина; prot — управляет защитой; flags — различные биты;
fd — дескриптор файла; offset — смещение в файле
Системный вызов
Описание
s=brk(addr)
Изменить размер сегмента данных
a=mmap(addr, len, prot, flags, fd, offset)
Отобразить файл на память
s=unmap(addr, len)
Отменить отображение файла на память
Системные вызовы mmap и unmap управляют отображением файлов на адресное про-
странство памяти. Первый параметр addr системного вызова mmap указывает адрес, по
которому будет отображаться файл (или его часть). Он должен быть кратен размеру
страницы. Если этот параметр равен 0, то операционная система определяет этот адрес
сама и возвращает его в a. Второй параметр — len — задает количество отображаемых
байтов. Он также должен быть кратен размеру страницы. Третий параметр — prot —
задает режим защиты для отображаемого файла. Файл может быть помечен как до-
ступный для чтения, записи, исполнения (или любой комбинацией этих трех битов).
Четвертый параметр — flags — определяет, является отображаемый файл приватным
или доступным для совместного использования, а также содержит параметр addr жест-
кое требование или это всего лишь подсказка. Пятый параметр — fd — представляет
собой дескриптор отображаемого файла. Отображаться могут только открытые файлы.
Наконец, параметр offset сообщает, с какого места должен отображаться файл. Файл
может быть отображен начиная с границы страницы.
Второй системный вызов — unmap — отменяет отображение файла на память. Если
отменяется отображение только части файла, то остальная часть файла продолжает
отображаться на память.
10.4.3. Реализация управления памятью в Linux
Каждый процесс системы Linux на 32-разрядной машине обычно получает 3 Гбайт
виртуального адресного пространства для себя, а оставшийся 1 Гбайт памяти резерви-
руется для его страничных таблиц и других данных ядра. 1 Гбайт ядра не виден в поль-
зовательском режиме, но становится доступным, когда процесс переключается в режим
ядра. Память ядра обычно находится в нижних физических адресах, но отображается
в верхний гигабайт виртуального адресного пространства процесса (между адресами
0xC0000000 и 0xFFFFFFFF, это диапазон от 3 до 4 Гбайт). На ныне существующих