Виртуальная память
Каждый процесс имеет свое независимое адресное пространство, и данные процесса хранятся в этом адресном пространстве, так что процессы не влияют друг на друга. В качестве примера возьмем 32-разрядную систему x86 Linux, диапазон адресного пространства которой составляет 0x00000000-0xFFFFFFFF, что соответствует 4G. Это 4G не настоящая физическая память 4G.Представьте, что если каждый процесс будет занимать 4G физической памяти, какой бы большой ни была флешка, он не выдержит такого потребления. Поэтому современные операционные системы переняли дизайн виртуальной памяти. Как виртуальная память, так и физическая память разделены на множество страниц в соответствии с определенным размером.Например, если каждая страница имеет размер 4 КБ, 4 ГБ виртуальной памяти можно разделить на 1048576 страниц. Страницы виртуальной памяти сопоставляются со страницами фактической физической памяти с помощью таблицы сопоставления, как показано на следующем рисунке.
Не все виртуальное адресное пространство 4G используется.Немногие программы используют виртуальное адресное пространство, и не все используемые виртуальные адреса сопоставляются с физической памятью. На самом деле в физическую память отображается только часть, а часть — в область подкачки (раздел подкачки) диска, что значительно сокращает занятие физической памяти одной программой.
Процесс доступа процесса к адресу в виртуальном адресном пространстве выглядит следующим образом: процесс обращается к адресу, сначала вычисляет страницу на основе этого адреса, а затем запрашивает таблицу сопоставления.Данные страницы извлекаются и возвращаются; если страница не отображается на фрейм физической памяти, данные страницы загружаются с диска в память, а таблица отображения обновляется. Есть две предпосылки, почему он может быть разработан таким образом:
(1) данные, к которым только что был осуществлен доступ, вероятно, будут доступны снова;
(2) принцип 28 — 80% времени приходится на доступ к 20% данных;
Распределение памяти Linux
Управление памятью в Linux делит адрес 4GB на две части, одна это память в пространстве ядра, а другая память в пространстве пользователя.Пространство ядра занимает адреса в диапазоне 3G-4G, а пространство пользователя занимает адреса в диапазоне 0G-3G. Пространство ядра — это память, используемая ядром операционной системы, а пространство пользователя — это память, которую могут использовать программы, работающие в операционной системе.
Пространство ядра Linux
Пространство ядра Linux — это адресное пространство, используемое ядром Linux.Ядро Linux всегда находится в памяти, и процессы пользовательского уровня не могут получить доступ к адресному пространству ядра. Таблица отношений отображения между таблицей страниц виртуальной памяти и упомянутой ранее физической памятью хранится в адресном пространстве ядра. Распределение памяти в пространстве ядра очень сложно, в этой статье в основном обсуждается выделение памяти в пользовательском пространстве.
Пользовательское пространство Linux
Порядок пользовательского адресного пространства Linux от младшего к старшему можно разделить на: текстовый сегмент (текстовый сегмент), сегмент инициализированных данных (сегмент данных), сегмент неинициализированных данных (сегмент Bss), куча (куча), стек (стек) и среда. Область переменных (переменные среды)
текстовый сегмент
Самый младший бит пользовательского пространства — это текстовый сегмент, который содержит машинный код для запуска программы. Текстовый сегмент имеет атрибут только для чтения, чтобы предотвратить случайное изменение инструкции процессом и сбой программы. А для многопроцессорных программ один и тот же программный код может использоваться совместно, что уменьшает занимаемую физическую память.
Но текстовый сегмент начинается не с 0x0000 0000, а с 0x0804 8000. Адрес ниже 0x0804 8000 является зарезервированной областью, и процесс не может получить доступ к данным в этом сегменте адреса, поэтому язык C будет указывать на 0 как на пустой указатель.
Инициализировать сегмент данных
Над текстовым сегментом находится сегмент инициализированных данных, а сегмент данных содержит глобальные переменные инициализации отображения и статические переменные. Когда программа загружается в память, значения этих данных считываются из исполняемого файла и загружаются в память. Поэтому значение этих переменных необходимо сохранить в исполняемом файле.
Bss
Сегмент Bss содержит неинициализированные глобальные переменные и статические переменные, а также содержит глобальные переменные, которые явно инициализированы значением 0 (в соответствии с реализацией компилятора). Когда программа загружается в память, этот раздел памяти инициализируется 0. Исполняемому файлу нужно только сохранить начальный адрес этого участка памяти, тем самым уменьшив размер исполняемого файла.
куча
Куча растет снизу вверх (в зависимости от реализации) и используется для динамического выделения памяти. Вершина кучи становится точкой прерывания программы, а положение вершины кучи можно настроить с помощью функций brk и sbrk. Язык c реализует динамическое выделение памяти через функцию malloc, а освобождает выделенную память через free Реализация этих двух функций будет подробно описана позже. Память в куче поддерживается двусвязным списком, и каждый узел связанного списка хранит информацию, например о том, доступен ли размер памяти или нет. Выделение памяти в куче может вызвать следующие проблемы: (1) Если выделенная память не будет освобождена, это вызовет утечку памяти; (2) Частое выделение небольших блоков памяти может привести к тому, что небольшие блоки памяти останутся в куче, что называется фрагментацией памяти;
куча
Стек — это сегмент, который динамически увеличивается и уменьшается, причем стек растет сверху вниз. Стек состоит из кадров стека. Каждый раз, когда вызывается функция, система выделяет кадр стека для каждой вызываемой в данный момент функции. Фрейм стека хранит фактические параметры параметров и локальные переменные, используемые в функции. Когда функция возвращается, функция Кадр стека функции будет извлечен, а локальные переменные в функции будут уничтожены.
переменная среды
В стеке еще есть небольшое пространство, в котором хранятся переменные окружения и параметры командной строки, причем как переменные окружения, так и параметры командной строки представляют собой массивы argv и environ, указывающие на строки.
malloc и бесплатная реализация
Выделение динамической памяти достигается за счет поддержки двусвязного списка, и каждый узел сохраняет использование размера блока памяти. Существуют различные алгоритмы распределения malloc, такие как первый принцип адаптации и принцип оптимальной адаптации. Здесь мы используем принцип первого соответствия. На самом деле функция free, когда на вершине кучи находится большой блок памяти, будет уменьшать адрес вершины кучи через функцию sbrk, которой мы здесь не занимаемся.
malloc и бесплатные функции
/* my_malloc.h */
#ifndef _MY_MALLOC_H_
#define _MY_MALLOC_H_
#include <unistd.h>
#include <stdlib.h>
#include <stdbool.h>
//保存每个内存块的信息
typedef struct _MEM_CONTROL_BLOCK_
{
unsigned int uiBlockSize; //当前块的大小
unsigned int uiPrevBlockSize; //前一个内存块的大小
bool bIsAvalible; //该内存块是否已经被分配内存
} MemControlBlock;
#define INIT_BLOCK_SIZE (0x21000) //初始化堆的大小
#define MEM_CONTROL_BLOCK_SIZE (sizeof(MemControlBlock))
void* g_pMallocStartAddr; //维护堆底地址
void* g_pMallocEndAddr; //维护堆顶地址
//初始化堆段
void malloc_init()
{
g_pMallocStartAddr = sbrk(INIT_BLOCK_SIZE);
g_pMallocEndAddr = g_pMallocStartAddr + INIT_BLOCK_SIZE;
//初始化时堆只有一个内存块
MemControlBlock* pFirstBlock;
pFirstBlock = (MemControlBlock*)g_pMallocStartAddr;
pFirstBlock->bIsAvalible = 1;
pFirstBlock->uiBlockSize = INIT_BLOCK_SIZE;
pFirstBlock->uiPrevBlockSize = 0;
}
void* my_malloc(unsigned int uiMallocSize)
{
static bool bIsInit = false;
if(!bIsInit)
{
malloc_init();
bIsInit = true;
}
void* pCurAddr = g_pMallocStartAddr;
MemControlBlock* pCurBlock = NULL;
MemControlBlock* pLeaveBlock = NULL;
void* pRetAddr = NULL;
//判断是否到了堆顶
while (pCurAddr < g_pMallocEndAddr)
{
pCurBlock = (MemControlBlock*)pCurAddr;
if (pCurBlock->bIsAvalible)
{
//判断该块可用的内存大小是否满足分配的需求
if (pCurBlock->uiBlockSize - MEM_CONTROL_BLOCK_SIZE >= uiMallocSize)
{
//该块分配空间后剩余的空间是否足够分配一个控制块,如果不能则把该块全部分配了
if ((pCurBlock->uiBlockSize - MEM_CONTROL_BLOCK_SIZE) <= (uiMallocSize + MEM_CONTROL_BLOCK_SIZE))
{
pCurBlock->bIsAvalible = 0;
pRetAddr = pCurAddr;
break;
}
else
{
//分配内存,并将剩余的空间独立成一个块
pLeaveBlock = (MemControlBlock*)(pCurAddr + MEM_CONTROL_BLOCK_SIZE + uiMallocSize);
pLeaveBlock->bIsAvalible = 1;
pLeaveBlock->uiBlockSize = pCurBlock->uiBlockSize - MEM_CONTROL_BLOCK_SIZE - uiMallocSize;
pLeaveBlock->uiPrevBlockSize = MEM_CONTROL_BLOCK_SIZE + uiMallocSize;
pCurBlock->bIsAvalible = 0;
pCurBlock->uiBlockSize = MEM_CONTROL_BLOCK_SIZE + uiMallocSize;
pRetAddr = pCurAddr;
break;
}
}
else
{
pCurAddr += pCurBlock->uiBlockSize;
continue;
}
}
else
{
pCurAddr += pCurBlock->uiBlockSize;
continue;
}
}
//已有的块中找不到合适的块,则通过sbrk函数增加堆顶地址
if (!pRetAddr)
{
unsigned int uiAppendMemSize = uiMallocSize + MEM_CONTROL_BLOCK_SIZE;
unsigned int uiPrevBlockSize = pCurBlock->uiBlockSize;
if(*((int*)sbrk(uiAppendMemSize)) == -1)
{
return NULL;
}
g_pMallocEndAddr = g_pMallocEndAddr + uiAppendMemSize;
pCurBlock = (MemControlBlock*)pCurAddr;
pCurBlock->bIsAvalible = 0;
pCurBlock->uiBlockSize = uiAppendMemSize;
pCurBlock->uiPrevBlockSize = uiPrevBlockSize;
pRetAddr = pCurAddr;
}
return pRetAddr + MEM_CONTROL_BLOCK_SIZE;
}
void my_free(void* pFreeAddr)
{
if (pFreeAddr == NULL)
{
return;
}
MemControlBlock* pCurBlock = (MemControlBlock*)(pFreeAddr - MEM_CONTROL_BLOCK_SIZE);
MemControlBlock* pPrevBlock = (MemControlBlock*)(pFreeAddr - MEM_CONTROL_BLOCK_SIZE - pCurBlock->uiPrevBlockSize);
MemControlBlock* pNextBlock = (MemControlBlock*)(pFreeAddr - MEM_CONTROL_BLOCK_SIZE + pCurBlock->uiBlockSize);
if (pCurBlock->bIsAvalible == 0)
{
pCurBlock->bIsAvalible = 1;
//判断前一个内存块是否可用
if (pCurBlock->uiPrevBlockSize != 0 && pPrevBlock->bIsAvalible)
{
pPrevBlock->uiBlockSize += pCurBlock->uiBlockSize;
if((void*)pNextBlock < g_pMallocEndAddr)
{
pNextBlock->uiPrevBlockSize = pPrevBlock->uiBlockSize;
}
}
}
return;
}
#endif //_MY_MALLOC_H_
тестовая программа
Эта тестовая программа предназначена для динамического выделения памяти в куче в цикле, а затем освобождения памяти.Вы можете выбрать расположение начального блока для освобождения или выбрать количество блоков в интервале.
/*test_malloc.c*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "my_malloc.h"
#define MAX_ALLOCS 1000
int main(int argc, char const *argv[])
{
if(argc < 3 || strcmp(argv[1], "--help") == 0)
{
printf("Usage: %s num_allocs block_size [step [min [max]]]\n", argv[1]);
return -1;
}
int iNumAllocs = atoi(argv[1]);
int iBlockSize = atoi(argv[2]);
if(iBlockSize > MAX_ALLOCS)
{
printf("Out range of the max mallocs.\n");
}
int iStep, iMin, iMax;
iStep = (argc > 3) ? atoi(argv[3]) : 1;
iMin = (argc > 4) ? atoi(argv[4]) : 1;
iMax = (argc > 5) ? atoi(argv[5]) : iNumAllocs;
printf("Initial program break: %10p\n", sbrk(0));
void* pArr[iNumAllocs];
memset(pArr, 0, sizeof(pArr));
for(int i = 0; i < iNumAllocs; ++i)
{
pArr[i] = my_malloc(iBlockSize);
if(pArr[i] == NULL)
{
printf("malloc failed.\n");
return -1;
}
printf("After malloc, program break: %10p\n", sbrk(0));
}
printf("After alloc, program break: %10p\n", sbrk(0));
for (int i = iMin; i <= iMax; i += iStep)
{
my_free(pArr[i - 1]);
}
printf("After free, program break: %10p\n", sbrk(0));
return 0;
}
Результаты теста
$:./a.out 10 100 2
Initial program break: 0xa13000
After malloc, program break: 0xa34000
After malloc, program break: 0xa34000
After malloc, program break: 0xa34000
After malloc, program break: 0xa34000
After malloc, program break: 0xa34000
After malloc, program break: 0xa34000
After malloc, program break: 0xa34000
After malloc, program break: 0xa34000
After malloc, program break: 0xa34000
After malloc, program break: 0xa34000
After alloc, program break: 0xa34000
After free, program break: 0xa34000