Объясните эту реализацию malloc из книги K & R

Это отрывок из книги К на Кернигане и Ричи. Он показывает, как реализовать версию 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;
}

Ответ 1

Хорошо, что у нас есть кусок действительно плохо написанного кода. То, что я сделаю в этом посте, лучше всего можно описать как программную археологию.

Шаг 1: исправьте форматирование.

Отступ и компактный формат никому не помогают. Должны быть вставлены различные пробелы и пустые строки. Комментарии могут быть написаны более читабельными способами. Я начну с исправления этого.

В то же время я меняю стиль подставки из стиля K & R - обратите внимание, что стиль K & R brace является приемлемым, это просто личное предпочтение. Другим личным предпочтением является запись * для указателей рядом с указанным типом. Здесь я не буду спорить о (субъективном) стиле.

Кроме того, определение типа 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 */
  }
}

Хорошо, теперь мы сможем прочитать код.

Шаг 2: отсекаем широко признанную плохую практику.

Этот код заполнен вещами, которые в настоящее время считаются плохой практикой. Их нужно удалить, поскольку они ставят под угрозу безопасность, удобочитаемость и обслуживание кода. Если вам нужна ссылка на авторитет, проповедующий те же самые практики, что и я, ознакомьтесь с широко признанным стандартом кодирования MISRA-C.

Я заметил и устранил следующие плохие практики:

1) Просто ввод текста unsigned в коде может привести к путанице: было ли это опечаткой программистом или было намерение написать unsigned int? Мы должны заменить все unsigned на unsigned int. Но по мере того как мы делаем это, мы обнаруживаем, что он используется в этом контексте, чтобы указать размер различных двоичных данных. Правильный тип для таких вопросов - стандартный тип C size_t. Это, по сути, просто беззнаковый int, но он гарантированно "достаточно большой" для конкретной платформы. Оператор sizeof возвращает результат типа size_t, и если мы посмотрим на стандартное определение C реального malloc, это void *malloc(size_t size);. Таким образом, size_t является самым правильным типом.

2) Плохая идея использовать одно и то же имя для нашей собственной функции malloc, как та, которая находится в stdlib.h. Если нам нужно включить stdlib.h, все будет беспорядочно. Как правило, никогда не используйте имена идентификаторов стандартных функций библиотеки C в вашем собственном коде. Я изменю имя на kr_malloc.

3) Код злоупотребляет тем, что все статические переменные гарантированно инициализируются до нуля. Это хорошо определено стандартом C, но довольно тонким правилом. Позволяет полностью инициализировать все статистические данные, чтобы показать, что мы не забываем их случайно инициировать.

4) Назначение внутри условий опасно и трудно читается. Этого следует избегать, если это возможно, поскольку это также может привести к ошибкам, таким как ошибка classic = vs ==.

5) Несколько присвоений в одной и той же строке трудно читать, а также, возможно, опасно из-за порядка оценки.

6) Несколько объявлений в одной строке трудно читать и опасны, поскольку это может привести к ошибкам при смешивании данных и деклараций указателей. Всегда объявляйте каждую переменную в отдельной строке.

7) Всегда использует фигурные скобки после каждого утверждения. Не делать этого приведет к ошибкам ошибок ошибок.

8) Никогда не набирайте cast из определенного типа указателя в void *. Это не нужно в C и может скрывать ошибки, которые компилятор в противном случае обнаружил бы.

9) Избегайте использования нескольких операторов возврата внутри функции. Иногда они приводят к более четкому коду, но в большинстве случаев они приводят к спагетти. Поскольку код стоит, мы не можем изменить это, не переписывая цикл, поэтому я исправлю это позже.

10) Держите для петель просто. Они должны содержать один оператор init, одно условие цикла и одну итерацию, ничего больше. Это для цикла, с запятой и всем, очень неясно. Опять же, мы видим необходимость переписать этот цикл в нечто нормальное. Я сделаю это дальше, но пока у нас есть:

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 */
}

Шаг 3: перепишите неясную петлю.

По причинам, упомянутым ранее. Мы видим, что этот цикл продолжается вечно, он завершается возвратом из функции либо при выполнении выделения, либо при отсутствии памяти. Поэтому давайте создадим это как условие цикла и выведем возврат к концу функции, где он должен быть. И позволяет избавиться от этого уродливого оператора запятой.

Я представлю две новые переменные: одну переменную результата, чтобы удерживать результирующий указатель, а другой - отслеживать, должен ли цикл продолжать или нет. Я буду использовать ум K & R, используя тип bool, который является частью языка C с 1999 года.

(Надеюсь, я не изменил алгоритм с этим изменением, я считаю, что не знаю)

#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;
}

Шаг 4: сделайте этот crap компиляцией.

Так как это от 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;
}

И теперь у нас есть несколько читаемый, поддерживаемый код, без многочисленных опасных практик, которые даже скомпилируются! Итак, теперь мы можем начать размышлять о том, что на самом деле делает код.

Структура "Заголовок" - это, как вы могли догадаться, объявление node в связанном списке. Каждый такой node содержит указатель на следующий. Я не совсем понимаю функцию morecore, а также "обертывание", я никогда не использовал эту функцию, а не sbrk. Но я предполагаю, что он выделяет заголовок, как указано в этой структуре, а также некоторый фрагмент необработанных данных, следующих за этим заголовком. Если это так, это объясняет, почему нет фактического указателя данных: предполагается, что данные следуют за заголовком, смежно в памяти. Поэтому для каждого node мы получаем заголовок, и мы получаем кусок необработанных данных, следующих за заголовком.

Итерация сама по себе довольно прямолинейна, они проходят через односвязный список, по одному node за раз.

В конце цикла они устанавливают указатель на точку один за концом "куска", а затем сохраняют его в статической переменной, так что программа будет помнить, где она ранее выделяла память, в следующий раз, когда функция называется.

Они используют трюк, чтобы их заголовок попадал на выровненный адрес памяти: они сохраняют всю служебную информацию в объединении вместе с переменной, достаточно большой, чтобы соответствовать требованию выравнивания платформы. Поэтому, если размер "ptr" плюс размер "размер" слишком малы, чтобы дать точное выравнивание, объединение гарантирует, что выделены по крайней мере размерные (Align) байты. Я считаю, что весь этот трюк устарел сегодня, поскольку стандарт C задает автоматическое дополнение структуры/объединения.

Ответ 2

Я изучаю K & R, так как я бы мог представить, что OP был, когда он задал этот вопрос, и я пришел сюда, потому что я также нашел, что эти реализации запутывают. Хотя принятый ответ очень подробный и полезный, я попытался использовать другой подход, который должен был понять код, поскольку он был изначально написан - я просмотрел код и добавил комментарии к разделам кода, которые мне были трудны, Это включает в себя код для других подпрограмм в разделе (которые являются функциями free и memcore - я переименовал их kandr_malloc и kandr_free, чтобы избежать конфликтов с stdlib). Я думал, что оставлю это здесь в качестве дополнения к принятому ответу, для других студентов, которые могут оказаться полезными.

Я признаю, что комментарии в этом коде чрезмерны. Пожалуйста, знайте, что я делаю это только как учебное упражнение, и я не предлагаю, чтобы это был хороший способ написать код.

Я взял на себя смелость изменить некоторые имена переменных на те, которые казались мне более интуитивными; кроме этого, код по существу оставлен нетронутым. Кажется, он компилируется и работает отлично для тестовых программ, которые я использовал, хотя у valgrind были жалобы на некоторые приложения.

Также: часть текста в комментариях снимается непосредственно с K & R или страниц man - я не собираюсь брать кредит на эти разделы.

#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 data space.  sbrk changes the
   * location of the program break, which defines the end of the process 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 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;
}

Ответ 3

Основной из malloc()

В Linux существует два типичных способа запроса памяти: sbrk и mmap. Эти системные вызовы имеют серьезные ограничения на частые небольшие распределения. malloc() является библиотечной функцией для решения этой проблемы. Он запрашивает большие куски памяти с sbrk/mmap и возвращает небольшие блоки памяти внутри больших кусков. Это намного эффективнее и гибче, чем прямой вызов sbrk/mmap.

K & R malloc()

В реализации K & R ядро ​​(более часто называемое ареной) представляет собой большую часть памяти. morecore() запрашивает ядро ​​из системы через sbrk(). Когда вы вызываете malloc()/free() несколько раз, некоторые блоки в ядрах используются/распределяются, а другие - свободны. K & R malloc хранит адреса свободных блоков в круговом одиночном связанном списке. В этом списке каждый node представляет собой блок свободной памяти. Первые байты sizeof(Header) сохраняют размер блока и указатель на следующий свободный блок. Остальные байты в свободном блоке не инициализируются. В отличие от типичных списков в учебниках узлы в свободном списке просто указывают на некоторые неиспользуемые области в ядрах; вы фактически не выделяете каждый node, за исключением ядер. Этот список является ключом к пониманию алгоритма.

На следующей диаграмме показан пример макета памяти с двумя ядрами/аренами. На диаграмме каждый символ принимает sizeof(Header) байты. @ - это выделенная память Header, +, а - отмечает свободную память внутри ядер. В этом примере три выделенных блока и три свободных блока. Три свободных блока хранятся в круговом списке. Для трех выделенных блоков только их размеры сохраняются в Header.

            This is core 1                             This is core 2

@[email protected][email protected]++++++++++++        @[email protected][email protected]
|                                        |                            |
p->ptr->ptr                              p = p->ptr->ptr->ptr         p->ptr

В вашем коде freep является точкой входа в свободный список. Если вы повторно следуете за freep->ptr, вы вернетесь к freep - это круговой. Когда вы понимаете круглый односвязный список, остальное относительно легко. malloc() находит свободный блок и, возможно, разбивает его. free() добавляет свободный блок обратно в список и может объединить его со смежными свободными блоками. Они оба пытаются сохранить структуру списка.

Другие комментарии к реализации

  • Комментарии кодов, упомянутые "обернутые" в malloc(). Эта строка возникает, когда вы перемещаете весь свободный список, но не можете найти свободный блок, превышающий требуемую длину. В этом случае вам нужно добавить новое ядро ​​с morecore().

  • base - это блок нулевого размера, который всегда включен в бесплатный список. Это трюк, чтобы избежать использования специального корпуса. Это не является строго необходимым.

  • free() может показаться немного сложным, потому что он должен рассмотреть четыре разных случая, чтобы объединить недавно освобожденный блок с другими свободными блоками в списке. Эта деталь не так важна, если вы не хотите переопределить себя.

  • Это сообщение в блоге более подробно описывает K & R malloc.

PS: K & R malloc - один из самых элегантных фрагментов кода, на мой взгляд. Это был действительно глаз, когда я впервые понял код. Мне грустно, что некоторые современные программисты, даже не понимая основности этой реализации, называют хард шедевра исключительно на основе его стиля кодирования.

Ответ 4

Я также нашел это упражнение большим и интересным.

По моему мнению, визуализация структуры может помочь в понимании логики - или, по крайней мере, это сработало для меня. Ниже приведен мой код, в котором максимально печатается поток K & R malloc.

Самое значительное изменение, которое я внес в K & R malloc, - это изменение "свободного", чтобы убедиться, что старый указатель не будет использоваться снова. Помимо этого я добавил комментарии и исправил некоторые небольшие опечатки.

Эксперимент с NALLOC, MAXMEM и тестовыми переменными в "main" также может помочь.

На моем компьютере (Ubuntu 16.04.3) это скомпилировано без ошибок с помощью:

gcc -g -std=c99 -Wall -Wextra -pedantic-errors krmalloc.c

krmalloc.c:

#include <stdio.h>
#include <unistd.h>

typedef long Align;             /* for alignment to long boundary */
union header {                  /* block header */
    struct {
        union header *ptr;      /* next block if on free list */
        size_t size;            /* size of this block */
                                /*      including the Header itself */
                                /*      measured in count of Header chunks */
                                /*      not less than NALLOC Header */
    } s;
    Align x;                    /* force alignment of blocks */
};
typedef union header Header;

static Header *morecore(size_t);
void *mmalloc(size_t);
void _mfree(void **);
void visualize(const char*);
size_t getfreem(void);
size_t totmem = 0;              /* total memory in chunks */

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

#define NALLOC 1                /* minimum chunks to request */
#define MAXMEM 2048             /* max memory available (in bytes) */

#define mfree(p) _mfree((void **)&p)

void *sbrk(__intptr_t incr);


int main(void)
{
    char *pc, *pcc, *pccc, *ps;
    long *pd, *pdd;
    int dlen = 100;
    int ddlen = 50;

    visualize("start");


    /* trying to fragment as much as possible to get a more interesting view */

    /* claim a char */
    if ((pc = (char *) mmalloc(sizeof(char))) == NULL)
        return -1;

    /* claim a string */
    if ((ps = (char *) mmalloc(dlen * sizeof(char))) == NULL)
        return -1;

    /* claim some long */
    if ((pd = (long *) mmalloc(ddlen * sizeof(long))) == NULL)
        return -1;

    /* claim some more long */
    if ((pdd = (long *) mmalloc(ddlen * 2 * sizeof(long))) == NULL)
        return -1;

    /* claim one more char */
    if ((pcc = (char *) mmalloc(sizeof(char))) == NULL)
        return -1;

    /* claim the last char */
    if ((pccc = (char *) mmalloc(sizeof(char))) == NULL)
        return -1;


    /* free and visualize */
    printf("\n");
    mfree(pccc);
    /*      bugged on purpose to test free(NULL) */
    mfree(pccc);
    visualize("free(the last char)");

    mfree(pdd);
    visualize("free(lot of long's)");

    mfree(ps);
    visualize("free(string)");

    mfree(pd);
    visualize("free(less long's)");

    mfree(pc);
    visualize("free(first char)");

    mfree(pcc);
    visualize("free(second char)");


    /* check memory condition */
    size_t freemem = getfreem();
    printf("\n");
    printf("--- Memory claimed  : %ld chunks (%ld bytes)\n",
                totmem, totmem * sizeof(Header));
    printf("    Free memory now : %ld chunks (%ld bytes)\n",
                freemem, freemem * sizeof(Header));
    if (freemem == totmem)
        printf("    No memory leaks detected.\n");
    else
        printf("    (!) Leaking memory: %ld chunks (%ld bytes).\n",
                    (totmem - freemem), (totmem - freemem) * sizeof(Header));

    printf("// Done.\n\n");
    return 0;
}


/* visualize: print the free list (educational purpose) */
void visualize(const char* msg)
{
    Header *tmp;

    printf("--- Free list after \"%s\":\n", msg);

    if (freep == NULL) {                   /* does not exist */
        printf("\tList does not exist\n\n");
        return;
    }

    if  (freep == freep->s.ptr) {          /* self-pointing list = empty */
        printf("\tList is empty\n\n");
        return;
    }

    printf("  ptr: %10p size: %-3lu -->  ", (void *) freep, freep->s.size);

    tmp = freep;                           /* find the start of the list */
    while (tmp->s.ptr > freep) {           /* traverse the list */
        tmp = tmp->s.ptr;
        printf("ptr: %10p size: %-3lu -->  ", (void *) tmp, tmp->s.size);
    }
    printf("end\n\n");
}


/* calculate the total amount of available free memory */
size_t getfreem(void)
{
    if (freep == NULL)
        return 0;

    Header *tmp;
    tmp = freep;
    size_t res = tmp->s.size;

    while (tmp->s.ptr > tmp) {
        tmp = tmp->s.ptr;
        res += tmp->s.size;
    }

    return res;
}


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

    /* smallest count of Header-sized memory chunks */
    /*  (+1 additional chunk for the Header itself) needed to hold nbytes */
    nunits = (nbytes + sizeof(Header) - 1) / sizeof(Header) + 1;

    /* too much memory requested? */
    if (((nunits + totmem + getfreem())*sizeof(Header)) > MAXMEM) {
        printf("Memory limit overflow!\n");
        return NULL;
    }

    if ((prevp = freep) == NULL) {          /* no free list yet */
        /* set the list to point to itself */
        base.s.ptr = freep = prevp = &base;
        base.s.size = 0;
    }

    /* traverse the circular list */
    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 */
                /* adjust the size */
                p->s.size -= nunits;
                /* find the address to return */
                p += p->s.size;
                p->s.size = nunits;
            }
            freep = prevp;
            return (void *)(p+1);
        }

        /* back where we started and nothing found - we need to allocate */
        if (p == freep)                     /* wrapped around free list */
            if ((p = morecore(nunits)) == NULL)
                return NULL;                /* none left */
    }
}


/* morecore: ask system for more memory */
/*      nu: count of Header-chunks needed */
static Header *morecore(size_t nu)
{
    char *cp;
    Header *up;

    /* get at least NALLOC Header-chunks from the OS */
    if (nu < NALLOC)
        nu = NALLOC;

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

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

    printf("... (sbrk) claimed %ld chunks.\n", nu);
    totmem += nu;                           /* keep track of allocated memory */

    up = (Header *) cp;
    up->s.size = nu;

    /* add the free space to the circular list */
    void *n = (void *)(up+1);
    mfree(n);

    return freep;
}


/* mfree: put block ap in free list */
void _mfree(void **ap)
{
    if (*ap == NULL)
        return;

    Header *bp, *p;
    bp = (Header *)*ap - 1;                 /* point to block header */

    if (bp->s.size == 0 || bp->s.size > totmem) {
        printf("_mfree: impossible value for size\n");
        return;
    }

    /* the free space is only marked as free, but 'ap' still points to it */
    /* to avoid reusing this address and corrupt our structure set it to '\0' */
    *ap = NULL;

    /* look where to insert the free space */

    /* (bp > p && bp < p->s.ptr)    => between two nodes */
    /* (p > p->s.ptr)               => this is the end of the list */
    /* (p == p->p.str)              => list is one element only */
    for (p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
        if (p >= p->s.ptr && (bp > p || bp < p->s.ptr))
            /* freed block at start or end of arena */
            break;

    if (bp + bp->s.size == p->s.ptr) {      /* join to upper nbr */
    /* the new block fits perfect up to the upper neighbor */

        /* merging up: adjust the size */
        bp->s.size += p->s.ptr->s.size;
        /* merging up: point to the second next */
        bp->s.ptr = p->s.ptr->s.ptr;

    } else
        /* set the upper pointer */
        bp->s.ptr = p->s.ptr;

    if (p + p->s.size == bp) {              /* join to lower nbr */
    /* the new block fits perfect on top of the lower neighbor */

        /* merging below: adjust the size */
        p->s.size += bp->s.size;
        /* merging below: point to the next */
        p->s.ptr = bp->s.ptr;

    } else
        /* set the lower pointer */
        p->s.ptr = bp;

    /* reset the start of the free list */
    freep = p;
}