从K&R书中解释malloc的这种实现

Sex*_*ast 31 c malloc pointers memory-management

这是Kernighan和Ritchie关于C的书的摘录.它显示了如何实现一个版本malloc.虽然评论很好,但我很难理解它.有人可以解释一下吗?

typedef long Align; /* for alignment to long boundary */
union header { /* block header */
struct {
union header *ptr; /* next block if on free list */
unsigned size; /* size of this block */
} s;
Align x; /* force alignment of blocks */
};
typedef union header Header;

static Header base; /* empty list to get started */
static Header *freep = NULL; /* start of free list */
/* malloc: general-purpose storage allocator */
void *malloc(unsigned nbytes)
{
   Header *p, *prevp;
   Header *morecore(unsigned);
   unsigned nunits;
   nunits = (nbytes+sizeof(Header)-1)/sizeof(header) + 1;
   if ((prevp = freep) == NULL) { /* no free list yet */
      base.s.ptr = freeptr = prevptr = &base;
      base.s.size = 0;
   }
   for (p = prevp->s.ptr; ; prevp = p, p = p->s.ptr) {
      if (p->s.size >= nunits) { /* big enough */
        if (p->s.size == nunits) /* exactly */
           prevp->s.ptr = p->s.ptr;
        else { /* allocate tail end */
           p->s.size -= nunits;
           p += p->s.size;
           p->s.size = nunits
             }
        freep = prevp;
        return (void *)(p+1);
      }
      if (p == freep) /* wrapped around free list */
         if ((p = morecore(nunits)) == NULL)
             return NULL; /* none left */
      }
}

#define NALLOC 1024 /* minimum #units to request */
/* morecore: ask system for more memory */

static Header *morecore(unsigned nu)
{

  char *cp, *sbrk(int);
  Header *up;

  if (nu < NALLOC)
    nu = NALLOC;

  cp = sbrk(nu * sizeof(Header));

  if (cp == (char *) -1) /* no space at all */
    return NULL;

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

  return freep;
}

/* free: put block ap in free list */
void free(void *ap) {
  Header *bp, *p;
  bp = (Header *)ap - 1; /* point to block header */
  for (p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
    if (p >= p->s.ptr && (bp > p || bp < p->s.ptr))
      break; /* freed block at start or end of arena */
  if (bp + bp->size == p->s.ptr) {
    bp->s.size += p->s.ptr->s.size;
    bp->s.ptr = p->s.ptr->s.ptr;
  } else
      bp->s.ptr = p->s.ptr;

  if (p + p->size == bp) {
    p->s.size += bp->s.size;
    p->s.ptr = bp->s.ptr;
  } else
    p->s.ptr = bp;
  freep = p;
}
Run Code Online (Sandbox Code Playgroud)

Lun*_*din 58

好吧,我们这里有一大堆写得很糟糕的代码.我将在这篇文章中做的最好被描述为软件考古学.

第1步:修复格式.

压痕和紧凑格式对任何人都没有好处.需要插入各种空格和空行.评论可以用更可读的方式编写.我将首先解决这个问题.

与此同时,我正在改变K&R风格的支架式 - 请注意K&R支架式是可以接受的,这仅仅是我个人的偏好.另一个个人偏好是在指向的类型旁边写指针.我不会在这里争论(主观)风格问题.

此外,类型定义Header是完全不可读的,它需要一个彻底的修复.

我发现了一些完全模糊不清的东西:它们似乎在函数内声明了一个函数原型.Header* morecore(unsigned);.这是非常古老而且非常糟糕的风格,我不确定C是否会再允许它.让我们删除该行,无论该函数做什么,都必须在别处定义.

typedef long Align;                      /* for alignment to long boundary */

typedef union header                     /* block header */
{
  struct
  {
    union header *ptr;                   /* next block if on free list */
    unsigned size;                       /* size of this block */
  } s;

  Align x;                               /* force alignment of blocks */

} Header;


static Header base;                      /* empty list to get started */
static Header* freep = NULL;             /* start of free list */


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

  nunits = (nbytes + sizeof(Header) - 1) / sizeof(header) + 1;

  if ((prevp = freep) == NULL)           /* no free list yet */
  {
    base.s.ptr = freeptr = prevptr = &base;
    base.s.size = 0;
  }

  for (p = prevp->s.ptr; ; prevp = p, p = p->s.ptr)
  {
    if (p->s.size >= nunits)             /* big enough */
    {
      if (p->s.size == nunits)           /* exactly */
        prevp->s.ptr = p->s.ptr;
      else                               /* allocate tail end */
      {
        p->s.size -= nunits;
        p += p->s.size;
        p->s.size = nunits
      }

      freep = prevp;
      return (void *)(p+1);
    }

    if (p == freep)                      /* wrapped around free list */
      if ((p = morecore(nunits)) == NULL)
        return NULL;                     /* none left */
  }
}
Run Code Online (Sandbox Code Playgroud)

好的,现在我们实际上可以读取代码了.

第二步:清除广泛认可的不良做法.

这段代码充满了现在被认为是不好的做法.它们需要被删除,因为它们会危害代码的安全性,可读性和维护性.如果您想要提及与我一样宣传相同做法的权威机构,请查看广泛认可的编码标准MISRA-C.

我发现并删除了以下不良做法:

1)只输入unsigned代码可能会导致混淆:这是程序员的错字还是打算写unsigned int?我们应该更换所有unsignedunsigned int.但是当我们这样做时,我们发现它在这个上下文中用于给出各种二进制数据的大小.用于此类事项的正确类型是C标准类型size_t.这基本上只是一个unsigned int,但它保证对于特定平台来说"足够大".该sizeof运算符返回类型的结果size_t,如果我们看一下真正的malloc的C标准的定义,它是void *malloc(size_t size);.这size_t是最正确的使用类型.

2)对于我们自己的malloc函数使用与stdlib.h中相同的名称是一个坏主意.如果我们需要包含stdlib.h,事情会变得混乱.根据经验,永远不要在自己的代码中使用C标准库函数的标识符名称.我将名称更改为kr_malloc.

3)代码滥用了所有静态变量保证初始化为零的事实.这是由C标准明确定义的,但却是一个相当微妙的规则.让我们明确初始化所有静态,以表明我们没有忘记偶然启动它们.

4)内部条件的分配是危险的,难以阅读.如果可能,应该避免这种情况,因为它也可能导致错误,例如classic = vs == bug.

5)由于评估的顺序,同一行上的多个分配难以阅读,也可能是危险的.

6)同一行上的多个声明很难阅读,也很危险,因为混合数据和指针声明时可能会导致错误.始终在每行上声明每个变量.

7)每次声明后都要使用大括号.不这样做会导致bug错误.

8)永远不要将特定指针类型的强制类型转换为void*.它在C中是不必要的,并且可以隐藏编译器本来会检测到的错误.

9)避免在函数内使用多个return语句.有时他们会导致更清晰的代码,但在大多数情况下,他们会导致意大利面.正如代码所代表的那样,我们不能在不重写循环的情况下改变它,所以我稍后会修复它.

10)保持循环简单.它们应该包含一个init语句,一个循环条件和一个迭代,没有别的.这个for循环,使用逗号运算符和所有内容,非常模糊.同样,我们发现需要将此循环重写为理智的东西.我接下来会这样做,但现在我们有:

typedef long Align;                      /* for alignment to long boundary */

typedef union header                     /* block header */
{
  struct
  {
    union header *ptr;                   /* next block if on free list */
    size_t size;                         /* size of this block */
  } s;

  Align x;                               /* force alignment of blocks */

} Header;


static Header base = {0};                /* empty list to get started */
static Header* freep = NULL;             /* start of free list */


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

  nunits = (nbytes + sizeof(Header) - 1) / sizeof(header) + 1;

  prevp = freep;
  if (prevp == NULL)                     /* no free list yet */
  {
    base.s.ptr  = &base;
    freeptr     = &base;
    prevptr     = &base;
    base.s.size = 0;
  }

  for (p = prevp->s.ptr; ; prevp = p, p = p->s.ptr)
  {
    if (p->s.size >= nunits)             /* big enough */
    {
      if (p->s.size == nunits)           /* exactly */
      {
        prevp->s.ptr = p->s.ptr;
      }
      else                               /* allocate tail end */
      {
        p->s.size -= nunits;
        p += p->s.size;
        p->s.size = nunits
      }

      freep = prevp;
      return p+1;
    }

    if (p == freep)                      /* wrapped around free list */
    {
      p = morecore(nunits);
      if (p == NULL)
      {
        return NULL;                     /* none left */
      }
    }
  } /* for */
}
Run Code Online (Sandbox Code Playgroud)

第3步:重写模糊循环.

由于前面提到的原因.我们可以看到这个循环永远持续,它通过从函数返回来终止,无论是在分配完成时,还是在没有内存的情况下.因此,我们将其创建为循环条件,然后将返回到函数末尾的位置.让我们摆脱那个丑陋的逗号运算符.

我将介绍两个新变量:一个用于保存结果指针的结果变量,另一个用于跟踪循环是否应该继续.我将通过使用bool类型来吹嘘K&R的思想,这是自1999年以来C语言的一部分.

(我希望我没有用这个改变改变算法,我相信我没有)

#include <stdbool.h>

typedef long Align;                      /* for alignment to long boundary */

typedef union header                     /* block header */
{
  struct
  {
    union header *ptr;                   /* next block if on free list */
    size_t size;                         /* size of this block */
  } s;

  Align x;                               /* force alignment of blocks */

} Header;


static Header base = {0};                /* empty list to get started */
static Header* freep = NULL;             /* start of free list */


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

  nunits = (nbytes + sizeof(Header) - 1) / sizeof(header) + 1;

  prevp = freep;
  if (prevp == NULL)                     /* no free list yet */
  {
    base.s.ptr  = &base;
    freeptr     = &base;
    prevptr     = &base;
    base.s.size = 0;
  }

  is_allocating = true;
  for (p = prevp->s.ptr; is_allocating; p = p->s.ptr)
  {
    if (p->s.size >= nunits)             /* big enough */
    {
      if (p->s.size == nunits)           /* exactly */
      {
        prevp->s.ptr = p->s.ptr;
      }
      else                               /* allocate tail end */
      {
        p->s.size -= nunits;
        p += p->s.size;
        p->s.size = nunits
      }

      freep = prevp;
      result = p+1;
      is_allocating = false;             /* we are done */
    }

    if (p == freep)                      /* wrapped around free list */
    {
      p = morecore(nunits);
      if (p == NULL)
      {
        result = NULL;                   /* none left */
        is_allocating = false;
      }
    }
    prevp = p;
  } /* for */

  return result;
}
Run Code Online (Sandbox Code Playgroud)

第4步:编译这个垃圾.

由于这是来自K&R,它充满了拼写错误.sizeof(header)应该是sizeof(Header).缺少分号.他们使用不同的名称freep,prevp与freeptr,prevptr,但显然意味着相同的变量.我相信后者实际上是更好的名字,所以让我们使用它们.

#include <stdbool.h>

typedef long Align;                      /* for alignment to long boundary */

typedef union header                     /* block header */
{
  struct
  {
    union header *ptr;                   /* next block if on free list */
    size_t size;                         /* size of this block */
  } s;

  Align x;                               /* force alignment of blocks */

} Header;


static Header base = {0};                /* empty list to get started */
static Header* freeptr = NULL;           /* start of free list */


/* malloc: general-purpose storage allocator */
void* kr_malloc (size_t nbytes)
{
  Header*  p;
  Header*  prevptr;
  size_t   nunits;
  void*    result;
  bool     is_allocating;

  nunits = (nbytes + sizeof(Header) - 1) / sizeof(Header) + 1;

  prevptr = freeptr;
  if (prevptr == NULL)                   /* no free list yet */
  {
    base.s.ptr  = &base;
    freeptr     = &base;
    prevptr     = &base;
    base.s.size = 0;
  }

  is_allocating = true;
  for (p = prevptr->s.ptr; is_allocating; p = p->s.ptr)
  {
    if (p->s.size >= nunits)             /* big enough */
    {
      if (p->s.size == nunits)           /* exactly */
      {
        prevptr->s.ptr = p->s.ptr;
      }
      else                               /* allocate tail end */
      {
        p->s.size -= nunits;
        p += p->s.size;
        p->s.size = nunits;
      }

      freeptr = prevptr;
      result = p+1;
      is_allocating = false;             /* we are done */
    }

    if (p == freeptr)                    /* wrapped around free list */
    {
      p = morecore(nunits);
      if (p == NULL)
      {
        result = NULL;                   /* none left */
        is_allocating = false;
      }
    }
    prevptr = p;
  } /* for */

  return result;
}
Run Code Online (Sandbox Code Playgroud)

现在我们有一些可读的,可维护的代码,没有很多危险的做法,甚至可以编译!所以现在我们真的可以开始思考代码实际在做什么了.

正如您可能已经猜到的那样,结构"标题"是链接列表中节点的声明.每个这样的节点都包含指向下一个节点的指针.我不太了解morecore函数,也没有"环绕",我从来没有使用过这个函数,也没有sbrk.但我假设它分配了这个结构中指定的头,以及该头后面的一些原始数据块.如果是这样,那就解释了为什么没有实际的数据指针:假设数据跟在标题之后,在内存中相邻.因此,对于每个节点,我们得到标头,然后我们在标头后面获得一大块原始数据.

迭代本身非常简单,它们通过单链表,一次一个节点.

在循环结束时,他们将指针设置为超过"块"结尾的第一点,然后将其存储在静态变量中,以便程序在下次调用函数时将记住它先前分配内存的位置.

他们正在使用一种技巧使其标题最终位于对齐的内存地址上:它们将所有开销信息存储在一个联合中,并且变量足够大,以对应平台的对齐要求.因此,如果"ptr"的大小加上"size"的大小太小而无法给出精确对齐,则union保证至少分配sizeof(Align)字节.我相信今天整个技巧已经过时,因为C标准要求自动结构/联合填充.

  • @Rob你还使用25年以上的编译器吗?25岁以上的操作系统?在一台25岁以上的电脑上?如果你只是环顾四周,对这本书有很多完全有效的批评.如果你打算投票给我,只是因为我告诉你太阳是太阳系的中心,而不是地球,至少为你认为我错的原因提供了一些理由.我只是想听听你为什么原始代码非常好的逻辑推理.它甚至会强迫你对这本书发表自己的看法,而不是遵循方便的行列. (26认同)
  • 在我25年多的编码中,这是我第一次听到K&R称之为"令人难以置信的大肆宣传"并且存在缺陷. (17认同)
  • 你提到的大多数不良做法都不是,它们是语言特征.我有点同意#1; #2是无关紧要的,剩下的就是风格问题. (14认同)
  • 我们从来没有解释代码实际如何工作 (13认同)
  • @Cupidvogel:当信息完全是主观的时,将信息传播为事实对我来说是一个很好的理由. (6认同)
  • @Lundin:这是主观的,因为有些人像我一样思考,并且有人像你一样思考.最后,社会上没有明确的答案或共识,无论是否可以接受.仅仅因为一些随机的人或协会说它的好坏意味着它.你的答案肯定给人的印象是事实并非如此.就这些. (6认同)
  • @Lundin这是一个非常微弱的答案.我很惊讶它甚至被允许上升,因为你告诉提问者"解决"的大部分内容都是完全主观和基于风格的.他不适合你,所以你为什么要告诉他"怎么"写循环,while循环,以及使用哪些运算符?这甚至不是codereview.stackexchange.com哪里更合适.我不能对拼写错误发表评论,因为我所拥有的这本书的版本现在已经修复了所有这些,所以我不能在那里争论.事实上,问题是:"解释这段代码"不是"告诉我这里有什么问题." 那有意义吗? (5认同)
  • @the_endian公平地说,这个答案是在问题从Code Review迁移到Stack Overflow之前编写的.在Code Review上,所有问题都隐含着"如何改进此代码?" 题.问题应该是关闭而不是迁移,因为它不是关于特定的编程问题,而是对大量代码的分析. (5认同)
  • 如果你环顾四周,你可以发现任何批评,但这不是你要问的任何事情的地方. (3认同)
  • 即使我不同意某些细节,你也提出了一些有用的建议.我会说如果写得很清楚并且没有过度,那么内部条件的分配并不总是很糟糕.我不同意,没有足够的括号会导致"bug bug bug" - 你忘记了你说"如果你不小心"的部分.此外,在函数内部声明原型可能很草率,但我觉得你的解雇是"老"和"模糊"很奇怪 - 把它放在头文件或其他函数之外可能更好但是它是我的东西用新代码看过,写过; 我昨天做到了. (2认同)
  • 他认为哪些主观主观是主观的?这本书确实充满了拼写错误,我可以证明这一点.即使像这样的20线也充满了错误.它可能是一本圣经书籍,因为它是同类中的第一本书,但这并不能证明它过量的错误是正当的.对于像我这样的C新手来说,它会增加足够的混乱来制造对C的永久厌恶,这绝对是不必要的. (2认同)
  • @Lundin,编程不是工程.这是一种手艺,至少对很多很多程序员来说都是如此.我有点希望它是一个工程,但编程是在某种维多利亚州... (2认同)
  • 除非您尝试为自动驾驶汽车或宇宙飞船编程,否则 MISRA C 是 100% 完全矫枉过正。不要试图将它应用到日常代码中。并且它不是禁止动态内存分配(即malloc)开始吗?所以这里正在实施的整个事情实际上违反了你的规则。 (2认同)
  • @MattiVirkkunen MISRA-C 文档的大部分内容是关于如何编写不依赖于指定不当行为的无错误代码。这就是该文档大约 90% 的内容。所以,是的,如果您正在编写一个鼓励错误并依赖于未定义行为的程序,那么就不要使用 MISRA-C。禁止动态分配只是另外 10% 中的一件事,它确实只适用于嵌入式系统。这里的关键是 MISRA-C 是公认的规范来源,与 SO 或 CR 上的随机人不同。 (2认同)

dpr*_*tch 31

我正在研究K&R,因为我想到OP在他问这个问题的时候,我来到这里是因为我也发现这些实现令人困惑.虽然接受的答案非常详细和有用,但我试图采用不同的方法来理解最初编写的代码 - 我已经完成了代码并在代码的部分添加了注释,这对我来说很难.这包括该部分中其他例程的代码(这些函数freememcore- 我已重命名它们kandr_mallockandr_free避免与stdlib冲突).我想我会把它留在这里作为接受答案的补充,对于其他可能觉得有用的学生.

我承认此代码中的注释过多.请知道我只是在做这个学习练习,我并不是说这是实际编写代码的好方法.

我冒昧地将一些变量名称改为对我来说更直观的变量名称; 除此之外,代码基本上保持不变.它似乎编译并运行我使用的测试程序,虽然valgrind有一些应用程序的投诉.

另外:评论中的一些文字是直接从K&R或手册页中提取的 - 我不打算对这些部分给予任何信任.

#include <unistd.h>  // sbrk

#define NALLOC 1024  // Number of block sizes to allocate on call to sbrk
#ifdef NULL
#undef NULL
#endif
#define NULL 0


// long is chosen as an instance of the most restrictive alignment type
typedef long Align;

/* Construct Header data structure.  To ensure that the storage returned by
 * kandr_malloc is aligned properly for the objects that are stored in it, all
 * blocks are multiples of the header size, and the header itself is aligned
 * properly.  This is achieved through the use of a union; this data type is big
 * enough to hold the "widest" member, and the alignment is appropriate for all
 * of the types in the union.  Thus by including a member of type Align, which
 * is an instance of the most restrictive type, we guarantee that the size of
 * Header is aligned to the worst-case boundary.  The Align field is never used;
 * it just forces each header to the desired alignment.
 */
union header {
  struct {
    union header *next;
    unsigned size;
  } s;

  Align x;
};
typedef union header Header;


static Header base;           // Used to get an initial member for free list
static Header *freep = NULL;  // Free list starting point


static Header *morecore(unsigned nblocks);
void kandr_free(void *ptr);




void *kandr_malloc(unsigned nbytes) {

  Header *currp;
  Header *prevp;
  unsigned nunits;

  /* Calculate the number of memory units needed to provide at least nbytes of
   * memory.
   *
   * Suppose that we need n >= 0 bytes and that the memory unit sizes are b > 0
   * bytes.  Then n / b (using integer division) yields one less than the number
   * of units needed to provide n bytes of memory, except in the case that n is
   * a multiple of b; then it provides exactly the number of units needed.  It
   * can be verified that (n - 1) / b provides one less than the number of units
   * needed to provide n bytes of memory for all values of n > 0.  Thus ((n - 1)
   * / b) + 1 provides exactly the number of units needed for n > 0.
   *
   * The extra sizeof(Header) in the numerator is to include the unit of memory
   * needed for the header itself.
   */
  nunits = ((nbytes + sizeof(Header) - 1) / sizeof(Header)) + 1;

  // case: no free list yet exists; we have to initialize.
  if (freep == NULL) {

    // Create degenerate free list; base points to itself and has size 0
    base.s.next = &base;
    base.s.size = 0;

    // Set free list starting point to base address
    freep = &base;
  }

  /* Initialize pointers to two consecutive blocks in the free list, which we
   * call prevp (the previous block) and currp (the current block)
   */
  prevp = freep;
  currp = prevp->s.next;

  /* Step through the free list looking for a block of memory large enough to
   * fit nunits units of memory into.  If the whole list is traversed without
   * finding such a block, then morecore is called to request more memory from
   * the OS.
   */
  for (; ; prevp = currp, currp = currp->s.next) {

    /* case: found a block of memory in free list large enough to fit nunits
     * units of memory into.  Partition block if necessary, remove it from the
     * free list, and return the address of the block (after moving past the
     * header).
     */
    if (currp->s.size >= nunits) {

      /* case: block is exactly the right size; remove the block from the free
       * list by pointing the previous block to the next block.
       */
      if (currp->s.size == nunits) {
    /* Note that this line wouldn't work as intended if we were down to only
     * 1 block.  However, we would never make it here in that scenario
     * because the block at &base has size 0 and thus the conditional will
     * fail (note that nunits is always >= 1).  It is true that if the block
     * at &base had combined with another block, then previous statement
     * wouldn't apply - but presumably since base is a global variable and
     * future blocks are allocated on the heap, we can be sure that they
     * won't border each other.
     */
    prevp->s.next = currp->s.next;
      }
      /* case: block is larger than the amount of memory asked for; allocate
       * tail end of the block to the user.
       */
      else {
    // Changes the memory stored at currp to reflect the reduced block size
    currp->s.size -= nunits;
    // Find location at which to create the block header for the new block
    currp += currp->s.size;
    // Store the block size in the new header
    currp->s.size = nunits;
      }

      /* Set global starting position to the previous pointer.  Next call to
       * malloc will start either at the remaining part of the partitioned block
       * if a partition occurred, or at the block after the selected block if
       * not.
       */
      freep = prevp;

      /* Return the location of the start of the memory, i.e. after adding one
       * so as to move past the header
       */
      return (void *) (currp + 1);

    } // end found a block of memory in free list case

    /* case: we've wrapped around the free list without finding a block large
     * enough to fit nunits units of memory into.  Call morecore to request that
     * at least nunits units of memory are allocated.
     */
    if (currp == freep) {
      /* morecore returns freep; the reason that we have to assign currp to it
       * again (since we just tested that they are equal), is that there is a
       * call to free inside of morecore that can potentially change the value
       * of freep.  Thus we reassign it so that we can be assured that the newly
       * added block is found before (currp == freep) again.
       */
      if ((currp = morecore(nunits)) == NULL) {
    return NULL;
      }
    } // end wrapped around free list case
  } // end step through free list looking for memory loop
}




static Header *morecore(unsigned nunits) {

  void *freemem;    // The address of the newly created memory
  Header *insertp;  // Header ptr for integer arithmatic and constructing header

  /* Obtaining memory from OS is a comparatively expensive operation, so obtain
   * at least NALLOC blocks of memory and partition as needed
   */
  if (nunits < NALLOC) {
    nunits = NALLOC;
  }

  /* Request that the OS increment the program's data space.  sbrk changes the
   * location of the program break, which defines the end of the process's data
   * segment (i.e., the program break is the first location after the end of the
   * uninitialized data segment).  Increasing the program break has the effect
   * of allocating memory to the process.  On success, brk returns the previous
   * break - so if the break was increased, then this value is a pointer to the
   * start of the newly allocated memory.
   */
  freemem = sbrk(nunits * sizeof(Header));
  // case: unable to allocate more memory; sbrk returns (void *) -1 on error
  if (freemem == (void *) -1) {
    return NULL;
  }

  // Construct new block
  insertp = (Header *) freemem;
  insertp->s.size = nunits;

  /* Insert block into the free list so that it is available for malloc.  Note
   * that we add 1 to the address, effectively moving to the first position
   * after the header data, since of course we want the block header to be
   * transparent for the user's interactions with malloc and free.
   */
  kandr_free((void *) (insertp + 1));

  /* Returns the start of the free list; recall that freep has been set to the
   * block immediately preceeding the newly allocated memory (by free).  Thus by
   * returning this value the calling function can immediately find the new
   * memory by following the pointer to the next block.
   */
  return freep;
}




void kandr_free(void *ptr) {

  Header *insertp, *currp;

  // Find address of block header for the data to be inserted
  insertp = ((Header *) ptr) - 1;

  /* Step through the free list looking for the position in the list to place
   * the insertion block.  In the typical circumstances this would be the block
   * immediately to the left of the insertion block; this is checked for by
   * finding a block that is to the left of the insertion block and such that
   * the following block in the list is to the right of the insertion block.
   * However this check doesn't check for one such case, and misses another.  We
   * still have to check for the cases where either the insertion block is
   * either to the left of every other block owned by malloc (the case that is
   * missed), or to the right of every block owned by malloc (the case not
   * checked for).  These last two cases are what is checked for by the
   * condition inside of the body of the loop.
   */
  for (currp = freep; !((currp < insertp) && (insertp < currp->s.next)); currp = currp->s.next) {

    /* currp >= currp->s.ptr implies that the current block is the rightmost
     * block in the free list.  Then if the insertion block is to the right of
     * that block, then it is the new rightmost block; conversely if it is to
     * the left of the block that currp points to (which is the current leftmost
     * block), then the insertion block is the new leftmost block.  Note that
     * this conditional handles the case where we only have 1 block in the free
     * list (this case is the reason that we need >= in the first test rather
     * than just >).
     */
    if ((currp >= currp->s.next) && ((currp < insertp) || (insertp < currp->s.next))) {
      break;
    }
  }

  /* Having found the correct location in the free list to place the insertion
   * block, now we have to (i) link it to the next block, and (ii) link the
   * previous block to it.  These are the tasks of the next two if/else pairs.
   */

  /* case: the end of the insertion block is adjacent to the beginning of
   * another block of data owned by malloc.  Absorb the block on the right into
   * the block on the left (i.e. the previously existing block is absorbed into
   * the insertion block).
   */
  if ((insertp + insertp->s.size) == currp->s.next) {
    insertp->s.size += currp->s.next->s.size;
    insertp->s.next = currp->s.next->s.next;
  }
  /* case: the insertion block is not left-adjacent to the beginning of another
   * block of data owned by malloc.  Set the insertion block member to point to
   * the next block in the list.
   */
  else {
    insertp->s.next = currp->s.next;
  }

  /* case: the end of another block of data owned by malloc is adjacent to the
   * beginning of the insertion block.  Absorb the block on the right into the
   * block on the left (i.e. the insertion block is absorbed into the preceeding
   * block).
   */
  if ((currp + currp->s.size) == insertp) {
    currp->s.size += insertp->s.size;
    currp->s.next = insertp->s.next;
  }
  /* case: the insertion block is not right-adjacent to the end of another block
   * of data owned by malloc.  Set the previous block in the list to point to
   * the insertion block.
   */
  else {
    currp->s.next = insertp;
  }

  /* Set the free pointer list to start the block previous to the insertion
   * block.  This makes sense because calls to malloc start their search for
   * memory at the next block after freep, and the insertion block has as good a
   * chance as any of containing a reasonable amount of memory since we've just
   * added some to it.  It also coincides with calls to morecore from
   * kandr_malloc because the next search in the iteration looks at exactly the
   * right memory block.
   */
  freep = currp;
}
Run Code Online (Sandbox Code Playgroud)

  • 好主,那是一个如此彻底而详细的答案!谢谢!我现在已经破产了,但是有一天我会变得富有(有SO学分),然后我会奖励你一个当之无愧的赏金.. :)虽然说得好,虽然评论很好,但我仍然遇到问题'Align`一词和它的作用,以及你对齐的意思.你能解释一下吗? (3认同)

use*_*818 9

malloc()的基础

在Linux中,有两种典型的请求内存的方法:sbrkmmap.这些系统调用对频繁的小分配有严格的限制.malloc()是一个解决此问题的库函数.它使用sbrk/mmap请求大块内存,并在大块内部返回小内存块.这比直接调用sbrk/mmap更有效,更灵活.

K&R malloc()

在K&R实现中,核心(通常称为竞技场)是一大块内存.morecore()从系统请求核心sbrk().当您多次调用malloc()/ free()时,将使用/分配核心中的某些块,而其他块则是空闲的.K&R malloc将空闲块的地址存储在循环单链表中.在此列表中,每个节点都是一个空闲内存块.第一个sizeof(Header)字节保持块的大小和指向下一个空闲块的指针.空闲块中的其余字节未初始化.与教科书中的典型列表不同,空闲列表中的节点只是指向核心中一些未使用区域的指针; 除了核心之外,您实际上并没有分配每个节点.该列表是理解算法的关键.

下图显示了具有两个核心/竞技场的示例内存布局.在图中,每个字符都占用sizeof(Header)字节.@是一个Header,+标记分配的内存并-标记内核中的空闲内存.在该示例中,有三个分配的块和三个空闲块.三个空闲块存储在循环列表中.对于三个已分配的块,仅存储它们的大小Header.

            This is core 1                             This is core 2

@---------@+++++++++@++++++++++++        @----------@+++++++++++++++++@------------
|                                        |                            |
p->ptr->ptr                              p = p->ptr->ptr->ptr         p->ptr
Run Code Online (Sandbox Code Playgroud)

在您的代码中,freep是自由列表的入口点.如果你反复跟随freep->ptr,你会回来freep- 它是循环的.一旦你理解了循环单链表,剩下的就相对简单了.malloc()找到一个空闲块并可能将其拆分.free()将一个空闲块添加回列表,并将其合并到相邻的空闲块.他们都试图维护列表的结构.

关于实施的其他意见

  • 代码注释提到"包裹"在malloc().当您遍历整个空闲列表但找不到大于请求长度的空闲块时,会发生该行.在这种情况下,您必须添加一个新的核心morecore().

  • base是一个零大小的块,始终包含在空闲列表中.避免特殊套管是一种技巧.这不是绝对必要的.

  • free()可能看起来有点复杂,因为它必须考虑四种不同的情况来将新释放的块合并到列表中的其他空闲块.除非您想自己重新实现,否则这个细节并不重要.

  • 这篇博文更详细地解释了K&R malloc.

PS: K&R malloc是我认为最优雅的代码之一.当我第一次理解代码时,真的很开眼界.令我感到难过的是,一些现代程序员,甚至不了解这种实现的基本原理,仅根据其编码风格调用了杰作.