web-dev-qa-db-fra.com

Expliquez cette implémentation de malloc à partir du livre K & R

Ceci est un extrait du livre sur C de Kernighan et Ritchie . Il montre comment implémenter une version de malloc. Bien que bien commenté, j'ai beaucoup de difficulté à le comprendre. Quelqu'un peut-il s'il vous plaît expliquer?

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;
}
27
SexyBeast

Ok, ce que nous avons ici est un morceau de code vraiment mal écrit. Ce que je ferai dans cet article pourrait être qualifié d'archéologie logicielle.

Etape 1: corrigez le formatage.

Le format compact et le format compact ne font de bien à personne. Différents espaces et lignes vides doivent être insérés. Les commentaires pourraient être écrits de manière plus lisible. Je vais commencer par résoudre ce problème.

En même temps, je modifie le style de corsetement de style K & R - veuillez noter que le style de corset K & R est acceptable, c’est simplement une de mes préférences personnelles. Une autre préférence personnelle consiste à écrire le * pour les pointeurs en regard du type pointé. Je ne discuterai pas des questions de style (subjectif) ici.

En outre, la définition de type de Header est complètement illisible, elle nécessite une correction radicale.

Et j'ai repéré quelque chose de complètement obscur: ils semblent avoir déclaré un prototype de fonction à l'intérieur de la fonction. Header* morecore(unsigned);. C'est un style très ancien et très médiocre, et je ne suis pas sûr que C le permette même plus longtemps. Supprimons simplement cette ligne. Quelle que soit la fonction utilisée, elle devra être définie ailleurs.

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

Ok, nous pourrions peut-être lire le code.

Étape 2: éliminer les mauvaises pratiques largement reconnues.

Ce code est rempli de choses qui sont à présent considérées comme une mauvaise pratique. Ils doivent être supprimés, car ils compromettent la sécurité, la lisibilité et la maintenance du code. Si vous voulez une référence à une autorité prêchant les mêmes pratiques que moi, consultez la norme de codage largement reconnue MISRA-C .

J'ai repéré et supprimé les mauvaises pratiques suivantes:

1) Il suffit de taper unsigned dans le code pour créer une confusion: s'agit-il d'une faute de frappe du programmeur ou est-ce que l'intention d'écrire unsigned int? Nous devrions remplacer tous les unsigned par unsigned int. Mais en faisant cela, nous constatons qu’il est utilisé dans ce contexte pour donner la taille de diverses données binaires. Le type correct à utiliser pour de telles questions est le type standard C size_t. Il s’agit également d’un fichier int non signé, mais il est garanti qu’il sera "suffisamment grand" pour la plate-forme en question. L'opérateur sizeof renvoie un résultat de type size_t. Si nous examinons la définition du vrai malloc par le standard C, il s'agit de void *malloc(size_t size);. Donc size_t est le type le plus correct à utiliser.

2) C'est une mauvaise idée d'utiliser le même nom pour notre propre fonction malloc que celle résidant dans stdlib.h. Si nous devions inclure stdlib.h, la situation deviendrait désordonnée. En règle générale, n'utilisez jamais les noms d'identifiant des fonctions de bibliothèque standard C dans votre propre code. Je changerai le nom en kr_malloc.

3) Le code abuse du fait que toutes les variables statiques sont garanties pour être initialisées à zéro. Ceci est bien défini par le standard C, mais une règle assez subtile. Permet d'initialiser toutes les statiques explicitement, pour montrer que nous n'avons pas oublié de les initialiser par accident.

4) L'affectation à l'intérieur des conditions est dangereuse et difficile à lire. Cela devrait être évité si possible, car cela peut également conduire à des bugs, tels que le bogue classic = vs ==. 

5) Plusieurs assignations sur la même ligne sont difficiles à lire et potentiellement dangereuses en raison de l'ordre d'évaluation.

6) Il est difficile de lire plusieurs déclarations sur la même ligne, et cela est dangereux car cela pourrait entraîner des erreurs lors du mélange des données et des déclarations de pointeur. Déclarez toujours chaque variable sur une ligne distincte.

7) utilise toujours des accolades après chaque déclaration. Ne pas le faire conduira à bugs bugs bugs.

8) Ne tapez jamais la conversion d'un type de pointeur spécifique sur void *. C'est inutile en C, et pourrait cacher des bogues que le compilateur aurait autrement détectés.

9) Évitez d’utiliser plusieurs instructions de retour dans une fonction. Parfois, ils permettent d'obtenir un code plus clair, mais dans la plupart des cas, ils conduisent à des spaghettis. En l'état actuel du code, nous ne pouvons pas changer cela sans réécrire la boucle, je vais donc régler le problème plus tard.

10) Gardez les boucles simples. Ils doivent contenir une instruction init, une condition de boucle et une itération, rien d'autre. Cette boucle for, avec l'opérateur virgule et tout, est très obscure. Encore une fois, nous identifions le besoin de réécrire cette boucle en quelque chose de sain. Je ferai ceci ensuite, mais pour l'instant nous avons:

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

Étape 3: réécrivez la boucle obscure.

Pour les raisons mentionnées précédemment. Nous pouvons voir que cette boucle dure indéfiniment, elle se termine en revenant de la fonction, soit lorsque l'allocation est terminée, soit lorsqu'il ne reste plus de mémoire. Nous allons donc créer cela comme une condition de boucle et lever le retour à la fin de la fonction où elle devrait être. Et nous allons nous débarrasser de cet opérateur de virgule laid.

Je vais introduire deux nouvelles variables: une variable de résultat pour contenir le pointeur résultant et une autre pour savoir si la boucle doit continuer ou non. Je vais faire sauter les esprits de K & R en utilisant le type bool, qui fait partie du langage C depuis 1999.(J'espère ne pas avoir modifié l'algorithme avec ce changement, je crois que je ne l'ai pas fait).

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

sizeof(header) devrait être sizeof(Header). Il manque des points-virgules. Ils utilisent des noms différents freep, prevp contre freeptr, prevptr, mais signifient clairement la même variable. Je crois que ces derniers étaient en fait de meilleurs noms, alors utilisons-les.

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

sbrk. Mais je suppose qu’elle alloue un en-tête comme spécifié dans cette structure, ainsi qu’une partie des données brutes suivant cet en-tête. Si c'est le cas, cela explique pourquoi il n'y a pas de pointeur de données réel: les données sont supposées suivre l'en-tête, de manière adjacente en mémoire. Donc, pour chaque nœud, nous obtenons l'en-tête, et nous obtenons un bloc de données brutes après l'en-tête.

L'itération elle-même est assez simple, ils parcourent une liste à lien unique, un nœud à la fois.

À la fin de la boucle, ils placent le pointeur de manière à ce qu'il pointe au-delà de la "partie", puis l'enregistrent dans une variable statique, de sorte que le programme se souvienne où il avait précédemment alloué de la mémoire, lors du prochain appel de la fonction.

Ils utilisent une astuce pour que leur en-tête aboutisse sur une adresse mémoire alignée: ils stockent toutes les informations de surcharge dans une union avec une variable suffisamment grande pour correspondre aux exigences d'alignement de la plate-forme. Donc, si la taille de "ptr" plus la taille de "taille" sont trop petites pour donner l'alignement exact, l'union garantit qu'au moins sizeof (Align) octets sont alloués. Je crois que toute cette astuce est obsolète aujourd'hui, car la norme C impose le remplissage automatique des structures/unions.

They are using a trick to make their header end up on an aligned memory address: they store all the overhead info in a union together with a variable large enough to correspond to the platform's alignment requirement. So if the size of "ptr" plus the size of "size" are too small to give the exact alignment, the union guarantees that at least sizeof(Align) bytes are allocated. I believe that this whole trick is obsolete today, since the C standard mandates automatic struct/union padding.

56
Lundin

J'étudie K & R comme j'imaginais qu'Op l'était lorsqu'il a posé cette question et je suis venu ici parce que je trouvais également que ces implémentations étaient source de confusion. Bien que la réponse acceptée soit très détaillée et utile, j’ai essayé de prendre un point de vue différent, qui était de comprendre le code tel qu’il avait été écrit à l’origine - j’ai parcouru le code et ajouté des commentaires qui me paraissaient difficiles. . Cela inclut le code pour les autres routines de la section (qui sont les fonctions free et memcore - je les ai renommées kandr_malloc et kandr_free pour éviter les conflits avec stdlib). Je pensais laisser ceci ici comme un complément à la réponse acceptée, pour les autres étudiants qui pourraient le trouver utile.

Je reconnais que les commentaires dans ce code sont excessifs. Sachez que je ne fais que cela à titre d’exercice d’apprentissage et que ce n’est pas un bon moyen d’écrire du code.

J'ai pris la liberté de changer certains noms de variables en noms qui me semblaient plus intuitifs; autre que celui le code est essentiellement laissé intact. Il semble que la compilation et le fonctionnement soient corrects pour les programmes de test que j’ai utilisés, bien que valgrind se soit plaint de certaines applications.

En outre: une partie du texte dans les commentaires provient directement de K & R ou des pages de manuel - je n’ai pas l’intention de prendre un crédit pour ces sections.

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

La base de malloc ()

Sous Linux, il existe deux méthodes classiques pour demander de la mémoire: sbrk et mmap . Ces appels système ont de sérieuses limites sur les petites attributions fréquentes. malloc () est une fonction de bibliothèque permettant de résoudre ce problème. Il demande de gros morceaux de mémoire avec sbrk/mmap et renvoie de petits blocs de mémoire à l'intérieur de gros morceaux. C'est beaucoup plus efficace et flexible que d'appeler directement sbrk/mmap.

K & R malloc ()

Dans l'implémentation K & R, un core (plus communément appelé arena) est une grande quantité de mémoire. morecore() demande un noyau au système via sbrk(). Lorsque vous appelez malloc ()/free () plusieurs fois, certains blocs des cœurs sont utilisés/alloués, d'autres sont gratuits. K & R malloc stocke les adresses des blocs libres dans une seule liste chaînée circulaire. Dans cette liste, chaque nœud est un bloc de mémoire libre. Les premiers octets sizeof(Header) conservent la taille du bloc et le pointeur sur le bloc libre suivant. Les autres octets du bloc libre ne sont pas initialisés. Différents des listes typiques des manuels, les nœuds de la liste libre ne sont que des pointeurs sur certaines zones inutilisées des cœurs; vous n'allouez pas réellement chaque noeud, à l'exception des cœurs. Cette liste est la clé de la compréhension de l’algorithme.

Le diagramme suivant montre un exemple d’agencement de mémoire avec deux cœurs/arènes. Dans le diagramme, chaque caractère prend sizeof(Header) octets. @ est une Header, + marque la mémoire allouée et - marque la mémoire libre dans les cœurs. Dans l'exemple, il y a trois blocs attribués et trois blocs libres. Les trois blocs libres sont stockés dans la liste circulaire. Pour les trois blocs alloués, seules leurs tailles sont stockées dans Header.

            This is core 1                             This is core 2

@---------@+++++++++@++++++++++++        @----------@+++++++++++++++++@------------
|                                        |                            |
p->ptr->ptr                              p = p->ptr->ptr->ptr         p->ptr

freep est dans votre code un point d’entrée dans la liste libre. Si vous suivez plusieurs fois freep->ptr, vous reviendrez à freep - il est circulaire. Une fois que vous avez compris la liste circulaire à lien unique, le reste est relativement facile. malloc() trouve un bloc libre et le divise éventuellement. free() ajoute un bloc libre à la liste et peut le fusionner avec des blocs libres adjacents. Ils essaient tous deux de maintenir la structure de la liste.

Autres commentaires sur la mise en œuvre

  • Les commentaires de code mentionnaient "enveloppés" dans malloc(). Cette ligne survient lorsque vous avez parcouru toute la liste des places libres, mais que vous ne trouvez pas un bloc libre plus grand que la longueur demandée. Dans ce cas, vous devez ajouter un nouveau noyau avec morecore().

  • base est un bloc de taille zéro qui est toujours inclus dans la liste libre. C'est un truc pour éviter les douilles spéciales. Ce n'est pas strictement nécessaire.

  • free() peut sembler un peu complexe car il doit prendre en compte quatre cas différents pour fusionner un bloc libéré avec d'autres blocs libres de la liste. Ce détail n’est pas si important à moins que vous ne souhaitiez le réimplémenter vous-même.

  • Cet article de blog explique K & R malloc en plus de détails.

(PS:} K & R malloc est l'un des codes les plus élégants à mes yeux. C'était vraiment une révélation quand j'ai compris le code pour la première fois. Cela me rend triste que certains programmeurs modernes, ne comprenant même pas les bases de cette implémentation, appellent la merde du chef-d'œuvre uniquement en fonction de son style de codage.

7
user172818

J'ai aussi trouvé cet exercice formidable et intéressant.

A mon avis, visualiser la structure peut aider beaucoup à comprendre la logique - ou du moins, cela a fonctionné pour moi. Ci-dessous se trouve mon code, qui imprime le plus possible sur le flux du K & R Malloc.

Le changement le plus important que j'ai apporté dans le malloc K & R est le changement de 'gratuit' pour m'assurer que certains anciens pointeurs ne seront plus utilisés ... __ A part cela, j'ai ajouté des commentaires et corrigé quelques petites fautes de frappe.

Expérimenter avec NALLOC, MAXMEM et les variables de test dans 'main' pourrait également être utile.

Sur mon ordinateur (Ubuntu 16.04.3), cela a été compilé sans erreur avec:

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 */
    } 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's */
    if ((pd = (long *) mmalloc(ddlen * sizeof(long))) == NULL)
        return -1;

    /* claim some more long's */
    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;
}
0
Vladimir