Управление областями памяти

Стек процесса

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

Cтек основной нити.

Он же стек процесса в однопоточном приложении. Начальный адрес выделяется ядром. Размер стека может быть изменён в командной строке до запуска программы командой 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 описывает режим доступа к памяти (не должен конфликтовать с режимом открытия файла).

  • PROT_EXEC данные в отображаемой памяти могут исполняться
  • PROT_READ данные можно читать
  • PROT_WRITE в эту память можно писать
  • PROT_NONE доступ к этой области памяти запрещен.

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

  • MAP_SHARED запись в эту область памяти будет эквивалентна записи в файл и изменения доступны другим процессам. Файл может не обновляться до вызова функций msync() или munmap().
  • MAP_PRIVATE неразделяемое отображение с механизмом copy-on-write. Запись в эту область приводит к созданию для процесса персональной копии данных в памяти и не влияет на файл.
  • MAP_ANONYMOUS память не отображается ни в какой файл, аргументы fd и offset игнорируются. Совместно с MAP_SHARED может использоваться для создания общей памяти, разделяемой с дочерними процессами
  • MAP_FIXED память обязательно отображается с адреса addr или возвращается ошибка. Не рекомендуется к использованию, так как сильно зависит от архитектуры процессора и конкретной ОС.

msync() сбрасывает изменения сделанные в памяти в отображенный файл. Параметры addr и length позволяют синхронизировать только часть отображенной памяти.

munmap() отменяет отображение файла в память и сохраняет сделанные в памяти изменения в файл.

Разделяемая память в System V IPC

В подсистеме межпроцессного взаимодействия 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 

OOM Killer

Выделение физической памяти в Linux оптимистично. В момент вызова brk() проверяется лишь то факт, что заказанная виртуальная память не превышает общего объёма физической памяти + размер файла подкачки. Реальное выделение памяти происходит при первой записи. В этом случае может оказаться, что вся доступная физическая память и своп уже распределены между другими процессами.

При нехватке физической памяти Linux запускает алгоритм Out of memory killer (OOM killer) который относительно случайно выбирает один из процессов и завершает его, освобождая тем самым физическую память.