これはKernighan and 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;
}
わかりました、私たちがここに持っているのは、本当に不十分に書かれたコードの塊です。この投稿で私がすることは、ソフトウェア考古学として最もよく説明できます。
ステップ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 */
}
}
_
さて、実際にコードを読むことができるかもしれません。
ステップ2:広く認識されている悪い習慣を取り除く
このコードは、今日では悪い習慣と見なされているもので満たされています。コードの安全性、可読性、およびメンテナンスを危険にさらすため、削除する必要があります。私と同じ慣行を説く当局への参照が必要な場合は、広く認識されているコーディング標準 MISRA-C を確認してください。
次の悪い習慣を見つけて削除しました。
1)コードにunsigned
と入力するだけで混乱を招く可能性があります。これはプログラマーによるタイプミスでしたか、それとも_unsigned int
_を書くつもりでしたか?すべてのunsigned
を_unsigned int
_に置き換える必要があります。しかし、それを行うと、このコンテキストでさまざまなバイナリデータのサイズを提供するために使用されることがわかります。そのような問題に使用する正しい型は、C標準型_size_t
_です。これは本質的には単に符号なし整数ですが、特定のプラットフォームに対して「十分に大きい」ことが保証されています。 sizeof
演算子は、タイプ_size_t
_の結果を返します。実際のmallocのC標準の定義を見ると、void *malloc(size_t size);
です。したがって、_size_t
_を使用するのが最も正しい型です。
2)独自のmalloc関数に、stdlib.hにある名前と同じ名前を使用するのは悪い考えです。 stdlib.hをインクルードする必要がある場合、事態は面倒になります。経験則として、独自のコードでC標準ライブラリ関数の識別子名を使用しないでください。名前をkr_mallocに変更します。
3)コードは、すべての静的変数がゼロに初期化されることが保証されているという事実を悪用しています。これはC標準によって明確に定義されていますが、かなり微妙なルールです。すべての静的変数を明示的に初期化して、誤って初期化するのを忘れていないことを示しましょう。
4)条件内の割り当ては危険で読みにくい。これは、古典的な= vs ==バグなどのバグにもつながる可能性があるため、可能であれば回避する必要があります。
5)同じ行の複数の割り当ては読みにくい上、評価の順序のために危険な可能性もあります。
6)同じ行での複数の宣言は読みにくく、危険です。データ宣言とポインター宣言を混在させるとバグが発生する可能性があるためです。各変数は常に独自の行で宣言します。
7)すべてのステートメントの後に必ず中括弧を使用します。そうしないと、バグ、バグ、バグにつながります。
8)特定のポインター型からvoid *へのキャストをタイプしないでください。 Cでは不要であり、コンパイラが検出しなかったバグを隠すことができます。
9)関数内で複数のreturnステートメントを使用しないでください。時々、より明確なコードにつながりますが、ほとんどの場合、スパゲッティにつながります。コードが立っているので、ループを書き直さずに変更することはできませんので、後で修正します。
10)forループをシンプルにします。これらには、1つのinitステートメント、1つのループ条件、1つの反復が含まれている必要があります。コンマ演算子とすべてを使用したこの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 */
}
_
ステップ3:あいまいなループを書き換える
前述の理由により。このループは永遠に続き、割り当てが完了したとき、またはメモリが残っていないときに関数から戻ることで終了することがわかります。そのため、それをループ条件として作成し、関数のあるべき場所への戻りを解除します。そして、そのいコンマ演算子を取り除きましょう。
2つの新しい変数を紹介します。1つは結果のポインターを保持する結果変数、もう1つはループを続行するかどうかを追跡する変数です。 1999年以来C言語の一部であるbool
型を使用して、K&Rの心を吹き飛ばします。
(この変更でアルゴリズムを変更していないことを願っていますが、変更していないと思います)
_#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:このがらくたをコンパイルする
これは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;
}
_
そして今、私たちは幾分読みやすく、保守可能なコードを持っています。多くの危険な慣行はなく、コンパイルさえできます!そのため、実際にコードが何をしているのかを考え始めることができます。
構造体「ヘッダー」は、ご想像のとおり、リンクリスト内のノードの宣言です。そのような各ノードには、次のノードへのポインターが含まれています。 morecore関数も「ラップアラウンド」もよくわかりません。この関数もsbrk
も使用したことがありません。しかし、この構造体で指定されたヘッダーと、そのヘッダーに続く生データのチャンクを割り当てると仮定します。もしそうなら、それは実際のデータポインターがない理由を説明します。データは、メモリ内で隣接してヘッダーに続くと想定されます。各ノードについて、ヘッダーを取得し、ヘッダーに続く生データのチャンクを取得します。
反復自体は非常に単純で、一度に1ノードずつ、単一リンクリストを通過します。
ループの最後で、「チャンク」の終わりを過ぎた位置を指すようにポインターを設定し、それを静的変数に保存します。これにより、プログラムは次回関数が呼び出されたときに以前にメモリを割り当てた場所を記憶します。
彼らはヘッダーをアライメントされたメモリアドレスで終わるためにトリックを使用しています:彼らはすべてのオーバーヘッド情報をプラットフォームのアライメント要件に対応するのに十分な大きさの変数とともにユニオンに保存します。そのため、「ptr」のサイズと「size」のサイズが小さすぎて正確な位置合わせができない場合、ユニオンは少なくともsizeof(Align)バイトが割り当てられることを保証します。 C標準では自動構造体/共用体のパディングが義務付けられているため、このトリック全体は今日では時代遅れになっていると思います。
私はOPが彼がこの質問をしたときだと想像していたので、私はK&Rを勉強しています、そして、私はこれらの実装も混乱させるためにここに来ました。受け入れられた答えは非常に詳細で有用ですが、コードを元々書かれていたとおりに理解するための別の取り組みを試みました-私はコードを調べて、私が難しいコードのセクションにコメントを追加しました。これには、セクション内の他のルーチン(関数free
およびmemcore
-の名前を変更しましたkandr_malloc
およびkandr_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;
}
Linuxでは、メモリを要求する2つの一般的な方法があります: sbrk および mmap 。これらのシステムコールには、頻繁な小さな割り当てに厳しい制限があります。 malloc()は、この問題に対処するライブラリ関数です。 sbrk/mmapで大きなメモリチャンクを要求し、大きなチャンク内の小さなメモリブロックを返します。これは、sbrk/mmapを直接呼び出すよりもはるかに効率的で柔軟です。
K&R実装では、core(より一般的にarenaと呼ばれる)は大きなメモリチャンクです。 morecore()
は、sbrk()
を介してシステムにコアを要求します。 malloc()/ free()を複数回呼び出すと、コア内の一部のブロックが使用/割り当てられ、他のブロックは解放されます。 K&R mallocは、フリーブロックのアドレスをcircular単一リンクリストに格納します。このリストでは、各ノードは空きメモリのブロックです。最初のsizeof(Header)
バイトは、ブロックのサイズと次の空きブロックへのポインターを保持します。空きブロックの残りのバイトは初期化されていません。教科書の一般的なリストとは異なり、フリーリストのノードはコアの一部の未使用領域へのポインターにすぎません。実際には、コア以外の各ノードを割り当てません。このリストは、アルゴリズムを理解するための鍵です。
次の図は、2つのコア/アリーナを使用したメモリレイアウトの例を示しています。図では、各文字はsizeof(Header)
バイトを取ります。 _@
_はHeader
で、_+
_は割り当てられたメモリをマークし、_-
_はコア内の空きメモリをマークします。この例では、3つの割り当て済みブロックと3つの空きブロックがあります。 3つの空きブロックは、循環リストに保存されます。割り当てられた3つのブロックの場合、サイズのみがHeader
に格納されます。
_ This is core 1 This is core 2
@---------@+++++++++@++++++++++++ @----------@+++++++++++++++++@------------
| | |
p->ptr->ptr p = p->ptr->ptr->ptr p->ptr
_
コードでは、freep
はフリーリストへのエントリポイントです。 _freep->ptr
_を繰り返した場合、freep
に戻ります。循環的です。循環単一リンクリストを理解すれば、残りは比較的簡単です。 malloc()
は、空きブロックを検出し、場合によっては分割します。 free()
は、空きブロックをリストに追加し、隣接する空きブロックにマージする場合があります。どちらもリストの構造を維持しようとします。
コードのコメントでは、malloc()
に「ラップアラウンド」と記載されています。その行は、フリーリスト全体をトラバースしたが、要求された長さを超えるフリーブロックが見つからない場合に発生します。この場合、morecore()
を使用して新しいコアを追加する必要があります。
base
は、常に空きリストに含まれるゼロサイズのブロックです。特殊なケースを避けるのはコツです。厳密には必要ありません。
free()
は、リスト内の他の空きブロックに新しく解放されたブロックをマージするために4つの異なるケースを考慮する必要があるため、少し複雑に見えるかもしれません。この詳細は、自分で再実装する場合を除き、それほど重要ではありません。
このブログ投稿 K&R mallocの詳細を説明しています。
PS:K&R mallocは、私の考えでは最もエレガントなコードの1つです。私が最初にコードを理解したとき、それは本当に目を見張るものでした。この実装の基本を理解していない現代のプログラマーが、コーディングスタイルだけに基づいて傑作を呼んでいるのは悲しいことです。
また、このエクササイズは素晴らしく、面白かったです。
私の意見では、構造を視覚化することは論理を理解するのに大いに役立つかもしれません-または少なくともこれは私のために働いた。以下は私のコードで、K&R mallocのフローについて可能な限り出力します。
K&R mallocで私が行った最も重要な変更は、いくつかの古いポインターが再び使用されないようにするための 'free'の変更です。それ以外はコメントを追加し、いくつかの小さなタイプミスを修正しました。
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 */
} 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;
}