Самые первые версии Unix работали на компьютерах без аппаратной поддержки виртуальной памяти, однако очень скоро такая поддержка появилась в большинстве процессорных архитектур и стала обязательным условием для возможности запуска Unix/Linux на соответствующей платформе. В линейке Intel аппаратная поддержка виртуальной памяти появилась в процессорах i286. С выходом этих процессоров на рынок связано появление лицензионной версии Unix для ПК - Xenix от Microsoft.
Несмотря на то, что современные процессоры адресуют память с точностью до байта, основными единицами управления памятью в Unix являются страницы.
Страница - это минимальная единица отображения непрерывного диапазона адресов виртуальной памяти на непрерывный диапазон адресов физической памяти. Соответственно, выделение физической память для данных процессов и ядра, а так же буферов дискового кеша и буферов для получения данных с устройств по DMA всегда выполняется страницами.
Страничная организация памяти поддерживается специализированным аппаратным устройством - модулем управления памятью (MMU, Memory Management Unit), которое выполняет преобразование виртуальных адресов в физические. MMU может быть интегрировано в процессор или входить в чипсет.
Размеры страниц отличаются для различных аппаратных платформ.Можно считать, что для 32-разрядных аппаратных платформ размер страницы равен 4 Кбайта, а для 64-разрядных платформ - 8 Кбайт. Современные процессоры от Intel позволяют выбирать размер страницы, и, соответственно, ядро Linux позволяет управлять этим выбором.
Отображение виртуальной памяти в физическую осуществляется через таблиц страниц, в которых каждой виртуальной странице ставится в соответствие физический адрес. Таблицы страниц хранятся в ОЗУ, а физический адрес текущей таблицы страниц заносится в специальный регистр процессора.
Для каждого процесса в момент его создания инициализируется своя таблица страниц. Таким образом одни и те же адреса в виртуальной памяти различных процессов ссылаются на различные адреса физической памяти. Благодаря этому, физические страницы памяти одного процесса становятся недоступными для других процессов. Таблица страниц ядра отображает часть адресов на физическую память, в которой расположено само ядро, а вторую часть - на страницы памяти какого-либо процесса. Такая таблица позволят ядру получить доступ к физической памяти процессов для копирования входных/выходных данных системных вызовов. Например, при вызове read(fd,buf,size)
код в ядре имеет доступ к буферу buf для заполнения его данными, прочитанными из файла.
Таблица страниц процесса не отображается в его виртуальную память, а команда процессора, загружающая адрес таблицы страниц в соответствующий регистр, является привилегированной и не может быть выполнена в коде пользовательского процесса. Это не даёт процессам возможности хоть как-то повлиять на таблицы страниц. В момент переключения на выполнение кода ядра (по аппаратному прерыванию или при системном вызове) процессор переключается в привилегированный режим, после чего код в ядре может указать в регистре свою таблицу страниц, которая включает отображение всей физической памяти, после чего ядро может поменять таблицу страниц любого процесса.
Интересное замечание: существуют процессорные архитектуры, в которых физическая память может быть больше виртуальной. Так серверные материнские платы на процессорах x32 позволяют устанавливать больше чем 4 Гбайта оперативной памяти, в этом случае ядру приходится модифицировать часть собственной таблицы страниц, чтобы получить доступ к нужным участкам физической памяти.
Часть ссылок в таблице страниц может иметь специальное значение, указывающее на то, что страница не отображается на физическую память. Данный механизм используется для реализации файлов подкачки (swap). При попытке обращения к такой "несуществующей" странице, возникает аппаратное прерывание, которое обрабатывается в ядре. Код ядра может выделить процессу свободную страницу физической памяти, загрузить в неё данные из файла подкачки, поменять таблицу страниц так, чтобы требуемая виртуальная память отображалась на свежеподготовленную физическую ти вернуть управления процессу. Выполнение процесса начнётся с той же инструкции доступа к памяти, на которой возникло прерывание, но теперь эта инструкция выполнится нормально, поскольку изменилась ТС.
В некоторых случаях возможно отображение одной физической страницы памяти в виртуальную память нескольких процессов. Такое совместное использование физической памяти называется разделяемой памятью (shared memory). Данный механизм используется для обмена данными между процессами и требует аппаратной или программной поддержки в виде семафоров, для синхронизации параллельного доступа к памяти.
Часть физической памяти используется для хранения файлового кеша. В кеше хранится блоки данных файла размерами кратными странице памяти. При операциях считывания/записи в ядре происходит копирование нужного числа страниц между памятью процесса и кешем, а операции с файловой системой откладываются по времени. Если физическую память требуется освободить, то она сохраняется в файл, если кто-то обращается к странице, отсутствующей в кеше, она считывается из файла. Как правило, страницы кеша доступны только ядру, однако существует несколько случаев, когда они отображаются непосредственно в виртуальную память процессов. Механизм отображения страниц кеша в память процесса называется отображением файлов в память (memory mapped file). Данный механизм может использоваться как альтернатива разделяемой памяти (анонимные файлы в памяти), а так же как механизм совместного использования кода программ, путём отображения секции кода из исполняемого файла в виртуальное адресное пространство процессов, которые этот код исполняют.
При создании нового процесса вызовом fork() (в Linux фактически вызывается clone()) таблица страниц копируется в дочерний процесс и некоторое время родительский и дочерний процесс совместно используют общую физическую память. В дальнейшем, при изменении какой-либо страницы в одном из двух процессов для этого процесса создаётся копия страницы в физической памяти и соответственно изменяется таблица страниц. Данный механизм позволяет экономить время при создании нового процесса. Этот механизм называется "копирование при записи" (Copy on write, COW).
Созданиие новых потоков в Linux вызовом clone() аналогично созданию нового процесса, но дочерний процесс наследует от родительского процесса таблицу страниц. Таким образом, потоки в Linux можно рассматривать как дочерние процессы, использующие общую с родительским процессом виртуальную память. Главным отличием между процессами является размещение стека в виртуальной памяти.
Вызов execve("prog_file"), загружающий в память процесса новую программу, полностью изменяет таблицу страниц, освобождая занятую физическую память и выделяя новую, которая инициализируется в соответвии с данными, хранящимися в исполняемом файле.
Вся виртуальная память процесса в Linux поделена на несколько областей (сегментов), различного назначения. Выделяются следующие области:
Каждая область памяти описывается структурой, которая хранит начало и конец области в виртуальном пространстве процесса, позицию в файле, если область является отображением файла в память, права доступа и флаги, описывающие свойства области.
Флаги, описывающие права доступа к страницам, входящих в область:
Просмотреть список областей процесса можно в файле /proc/
Пример вывода программой cat собственных областей памяти. Буква p в флагах доступа означает private. Если бы область использовалась совместно, то стояла бы буква s -shared.
$ cat /proc/self/maps
00400000-0040b000 r-xp 00000000 fd:01 134395423 /usr/bin/cat
0060b000-0060c000 r--p 0000b000 fd:01 134395423 /usr/bin/cat
0060c000-0060d000 rw-p 0000c000 fd:01 134395423 /usr/bin/cat
017f1000-01812000 rw-p 00000000 00:00 0 [heap]
7f5f529df000-7f5f58f08000 r--p 00000000 fd:01 67442653 /usr/lib/locale/locale-archive
7f5f58f08000-7f5f590cb000 r-xp 00000000 fd:01 201328510 /usr/lib64/libc-2.17.so
7f5f590cb000-7f5f592ca000 ---p 001c3000 fd:01 201328510 /usr/lib64/libc-2.17.so
7f5f592ca000-7f5f592ce000 r--p 001c2000 fd:01 201328510 /usr/lib64/libc-2.17.so
7f5f592ce000-7f5f592d0000 rw-p 001c6000 fd:01 201328510 /usr/lib64/libc-2.17.so
7f5f592d0000-7f5f592d5000 rw-p 00000000 00:00 0
7f5f592d5000-7f5f592f7000 r-xp 00000000 fd:01 201328504 /usr/lib64/ld-2.17.so
7f5f594e8000-7f5f594eb000 rw-p 00000000 00:00 0
7f5f594f5000-7f5f594f6000 rw-p 00000000 00:00 0
7f5f594f6000-7f5f594f7000 r--p 00021000 fd:01 201328504 /usr/lib64/ld-2.17.so
7f5f594f7000-7f5f594f8000 rw-p 00022000 fd:01 201328504 /usr/lib64/ld-2.17.so
7f5f594f8000-7f5f594f9000 rw-p 00000000 00:00 0
7ffc13623000-7ffc13644000 rw-p 00000000 00:00 0 [stack]
7ffc13785000-7ffc13787000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Прикрепленный файл | Размер |
---|---|
memory-maps.png | 33.08 КБ |
Подробности связанные с аппаратной поддержкой управления памятью в процессорах, совместимых с i386, можно посиотреть по ссылке. Ссылка: Исследование модели памяти Linux
Размеры сегментов text, data и bss исполняемого файла можно посмотреть командой size
:
$ size /bin/ls
text data bss dec hex filename
103119 4768 3360 111247 1b28f /bin/ls
В новых версиях Linux положение стека и кучи рандомизируется случайными смещениями, а область Mmap растёт вниз. Ссылка: Организация памяти процесса
#include <unistd.h>
#include <malloc.h>
#include <stdio.h>
#include <sys/mman.h>
int bss, data=10;
int main(int argc, char *argv[])
{
int stack;
void *heap, *brk, *mmp;
heap=malloc(1);
brk=sbrk(0);
mmp=mmap(0,1,PROT_READ,MAP_SHARED|MAP_ANONYMOUS,-1,0);
printf("Text=%p\nData=%p\nBSS=%p\nHeap=%p\nBrk=%p\nlibc.so=%p\nMmap=%p\nStack=%p\nArgv=%p\n ",main,&data,&bss,heap,brk,printf,mmp,&stack,argv);
sleep(100);
return 0;
}
$ ./testmap
Text =0x0045c690
Data =0x0045e02c
BSS =0x0045e034
Heap =0x01435008
Brk =0x01456000
libc.so=0xb75bb940
Mmap =0xb773a000
Stack =0xbfe8f850
Argv =0xbfe8f924
$ ./testmap >/dev/null &
[1] 6662
$ cat /proc/6662/maps
0042a000-0042b000 r-xp 00000000 08:01 305009 /home/student/testmap
0042b000-0042c000 r--p 00000000 08:01 305009 /home/student/testmap
0042c000-0042d000 rw-p 00001000 08:01 305009 /home/student/testmap
00ece000-00eef000 rw-p 00000000 00:00 0 [heap]
b7603000-b77b4000 r-xp 00000000 08:01 151995 /lib/i386-linux-gnu/libc-2.24.so
b77b4000-b77b5000 ---p 001b1000 08:01 151995 /lib/i386-linux-gnu/libc-2.24.so
b77b5000-b77b7000 r--p 001b1000 08:01 151995 /lib/i386-linux-gnu/libc-2.24.so
b77b7000-b77b8000 rw-p 001b3000 08:01 151995 /lib/i386-linux-gnu/libc-2.24.so
b77b8000-b77bb000 rw-p 00000000 00:00 0
b77cb000-b77cc000 r--s 00000000 00:05 100324 /dev/zero (deleted)
b77cc000-b77ce000 rw-p 00000000 00:00 0
b77ce000-b77d0000 r--p 00000000 00:00 0 [vvar]
b77d0000-b77d2000 r-xp 00000000 00:00 0 [vdso]
b77d2000-b77f5000 r-xp 00000000 08:01 138298 /lib/i386-linux-gnu/ld-2.24.so
b77f5000-b77f6000 r--p 00022000 08:01 138298 /lib/i386-linux-gnu/ld-2.24.so
b77f6000-b77f7000 rw-p 00023000 08:01 138298 /lib/i386-linux-gnu/ld-2.24.so
bff5d000-bff7e000 rw-p 00000000 00:00 0 [stack]
$ uname -a
Linux antix-1 4.9.87-antix.1-486-smp #1 SMP Tue Mar 13 12:29:54 EDT 2018 i686 GNU/Linux
Вообще говоря, стеков в процессе может быть много и размещаться они могут в разных областях виртуальной памяти. Вот несколько примеров:
Он же стек процесса в однопоточном приложении. Начальный адрес выделяется ядром. Размер стека может быть изменён в командной строке до запуска программы командой ulimit -s <размер в килобайтах>
или вызовом setrlimit
#include <sys/resource.h>
...
struct rlimit rl;
int err;
rl.rlim_cur = 64*1024*1024;
err = setrlimit(RLIMIT_STACK, &rl);
Стек обработчика сигналов может быть расположен в любой области памяти, доступной на чтение и запись. Альтернативный стек создаётся вызовом sigaltstack(new, old), которому передаются указатели на структуры, описывающие стек:
typedef struct {
void *ss_sp; /* Base address of stack */
int ss_flags; /* Flags */
size_t ss_size; /* Number of bytes in stack */
} stack_t;
После создания альтернативного стека можно задавать обработчик сигнала, указав в параметрах функции sigaction() флаг SS_ONSTACK.
При создании новой нити для всегда создаётся новый стек. Функция инициализации нити pthread_attr_init(pthread_attr_t *attr) позволяет вручную задать базовый адрес стека нити и его размер через поля attr.stackaddr и attr.stacksize. В большинстве случаев рекомендуется предоставить выбор адреса и размера стека системе, задав attr.stackaddr=NULL; attr.stacksize=0;.
Динамическое выделение памяти в куче (heap) реализовано на уровне стандартных библиотек C/C++ (функция malloc() и оператор new соответственно). Для распределения памяти из кучи процесс должен сообщить ядру, какой размер виртуальной памяти должен быть отображён на физическую память. Для этого выделяется участок виртуальной памяти, расположенный между адресами start_brk и brk. Величина start_brk фиксирована, а brk может меняться в процессе выполнения программы. Brk (program break - обрыв программы) - граница в виртуальной памяти на которой заканчивается отображение в физическую память. В современном Linux за этой границей могут быть отображения файлов и кода ядра в память процесса, но в оригинальном Unix это был "край" памяти программы. Начальное значение brk - start_brk устанавливается в момент загрузки программы из файла вызовом execve() и указывает на участок после инициализированных (data) и неинициализированных (BSS) глобальных переменных.
Для того, чтобы изменить размер доступной физической памяти, необходимо изменить верхнюю границу области кучи.
#include <unistd.h>
int brk(void *addr); //явное задание адреса
void *sbrk(intptr_t increment); //задание адреса относительно текущего значения
// возвращает предыдущее значение адреса границы
Вызов brk()
устанавливает максимальный адрес виртуальной памяти, для которого в сегменте данных выделяется физическая память. Увеличение адреса равноценно запросу физической памяти, уменьшение - освобождению физической памяти.
Вызов sbrk(0)
позволяет узнать текущую границу сегмента памяти.
В Linux вызов brk() транслируется в вызов функции ядра do_mmap(), изменяющий размер анонимного файла, отображаемого в память.
do_mmap(NULL, oldbrk, newbrk-oldbrk,
PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_FIXED|MAP_PRIVATE, 0)
Отображение содержимого файла в виртуальную память mmap()
задействует кэш файловой системы. Некоторые страницы виртуальной памяти отображаются на физические страницы кэша. В тех ситуациях, когда обычные физические страницы сохраняются в своп, страницы, отображенные в файлы сохраняются в сами файлы. Отображение файлов в память может использоваться, в частности, для отображения секций кода исполняемых файлов и библиотек, в соответствующие сегменты памяти процесса.
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int msync(void *addr, size_t length, int flags);
int munmap(void *addr, size_t length);
Функция mmap()
отображает length
байтов, начиная со смещения offset
файла, заданного файловым дескриптором fd
, в память, начиная с адреса addr
. Параметр addr является рекомендательным, и обычно бывает выставляется в NULL. mmap()
возвращает фактический адрес отображения или значение MAP_FAILED (равное (void *) -1) в случае ошибки.
Аргумент prot
описывает режим доступа к памяти (не должен конфликтовать с режимом открытия файла).
Параметр flags указывает, видны ли изменения данных в памяти другим процессам, отобразившим этот фрагмент файла в свою память, и влияют ли эти изменения на сам файл.
msync()
или munmap()
.addr
или возвращается ошибка. Не рекомендуется к использованию, так как сильно зависит от архитектуры процессора и конкретной ОС.msync()
сбрасывает изменения сделанные в памяти в отображенный файл. Параметры addr
и length
позволяют синхронизировать только часть отображенной памяти.
munmap()
отменяет отображение файла в память и сохраняет сделанные в памяти изменения в файл.
В подсистеме межпроцессного взаимодействия System V IPC (System five interprocess communication - названо в по имени версии Unix, в которой эта подсистема появилась) для совместной работы с памятью используется резервирование физической памяти на уровне ядра. После резервирования процессы могут отображать зарезервированную память в своё виртуальное адресное пространство используя для идентификации зарезервированного участка идентификатор, генерируемый специальным образом.
Данное резервирование не привязано к какому-либо процессу и сохраняется даже тогда, когда ни один процесс эту физическую память не использует. Таким образом выделение памяти для совместного использования становится "дорогим" с точки зрения использованных ресурсов. Программа, резервирующая области памяти для совместного доступа и не освобождающая их, может либо исчерпать всю доступную память, либо занять максимально доступное в системе количество таких областей.
У областей совместно используемой памяти, как и других объектов System V IPC есть атрибуты "пользователь-владелец", "группа-владельца", "пользователь-создатель", "группа-создателя", а так же права на чтение и запись для владельца, группы-владельца и остальных, аналогичные файловым. Например: rw- r-- ---.
Пример вызовов для работы общей памятью:
// создание ключа на основе inode файла и номера проекта
// файл не ложен удаляться до завершения работы с общей памятью
key_t key=ftok("/etc/hostname", 456);
// получение идентификатора общей памяти на основе ключа
// размером size байт с округлением вверх
// до размера кратного размеру страницы
//
// опция IPC_CREAT - говорит, что если память ещё не зарезервирована
// то должна быть выполнена резервация физической памяти
size=8000
int shmid=shmget(key, size, IPC_CREAT);
// подключение зарезервированной памяти к виртуальному адресному пространству
// второй параметр - желаемый начальный адрес отображения
// третий параметр - флаги, такие как SHM_RDONLY
int *addr=(int *)shmat(shmid, NULL, 0);
// можно работать с памятью по указателю
addr[10]=23;
// отключение разделяемой памяти от виртуального адресного пространства
int err;
err=shmdt(addr);
// освобождение зарезервированной памяти
err=shmctl(shmid, IPC_RMID, NULL);
Список всех зарезервированных областей памяти в системе можно просмотреть командой lspci -m
:
lsipc -m
KEY ID PERMS OWNER SIZE NATTCH STATUS CTIME CPID LPID COMMAND
0xbe130fa1 3112960 rw------- root 1000B 11 May03 7217 9422 /usr/sbin/httpd -DFOREGROUND
0x00000000 557057 rw------- usr74 384K 2 dest Apr28 17752 7476 kdeinit4: konsole [kdeinit]
0x00000000 5898243 rw------- usr92 512K 2 dest 12:05 5265 9678 /usr/bin/geany /home/s0392/1_1.s
0x00000000 4521988 rw------- usr75 384K 2 dest May06 22351 16323 sview
0x00000000 3276805 rw------- usr15 384K 1 dest May05 24835 15236
0x00000000 4587530 rw------- usr75 2M 2 dest May06 19404 16323 metacity
Выделение физической памяти в Linux оптимистично. В момент вызова brk() проверяется лишь то факт, что заказанная виртуальная память не превышает общего объёма физической памяти + размер файла подкачки. Реальное выделение памяти происходит при первой записи. В этом случае может оказаться, что вся доступная физическая память и своп уже распределены между другими процессами.
При нехватке физической памяти Linux запускает алгоритм Out of memory killer (OOM killer) который относительно случайно выбирает один из процессов и завершает его, освобождая тем самым физическую память.
Программа size выдает размер секций исполняемого файла. По умолчанию выдаётся суммарный размер по типам в формате Berkley:
$ size /bin/ls
text data bss dec hex filename
103119 4768 3360 111247 1b28f /bin/ls
Опция -A выдает все секции в формате SystemV:
$ size -A /bin/ls
/bin/ls :
section size addr
.interp 28 4194872
.note.ABI-tag 32 4194900
...
.init 26 4202832
.plt 1808 4202864
.plt.got 24 4204672
.text 65866 4204704
.fini 9 4270572
...
.data 576 6402976
.bss 3360 6403552
.gnu_debuglink 16 0
.gnu_debugdata 3296 0
Total 114559
$ objdump -f ./mmp
./mmp: file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x080483e0
$ objdump -h ./mmp
./mmp: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 00000013 08048154 08048154 00000154 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.ABI-tag 00000020 08048168 08048168 00000168 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 08048188 08048188 00000188 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .gnu.hash 00000024 080481ac 080481ac 000001ac 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .dynsym 00000090 080481d0 080481d0 000001d0 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynstr 00000063 08048260 08048260 00000260 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .gnu.version 00000012 080482c4 080482c4 000002c4 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .gnu.version_r 00000020 080482d8 080482d8 000002d8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .rel.dyn 00000008 080482f8 080482f8 000002f8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .rel.plt 00000030 08048300 08048300 00000300 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .init 00000023 08048330 08048330 00000330 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
11 .plt 00000070 08048360 08048360 00000360 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .plt.got 00000008 080483d0 080483d0 000003d0 2**3
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .text 00000242 080483e0 080483e0 000003e0 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .fini 00000014 08048624 08048624 00000624 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .rodata 00000058 08048638 08048638 00000638 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
16 .eh_frame_hdr 0000002c 08048690 08048690 00000690 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
17 .eh_frame 000000b0 080486bc 080486bc 000006bc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
18 .init_array 00000004 08049f08 08049f08 00000f08 2**2
CONTENTS, ALLOC, LOAD, DATA
19 .fini_array 00000004 08049f0c 08049f0c 00000f0c 2**2
CONTENTS, ALLOC, LOAD, DATA
20 .jcr 00000004 08049f10 08049f10 00000f10 2**2
CONTENTS, ALLOC, LOAD, DATA
21 .dynamic 000000e8 08049f14 08049f14 00000f14 2**2
CONTENTS, ALLOC, LOAD, DATA
22 .got 00000004 08049ffc 08049ffc 00000ffc 2**2
CONTENTS, ALLOC, LOAD, DATA
23 .got.plt 00000024 0804a000 0804a000 00001000 2**2
CONTENTS, ALLOC, LOAD, DATA
24 .data 00000008 0804a024 0804a024 00001024 2**2
CONTENTS, ALLOC, LOAD, DATA
25 .bss 00000008 0804a02c 0804a02c 0000102c 2**2
ALLOC
26 .comment 0000002d 00000000 00000000 0000102c 2**0
CONTENTS, READONLY