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

內存分配

前序課程

操作系統接口:dreamerjonson.com/2020/

系統編程(Systems programming)

wiki參考

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

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

例如:

  • unix utilities

  • K/V servers

  • ssh

  • 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

參考資料

源碼
講義


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章