[置顶] 浅谈双向链表的逆转以及用双向链表实现malloc/free/realloc

时间:2021-11-25 03:16:26

  双向链表因为在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;
}