s081[2]-unix內存分配方式-malloc實現

內存分配

前序課程

操做系統接口:dreamerjonson.com/2020/編程

系統編程(Systems programming)

wiki參考數組

  • 與應用程序編程相比,系統編程的主要區別在於,應用程序編程旨在產生直接向用戶提供服務的軟件。數據結構

  • 系統編程主要爲其餘應用程序提供服務,直接操做操做系統。它的目標是實現對可用資源的有效利用。併發

例如:app

  • unix utilitiesless

  • K/V serversssh

  • sshide

  • bigint library函數

挑戰:性能

  • 低級編程環境

  • 數字是64位(不是無限的整數)

  • 分配/釋放內存

  • 併發

    • 容許並行處理請求


  • 崩潰

  • 性能

  • 爲應用程序提供硬件支持

動態內存分配是一項基本的系統服務

  • 底層編程: 指針操做,類型轉換

  • 使用底層操做系統請求內存塊(memory chunks)

  • 性能很是重要

  • 支持普遍類型的應用程序負載

應用程序結構

  • text

  • data (靜態存儲)

  • stack (棧存儲)

  • heap (動態內存分配)
    使用 sbrk() o或 mmap() 操做系統接口擴展堆。
    [text | data | heap -> … <- stack]
    0 top of address space

  • data段的內存分配是靜態的,始終存在。

  • 棧的分配在函數中,隨着函數消亡而釋放。

  • 堆的分配與釋放,調用接口:
    — malloc(int sz)
    — free(p)

堆分配的目標

  • 快速分配和釋放

  • 內存開銷小

  • 想要使用全部內存

  • 避免碎片化

下面介紹幾種malloc實現的方式

方式1:K&R malloc

又叫作first-fit規則, 即查找第一個可用的匹配塊。與之相對應的是查找第一個最符合(best-fit)的可用塊。
K&R malloc的實現來自書籍 the C programming language by Kernighan and Ritchie (K&R) Section 8.7

維持一個鏈表

維持的free list是一個環。 第一個元素是base。

#define NALLOC  1024  /* minimum #units to request */

struct header {
  struct header *ptr;
  size_t size;
};

typedef struct header Header;

static Header base;
static Header *freep = NULL;

內存分配

指定分配的內存大小爲sizeof(Header)的倍數,且必定大於nbytes。nunits就是這個倍數。

  • 循環free list。 找到第一個符合即大於等於nbytes的塊。
    — 若是恰好合適,則鏈表刪除此塊並返回此塊。
    — 若是大於,則截斷此元素
    — 若是沒有找到合適的塊,則調用moreheap新分配一個。

/* malloc: general-purpose storage allocator */
void *
kr_malloc(size_t nbytes)
{
  Header *p, *prevp;
  unsigned nunits;

  nunits = (nbytes + sizeof(Header) - 1) / sizeof(Header) + 1;
  // base做爲第一個元素。
  if ((prevp = freep) == NULL) {    /* no free list yet */
    base.ptr = freep = prevp = &base;
    base.size = 0;
  }

  for (p = prevp->ptr; ; prevp = p, p = p->ptr) {
    if (p->size >= nunits) {    /* big enough */
      if (p->size == nunits)    /* exactly */
    prevp->ptr = p->ptr;
      else {    /* allocate tail end */
    p->size -= nunits;
    p += p->size;
    p->size = nunits;
      }
      freep = prevp;
      return (void *) (p + 1);
    }
    if (p == freep) {    /* wrapped around free list */
      if ((p = (Header *) moreheap(nunits)) == NULL) {
    return NULL;    /* none left */
      }
    }
  }
}

sbrk

sbrk是unix增長內存的操做系統調用。
wiki的解釋是:

The brk and sbrk calls dynamically change the amount of space allocated for the data segment of the calling process. The change is made by resetting the program break of the process, which determines the maximum space that can be allocated. The program break is the address of the first location beyond the current end of the data region. The amount of available space increases as the break value increases. The available space is initialized to a value of zero, unless the break is lowered and then increased, as it may reuse the same pages in some unspecified way. The break value can be automatically rounded up to a size appropriate for the memory management architecture.[4]
Upon successful completion, the brk subroutine returns a value of 0, and the sbrk subroutine returns the prior value of the program break (if the available space is increased then this prior value also points to the start of the new area). If either subroutine is unsuccessful, a value of −1 is returned and the errno global variable is set to indicate the error.

因爲這裏是增長內存,sbrk成功時會返回新增長區域的開始地址,若是失敗則會返回-1。
新生成一個塊以後,調用free函數將其加入到freelist當中。
注意這裏的up+1 是什麼意思。其表明的是新分配的區域的開頭。覺得新分配的區域以前有Header大小,用於標識大小和下一個區域。

static Header *moreheap(size_t nu)
{
  char *cp;
  Header *up;

  if (nu < NALLOC)
    nu = NALLOC;
  cp = sbrk(nu * sizeof(Header));
  if (cp == (char *) -1)
    return NULL;
  up = (Header *) cp;
  up->size = nu;
  kr_free((void *)(up + 1));
  return freep;
}

free

ap是要釋放的區域,其前面還有Header大小。bp 指向了塊的開頭。

  • 遍歷free list,找到要插入的中間區域。

  • 若是先後的區域正好是連在一塊兒的,則進行合併。

/* free: put block ap in free list */
void
kr_free(void *ap)
{
  Header *bp, *p;

  if (ap == NULL)
    return;

  bp = (Header *) ap - 1;    /* point to block header */
  for (p = freep; !(bp > p && bp < p->ptr); p = p->ptr)
    if (p >= p->ptr && (bp > p || bp < p->ptr))
      break;    /* freed block at start or end of arena */

  if (bp + bp->sizfe == p->ptr) {    /* join to upper nbr */
    bp->size += p->ptr->size;
    bp->ptr = p->ptr->ptr;
  } else {
    bp->ptr = p->ptr;
  }

  if (p + p->size == bp) {    /* join to lower nbr */
    p->size += bp->size;
    p->ptr = bp->ptr;
  } else {
    p->ptr = bp;
  }

  freep = p;
}

方式2:Region-based allocator, a special-purpose allocator.

  • malloc 與free 快速

  • 內存開銷低

  • 內存碎片嚴重

  • 不通用,用於特定應用程序。

struct region {
  void *start;
  void *cur;
  void *end;
};
typedef struct region Region;

static Region rg_base;

Region *
rg_create(size_t nbytes)
{
  rg_base.start = sbrk(nbytes);
  rg_base.cur = rg_base.start;
  rg_base.end = rg_base.start + nbytes;
  return &rg_base;
}

void *
rg_malloc(Region *r, size_t nbytes)
{
  assert (r->cur + nbytes <= r->end);
  void *p = r->cur;
  r->cur += nbytes;
  return p;
}

// free all memory in region resetting cur to start
void
rg_free(Region *r) {
  r->cur = r->start;
}

方式3:Buddy allocator

我以爲wiki的解釋挺好的:
wiki解析
提示:

  • 對於2^k 大小的空間,咱們能夠將其分割爲大小爲2^0, 2^1, 2^2, … 2^k的多種可能。

  • malloc(17) 會分配 32 bytes,所以其會必定程度上浪費空間。

  • 數據結構帶來的格外內存開銷

  • malloc 和free快速。

下面介紹一種代碼實現。

基本參數

  • ROUNDUP(n,sz) 求出要分配n哥字節時,但願分配的實際內存是大於等於n 而且是sz的倍數。

#define LEAF_SIZE     16 // The smallest allocation size (in bytes)
#define NSIZES        15 // Number of entries in bd_sizes array
#define MAXSIZE       (NSIZES-1) // Largest index in bd_sizes array
#define BLK_SIZE(k)   ((1L << (k)) * LEAF_SIZE) // Size in bytes for size k
#define HEAP_SIZE     BLK_SIZE(MAXSIZE)
#define NBLK(k)       (1 << (MAXSIZE-k))  // Number of block at size k
#define ROUNDUP(n,sz) (((((n)-1)/(sz))+1)*(sz))  // Round up to the next multiple of sz
  • NBLK求出在第k位置有多少塊。

每個大小k維護一個sz_infosz_info中都有一個free list, alloc 是一個char數組用於記錄塊是否分配。split 是一個char數組用於塊是否割裂。
這裏要注意,使用的是bit數組來記錄。 char有8位,如第n位表明的是當前k大小的第5個區塊。

// The allocator has sz_info for each size k. Each sz_info has a free
// list, an array alloc to keep track which blocks have been
// allocated, and an split array to to keep track which blocks have
// been split.  The arrays are of type char (which is 1 byte), but the
// allocator uses 1 bit per block (thus, one char records the info of
// 8 blocks).
struct sz_info {
  struct bd_list free;
  char *alloc;
  char *split;
};
typedef struct sz_info Sz_info;

每個級別的雙鏈表 for free list

// A double-linked list for the free list of each level
struct bd_list {
  struct bd_list *next;
  struct bd_list *prev;
};

bit數組操做

// Return 1 if bit at position index in array is set to 1
int bit_isset(char *array, int index) {
  char b = array[index/8];
  char m = (1 << (index % 8));
  return (b & m) == m;
}

// Set bit at position index in array to 1
void bit_set(char *array, int index) {
  char b = array[index/8];
  char m = (1 << (index % 8));
  array[index/8] = (b | m);
}

// Clear bit at position index in array
void bit_clear(char *array, int index) {
  char b = array[index/8];
  char m = (1 << (index % 8));
  array[index/8] = (b & ~m);
}

初始化

首先調用mmap分配一個很是大的區域。此大小爲2 ^ MAXSIZE * 16,16爲分配的最小塊。

// Allocate memory for the heap managed by the allocator, and allocate
// memory for the data structures of the allocator.
void
bd_init() {
  bd_base = mmap(NULL, HEAP_SIZE, PROT_READ | PROT_WRITE,
         MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (bd_base == MAP_FAILED) {
    fprintf(stderr, "couldn't map heap; %s\n", strerror(errno));
    assert(bd_base);
  }
  // printf("bd: heap size %d\n", HEAP_SIZE);
  for (int k = 0; k < NSIZES; k++) {
    lst_init(&bd_sizes[k].free);
    int sz = sizeof(char)*ROUNDUP(NBLK(k), 8)/8;
    bd_sizes[k].alloc = malloc(sz);
    memset(bd_sizes[k].alloc, 0, sz);
  }
  for (int k = 1; k < NSIZES; k++) {
    int sz = sizeof(char)*ROUNDUP(NBLK(k), 8)/8;
    bd_sizes[k].split = malloc(sz);
    memset(bd_sizes[k].split, 0, sz);
  }
  lst_push(&bd_sizes[MAXSIZE].free, bd_base);
}

找到第一個k, 使得2^k >= n

// What is the first k such that 2^k >= n?
int
firstk(size_t n) {
  int k = 0;
  size_t size = LEAF_SIZE;

  while (size < n) {
    k++;
    size *= 2;
  }
  return k;
}

查找位置p , 若是以2^k 大小爲區塊,那麼其位於第幾個區塊。

// Compute the block index for address p at size k
int
blk_index(int k, char *p) {
  int n = p - (char *) bd_base;
  return n / BLK_SIZE(k);
}

將k大小,序號爲bi的區塊的首地址計算出來

// Convert a block index at size k back into an address
void *addr(int k, int bi) {
  int n = bi * BLK_SIZE(k);
  return (char *) bd_base + n;
}

malloc

找到一個大小k,塊2^k是大於等於要分配的大小。
若是db_size[k].free 不爲空,說明當前有大小爲k的空閒空間。
若是沒有找到,則讓k+1,繼續找到更大的空間有無空閒。

若是找到,則lst_pop(&bd_sizes[k].free)獲取第一個塊。 blk_index(k, p)獲取以k爲衡量指標,p位於的第n個塊。 並經過bit_set將bd_sizes[k].alloc 在第n號位置設置爲1。
若是找到的k是比較大的空間,這時候須要對此空間進行分割,一分爲2。 一直到找到一個比較符合的空間。

void *
bd_malloc(size_t nbytes)
{
  int fk, k;

  assert(bd_base != NULL);

  // Find a free block >= nbytes, starting with smallest k possible
  fk = firstk(nbytes);
  for (k = fk; k < NSIZES; k++) {
    if(!lst_empty(&bd_sizes[k].free))
      break;
  }
  if(k >= NSIZES)  // No free blocks?
    return NULL;

  // Found one; pop it and potentially split it.
  char *p = lst_pop(&bd_sizes[k].free);
  bit_set(bd_sizes[k].alloc, blk_index(k, p));
  for(; k > fk; k--) {
    // 第2半的空間
    char *q = p + BLK_SIZE(k-1);
    // 對於大小k來講,其在位置p處是分割的。
    bit_set(bd_sizes[k].split, blk_index(k, p));
    // 對於大小k-1來講,其在位置p處是分配的。
    bit_set(bd_sizes[k-1].alloc, blk_index(k-1, p));
    // 對於大小k-1來講,其在位置q處是空閒的。
    lst_push(&bd_sizes[k-1].free, q);
  }
  // printf("malloc: %p size class %d\n", p, fk);
  return p;
}

free

當要free位置p時,size找到第一個k,當k+1在p位置是分割的,則返回k。這時候說明在k處區域是能夠合併的。這是一種優化。
// Find the size of the block that p points to. int size(char *p) { for (int k = 0; k < NSIZES; k++) { if(bit_isset(bd_sizes[k+1].split, blk_index(k+1, p))) { return k; } } return 0; }

void
bd_free(void *p) {
  void *q;
  int k;

  for (k = size(p); k < MAXSIZE; k++) {
    int bi = blk_index(k, p);
    bit_clear(bd_sizes[k].alloc, bi);
    int buddy = (bi % 2 == 0) ? bi+1 : bi-1;
    if (bit_isset(bd_sizes[k].alloc, buddy)) {
      break;
    }
    // budy is free; merge with buddy
    q = addr(k, buddy);
    lst_remove(q);
    if(buddy % 2 == 0) {
      p = q;
    }
    bit_clear(bd_sizes[k+1].split, blk_index(k+1, p));
  }

  // 放入freelist當中。
  // printf("free %p @ %d\n", p, k);
  lst_push(&bd_sizes[k].free, p);
}

環形雙鏈表的基本操做

// Implementation of lists: double-linked and circular. Double-linked
// makes remove fast. Circular simplifies code, because don't have to
// check for empty list in insert and remove.

void
lst_init(Bd_list *lst)
{
  lst->next = lst;
  lst->prev = lst;
}

int
lst_empty(Bd_list *lst) {
  return lst->next == lst;
}

void
lst_remove(Bd_list *e) {
  e->prev->next = e->next;
  e->next->prev = e->prev;
}

void*
lst_pop(Bd_list *lst) {
  assert(lst->next != lst);
  Bd_list *p = lst->next;
  lst_remove(p);
  return (void *)p;
}

void
lst_push(Bd_list *lst, void *p)
{
  Bd_list *e = (Bd_list *) p;
  e->next = lst->next;
  e->prev = lst;
  lst->next->prev = p;
  lst->next = e;
}

void
lst_print(Bd_list *lst)
{
  for (Bd_list *p = lst->next; p != lst; p = p->next) {
    printf(" %p", p);
  }
  printf("\n");
}

其餘順序分配方式

* dlmalloc
* slab allocator

其餘目標

  • 內存開銷小

  • 例如buddy的元數據很大

  • 良好的內存位置

  • cpu核心增長時,擴展性好

  • 併發malloc / free

參考資料

源碼
講義

相關文章
相關標籤/搜索