1. 程式人生 > >淺談雙向連結串列的逆轉以及用雙向連結串列實現malloc/free/realloc

淺談雙向連結串列的逆轉以及用雙向連結串列實現malloc/free/realloc

  雙向連結串列因為在Linux核心廣泛使用,且能較好地考察對指標的理解,所以它的增刪、逆轉,以及如何用它來實現malloc/free/realloc等問題就成了技術型公司的偏好。

  本篇第一部分談雙向連結串列的建立、增、刪,不想閱讀的讀者可以直接跳過,第二部分談如何實現逆轉雙向連結串列,第三部分談如何malloc的常用實現,以及如何用雙向連結串列來實現

malloc函式。

一、初識雙向連結串列

1、1 常規雙向連結串列

1、2 linux雙向連結串列

二、逆轉雙向連結串列

三、用雙向連結串列來實現malloc/free/realloc等函式

四、記憶體分配的擴充套件

1、夥伴演算法

2、slab

3、glibc中malloc的實現

4、一種簡單malloc實現

  void *malloc (long numbytes):該函式負責分配 numbytes 大小的記憶體,並返回指向第一個位元組的指標。

  void free(void *firstbyte):如果給定一個由先前的 malloc 返回的指標,那麼該函式會將分配的空間歸還給程序的“空閒空間”。

  malloc_init 將是初始化記憶體分配程式的函式。有三個全域性變數,如下:

  int has_initialized = 0; //malloc_init是否被呼叫

  void *managed_memory_start;//管理記憶體的開始
  void *last_valid_address; //最後有效的記憶體地址


  void malloc_init()
  {
    /* grab the last valid address from the OS */
    last_valid_address = sbrk(0);
    /* we don't have any memory to manage yet, so just set the beginning to be last_valid_address */
    managed_memory_start = last_valid_address;
    /* Okay, we're initialized and ready to go */
    has_initialized = 1;
  }


  現在,為了完全地管理記憶體,我們需要能夠追蹤要分配和回收哪些記憶體。在對記憶體塊進行了 free 呼叫之後,我們需要做的是諸如將它們標記為未被使用的等事情,並且,在呼叫malloc時,我們要能夠定位未被使用的記憶體塊。因此,malloc 返回的每塊記憶體的起始處首先要有這個記憶體控制塊結構:

  struct mem_control_block 

  {
    int is_available;
    int size;
  };

 現在,您可能會認為當程式呼叫 malloc 時這會引發問題---它們如何知道這個結構?答案是它們不必知道;在返回指標之前,我們會將其移動到這個結構之後,把它隱藏起來。這使得返回的指標指向沒有用於任何其他用途的記憶體。那樣,從呼叫程式的角度來看,它們所得到的全部是空閒的、開放的記憶體。然後,當通過 free() 將該指標傳遞回來時,我們只需要倒退幾個記憶體位元組就可以再次找到這個結構。


  在討論分配記憶體之前,我們將先討論釋放,因為它更簡單。為了釋放記憶體,我們必須要做的惟一一件事情就是,獲得我們給出的指標,回退 sizeof(struct mem_control_block) 個位元組,並將其標記為可用的。這裡是對應的程式碼:


  void free(void *firstbyte)

  {
    struct mem_control_block *mcb;
    /* Backup from the given pointer to find the mem_control_block */
    mcb = firstbyte - sizeof(struct mem_control_block);
    /* Mark the block as being available */
    mcb->is_available = 1;
    /* That's It! We're done. */
    return;
  }


  如您所見,在這個分配程式中,記憶體的釋放使用了一個非常簡單的機制,在固定時間內完成記憶體釋放。分配記憶體稍微困難一些。以下是該演算法的略述:

1. If our allocator has not been initialized, initialize it.
2. Add sizeof(struct mem_control_block) to the size requested.
3. start at managed_memory_start.
4. Are we at last_valid address?
5. If we are:
   A. We didn't find any existing space that was large enough
      -- ask the operating system for more and return that.
6. Otherwise:
   A. Is the current space available (check is_available from
      the mem_control_block)?
   B. If it is:
      i)   Is it large enough (check "size" from the
           mem_control_block)?
      ii) If so:
           a. Mark it as unavailable
           b. Move past mem_control_block and return the
              pointer
      iii) Otherwise:
           a. Move forward "size" bytes
           b. Go back go step 4
   C. Otherwise:
      i)   Move forward "size" bytes
      ii) Go back to step 4

  void *malloc(long numbytes)

  {
    /* Holds where we are looking in memory */
    void *current_location;
    /* This is the same as current_location, but cast to a memory_control_block */
    struct mem_control_block *current_location_mcb;
    /* This is the memory location we will return. It will be set to 0 until we find something suitable */
    void *memory_location;
    /* Initialize if we haven't already done so */
    if(! has_initialized)

    {
      malloc_init();
    }


    numbytes = numbytes + sizeof(struct mem_control_block);
    /* Set memory_location to 0 until we find a suitable location */
    memory_location = 0;
    /* Begin searching at the start of managed memory */
    current_location = managed_memory_start;
    /* Keep going until we have searched all allocated space */
    while(current_location != last_valid_address)
    {
      current_location_mcb = (struct mem_control_block *)current_location;
      if(current_location_mcb->is_available)
      {
        if(current_location_mcb->size >= numbytes)
        {
          /* It is no longer available */
          current_location_mcb->is_available = 0;
          memory_location = current_location;
          /* Leave the loop */
          break;
        }
      }
      /* If we made it here, it's because the Current memory block not suitable; move to the next one */
      current_location = current_location +
      current_location_mcb->size;
    }
    /* If we still don't have a valid location, we'll have to ask the operating system for more memory */
    if(! memory_location)
    {
      /* Move the program break numbytes further */
      sbrk(numbytes);
      memory_location = last_valid_address;
      last_valid_address = last_valid_address + numbytes;
      /* We need to initialize the mem_control_block */
      current_location_mcb = memory_location;
      current_location_mcb->is_available = 0;
      current_location_mcb->size = numbytes;
    }
  
    /* Move the pointer past the mem_control_block */
    memory_location = memory_location + sizeof(struct mem_control_block);
    /* Return the pointer */
    return memory_location;
  }

  malloc() 的實現有很多,這些實現各有優點與缺點。在設計一個分配程式時,要面臨許多需要折衷的選擇,其中包括:

   分配的速度。 
   回收的速度。 
   有執行緒的環境的行為。 
   記憶體將要被用光時的行為。 
   小的或者大的物件。 
   實時保證。
   每一個實現都有其自身的優缺點集合。在這個簡單的分配程式中,分配非常慢而回收非常快。另外,由於它在使用虛擬記憶體系統方面較差,所以它最適於處理大的物件。

摘錄知識點:

brk和sbrk主要的工作是實現虛擬記憶體到記憶體的對映.在GNUC中,記憶體分配是這樣的:
  每個程序可訪問的虛擬記憶體空間為3G,但在程式編譯時,不可能也沒必要為程式分配這麼大的空間,只分配並不大的資料段空間,程式中動態分配的空間就是從這 一塊分配的。如果這塊空間不夠,malloc函式族(realloc,calloc等)就呼叫sbrk函式將資料段的下界移動,sbrk函式在核心的管理 下將虛擬地址空間對映到記憶體,供malloc函式使用。(參見linux核心情景分析)

#include <unistd.h>
       int brk(void *end_data_segment);
       void *sbrk(ptrdiff_t increment);
DESCRIPTION
       brk   sets   the   end   of   the   data   segment   to   the value specified by end_data_segment, when that value is reasonable, the system   does   have enough   memory   and   the process does not exceed its max data size (see setrlimit(2)).
       sbrk increments the program's data   space   by   increment   bytes.    sbrk isn't a system call, it is just a C library wrapper.   Calling sbrk with an increment of 0 can be used to find the current location of the   program break.
RETURN VALUE
       On   success,   brk returns zero, and sbrk returns a pointer to the start of the new area.   On error, -1 is returned, and errno is set to ENOMEM.

  sbrk不是系統呼叫,是C庫函式。系統呼叫通常提供一種最小功能,而庫函式通常提供比較複雜的功能。
  在Linux系統上,程式被載入記憶體時,核心為使用者程序地址空間建立了程式碼段、資料段和堆疊段,在資料段與堆疊段之間的空閒區域用於動態記憶體分配。
  核心資料結構mm_struct中的成員變數start_code和end_code是程序程式碼段的起始和終止地址,start_data和 end_data是程序資料段的起始和終止地址,start_stack是程序堆疊段起始地址,start_brk是程序動態記憶體分配起始地址(堆的起始 地址),還有一個 brk(堆的當前最後地址),就是動態記憶體分配當前的終止地址。

  C語言的動態記憶體分配基本函式是malloc(),在Linux上的基本實現是通過核心的brk系統呼叫。brk()是一個非常簡單的系統呼叫,只是簡單地改變mm_struct結構的成員變數brk的值。
  mmap系統呼叫實現了更有用的動態記憶體分配功能,可以將一個磁碟檔案的全部或部分內容對映到使用者空間中,程序讀寫檔案的操作變成了讀寫記憶體的操作。在 linux/mm/mmap.c檔案的do_mmap_pgoff()函式,是mmap系統呼叫實現的核心。do_mmap_pgoff()的程式碼,只是 新建了一個vm_area_struct結構,並把file結構的引數賦值給其成員變數m_file,並沒有把檔案內容實際裝入記憶體。
  Linux記憶體管理的基本思想之一,是隻有在真正訪問一個地址的時候才建立這個地址的物理對映。

5、K&R C語言中關於malloc的實現

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

typedef int Align;

union header
{
    struct 
    {
        union header* next;
        size_t size; /* 注意:這裡的size以sizeof(Header)為單元 */
    }s;

    Align x;
};

typedef union header Header;

int g_malloc_init = 0;
static Header base;
static Header *freep = NULL;

void malloc_init(void)
{
    base.s.next = freep = &base;
    base.s.size = 0;
    g_malloc_init = 1;
}

static Header* morecore(size_t nunits);
void *k_r_malloc(size_t nbytes)
{
    Header *prevp, *p;
    size_t nunits;

	/* 申請的記憶體節點佔幾個Header的大小,注意包含了Header */
    nunits = (nbytes + sizeof(Header) - 1) / sizeof(Header) + 1;

    if (!g_malloc_init)
    {
        malloc_init();
    }
   
    prevp = freep;
    for (p = prevp->s.next; ; prevp = p, p = p->s.next)
    {
		/* 如果找到了滿足要求的結點,注意這個結點的size可能大於或等於nunits */
        if (p->s.size >= nunits)
        {
            if (p->s.size == nunits) /* 恰好等於 */
            {
                prevp->s.next = p->s.next;
            }
            else /* 大於則分為兩部分,後一部分組成申請的結點,前一部分為剩餘大小構成的結點 */
            {
                p->s.size -= nunits;
                p += p->s.size;
                p->s.size = nunits;
            }

			/* 每次找到一個合適的空閒節點,都把這個節點的前一個節點複製到freep */
            freep = prevp;
            return (void*)p + 1;
        }
      
		/* 如果查詢到了迴圈起點freep */
        if (p == freep)
        {
			/* 沒有滿足要求的空閒結點(太小或沒有空閒結點), 則向作業系統申請更大的空間,在morecore會呼叫k_r_free函式把該空間加入到空閒結點連結串列中 */
             p = morecore(nunits);
             if (!p)
             {
                 return NULL;
             }
        }

		/* 如果這次沒有找到,則繼續往後找 */
    }
}


void k_r_free(void *ap)
{
    Header *bp, *p;

    bp = (Header*)ap - 1;

    /* bp插在空閒連結串列中的位置有三處,一是在空閒連結串列的結點之間,一是在空閒連結串列最後一個結點之後,一是在空閒連結串列最前一個結點之前 */
    for (p = freep; !(bp > p && bp < p->s.next); p = p->s.next)
    {
        if (p->s.next <= p && (bp > p || bp < p->s.next))
        {
                break;
        }
    }

    /* 將bp加入到空閒連結串列中,如果有左右相鄰的空閒結點,先合併右邊結點,再合併左邊結點 */
    if (bp + bp->s.size == p->s.next)
    {
        bp->s.size += p->s.size;
        bp->s.next = p->s.next->s.next;
    }
    else
    {
        bp->s.next = p->s.next;
    }

    if (p + p->s.size == bp)
    {
        p->s.size += bp->s.size;
        p->s.next = bp->s.next;
    }
    else
    {
        p->s.next = bp;
    }

    freep = p;
}

static Header* morecore(size_t nunits)
{
    char *cp;
    Header *up;

    if (nunits < 1024)
    {
        nunits = 1024;
    }

    /* sbrk失敗返回-1,返回值是申請空間之前的break點。*/
    cp = sbrk(nunits * sizeof(Header));
    if (cp == (char*)-1)
    {
        return NULL;
    }

    up = (Header*)cp;
    up->s.size = nunits;
    k_r_free((void*)(up + 1));

    return freep;
}

int main(void)
{
    void *p;
	
    p = k_r_malloc(20);
	printf("p = 0x%08x\n", (unsigned int)p);
    k_r_free(p);

    p = k_r_malloc(30);
	printf("p = 0x%08x\n", (unsigned int)p);
    k_r_free(p);

    p = k_r_malloc(40);
	printf("p = 0x%08x\n", (unsigned int)p);
    k_r_free(p);

    return 0;
}