C言語によるメモリー管理

記事
IT・テクノロジー
マルチスレッドデータベースサーバで必要なメモリー管理
本ブログも5回目となりました。
前回のブログでは、マルチスレッドデータベースサーバでCGIと通信するソケットプログラミングについて詳細を書いておきました。
今回は、マルチスレッドデータベースサーバで重要なメモリー管理についてお話します。この話は、前々回のブログの内容とかなり関連しますので、一応リンクしておきます。

C言語でメモリー領域は、1)関数内で宣言する固定的なメモリー領域のもの(スタック上に確保される)と、2)malloc()などの関数で、動的に確保したメモリー領域があります。

1の場合はスタック上に確保されたものだから、そのメモリー領域は、その関数内と、その関数から呼び出される関数の中だけで有効です。そして、関数からreturnで抜けると、そのメモリー領域はスタックから解放されて、以降、参照できなくなります。いや参照してもよいですが、内容はどうなっているか不明です。
2の場合は、ヒープ領域にメモリが確保されていますから、プログラム終了までメモリー領域は解放されず、もし、解放する必要があるときは、free()で明示的に解放する必要があります。
WEBサーバでCGIは、httpdがリクエストを受けた段階で立ち上げられ、終ると終了しますので、CGIの中でmallocなどによるメモリー確保をしても、それは終了とともに解放されます。しかし、マルチスレッドのデータベースサーバの場合は、スレッド内で確保したメモリー領域は、スレッドが終了してもサーバ自体が動いている間は、解放されません。ですから、スレッドが終了するときに、明示的にメモリーを解放する必要があります。
となると、スレッドから呼ばれた関数がそれぞれあちこちでmalloc()でメモリー領域を確保している場合、スレッド終了時に全部メモリーを解放して回るのは、結構面倒です。また、データベースサーバですから、データベースを格納しているメモリー領域は、スレッドが終了しても、解放するわけにはいかず、解放する前に、必ずファイルに内容をセーブするなどの機能も必要です。
また、malloc()の呼び出しとメモリーの領域確保はそれなりに重たい処理なので、malloc() の呼び出しはできるだけ最小限に済ませたいものです。とすれば、ある程度大きいメモリー領域を予め確保し、ちまちましたメモリー領域確保は、大きく確保した領域の上で再度、必要な分だけちまちまとればよいことになります。そうすると、スレッドが終了したときに、大きく確保したメモリー領域を一挙に解放することで、malloc()やfree()の呼び出しがかなり減ります。
また、データベースサーバの中にあるデータのメモリー領域は、データその部分のメモリー領域については、その部分の内容ををファイルに書き出したあと、解放されるべきです。

メモリーマネージャ memoryManager型構造体の仕様
以上から、メモリーマネージャは、以下のような構造体として定義されます。
----ここから----
//メモリー領域(断片)を表すheap型構造体
typedef struct          heap
{
//次のヒープへのポインタ
  heap*                 Next;//初期値はNULL
//確保されているメモリーのサイズ(blockユニオンの数)
  int64                 NumBlocks;//初期値は確保されたブロック数
//あらたにメモリーを確保する場合の先頭
  int64                 IndexNew;//初期値は0
//メモリー確保をした時間
  int64                 TimeStamp;
//実際のメモリー領域である blockユニオンの配列
  block                 Blocks [];
// union block は、128ビット(16バイト)までの
//様々なデータを入れることのできるユニオン
};

//メモリーマネージャの構造体
typedef struct          memoryManager
{
//ヒープ領域で現在メモリー確保をしているヒープへのポインタ
  heap*                 Now; //初期値はNULL
//メモリーマネージャーの最初のヒープ領域
  heap*                 First; //初期値はNULL
//標準的なヒープサイズ(blockユニオンの数)
  size_t                StdNumBlocks; //初期値は 0x1000(=4096)
//メモリーが確保されていることを表すフラグ
  bool                  BusyFlag;
//メモリーの内容をファイルにセーブする際のパス
  char*                 BasePath; 
//セーブするファイル名
  char*                 FileName;
};

//スレッドマネージャの構造体
typedef struct           threadManager
{
//メモリーマネージャMM
  memoryManager         MM;
  //ほかにもいろいろ変数あり。
};

//データベースの構造体(前々回ブログでご紹介)
typedef struct          dbData
{
//メモリーマネージャMM
  memoryManager         MM;
  pthread_mutex_t       Mutex;
  pthread_mutex_t       MutexCW; 
  pthread_mutex_t       MutexCA;
  int32                 CntAccessing;
  int32                 CntWaiting;
  int64                 TimeOpened;
  structId*             StructId;
  block                 DataBlock;
};
----ここまで----

メモリーマネージャーは、実際には、スレッドごとに確保されるthreadManager構造体の中で定義されていて、そこからスレッドから呼ばれる関数のほぼすべてで、threadManager構造体が渡されます。また、前々回のブログ(データベースサーバ篇)でご紹介した dbData型構造体の中にもメモリーマネージャ構造体がしっかり入っています。
threadManagerに対して、メモリーを確保する場合は、次のmemoryManager_alloc()関数を使います。
void*  memoryManager_alloc (memoryManager*  This,  int64  Size);
ここで引数にあるSizeは、バイト数です。

メモリー領域確保の実際
C言語ではメモリー確保の際、いろいろ面倒な仕組みがあります。メモリーアドレスを16進数で書いたとき、確保されるメモリー領域は下一桁が0である領域を先頭にしないといけません。構造体の中での要素のデータは、さらに複雑に配置されていて、これをパディングといいます。64ビットのデータ(ポインタや、double型の浮動小数点数など)は、確保される場合データ先頭のアドレスは、16進数では下一桁が0か8のみ許されます。32ビット型では、0,4,8,12,のみ、16ビット型では、0,2,4,6,8,10,12,14のみになります。
そこで、malloc()関数でも、必ず確保したメモリー領域の先頭アドレスは、16進数で表したときに、下一桁が0になるようになっています。
memoryManager_alloc()関数もそのため、内部では16バイト長のblockユニオンでメモリーを確保します。
さて、memoryManager_alloc()が呼ばれたときに、memoryManager構造体(引数This で渡される)が初期状態の場合、最初のheapをmallocして確保し、This->Now とThis->Firstにポインタをいれます。そして、heapの中で必要なサイズSizeを確保し、確保された部分の次のblockの場所をheap のindexNewにいれます。以降、memoryManager_alloc()が呼び出されたときは、heapの中でメモリー領域に空きがあれば、そこに領域を確保していきます。そして、空きがないときは、新なheapを確保して、それをThis->Nowにつなぎます。
よって、memoryManagerに確保されたメモリー領域を解放するときは、Firstからたどって、heapを、free()で解放すればよく、スレッドの終了時には、一気にスレッド内で確保したメモリー領域が解放できるのです。もちろん、メモリー上のデータベースの内容をセーブしたときも、その部分に確保されたメモリー領域は一気に解放されます。
この処理により、システム全体のメモリーはつねに必要な部分のみメモリーが確保され、必要なくなった部分は即座に解放され、不要なメモリー領域が解放されずにどんどん溜まっていくような事態は避けられます。

IO関係のバッファ用メモリーの管理のための、ioManager構造体
ファイルの入出力、あるいはソケット通信などでのデータを蓄積する場合は、上記のmemoryManager構造体ではオーバースペックで、バイト単位での書き込みや読み込みが必要で、一方、内部でポインタが張り巡らされることはないので、固定長のメモリー領域にバイト単位で情報を書き込んだり、読み込んだりができるほうが便利です。C言語では、標準でFILE構造体が用意されていますね。それと類似のものを実装したのが、ioManager構造体です。

----ここから-----
typedef struct ioBuffer
{
//次のioBufferへのポインタ
  ioBuffer*             Next;
//書き込まれた場所の配列上の位置
  int32                 PtrEnd;
//読み込まれている場所の配列上の位置
  int32                 PtrNow;
//uint8型の配列(実際に書き込まれる場所)
  uint8                 UInt8Array     [];
};

typedef struct          ioManager
{
//最後に確保したioBufferへのポインタ
  ioBuffer*             Last;
//最初に確保したioBufferへのポインタ
  ioBuffer*             First;
//現在読み出し中のioBufferへのポインタ
  ioBuffer*             Now;
//現在読み出し中のioBufferの一つ前のioBufferへのポインタ
  ioBuffer*             Prev;
};
----ここまで----
ioBufferのUInt8Arrayのサイズは、32kBです。これは、ソケット通信などで、一回に送れるサイズがその程度であることが多いからです。
ioManagerでは、書き込みモードと、読み出しモードがあります。
まず、ファイルから読み込むときは、書き込みモードで、そしてその内容を読み出すときは、読み出しモードです。
書き込みモードでは、ioBuffer中のPtrEndが増えていきます。32kBまで書き込むと、新なioBufferをmallocで確保して、先を続けます。
読み込みモードでは、PtrNowが増えていき、PtrEndにくるまで読み込みが可能になっています。
バイナリの読み込み書き込みも可能なので、charではなく、uint8 (unsigned char)型の配列になっています。
書き込み用の関数や、読み込み用の関数などが多数準備されていて、画像や動画などをファイルから読み込んだり、それをソケット通信のファイルデスクリプタに書き出したり、いろいろなことができます。
また、HTMLファイルの生成のためには、マクロ展開をする必要がありますが、マクロ展開する前の内容を保存しているioManagerから、マクロ展開をしつつ、別のioManagerにコピーする関数なども揃っています。

まとめに変えて
以上のように、HOTPortでは、二つのメモリーマネージャ、memoryManagerと、ioManagerがあって、malloc()を使ったメモリー領域確保は、これらのメモリーマネージャが行います。また、memoryManagerは、スレッドではthreadManager、データベースについては、dbDataによって管理されているので、メモリーの解放などは気にせず、プログラミングができます。ioManagerについては、使い終わったら解放するのが基本です。
malloc()をつかったメモリー領域確保は最小限であり、解放も一気に解放するので、非常に効率のよいメモリー管理になっています。たぶん、通常の参照カウンタをつかったメモリー管理(Javaなどで使われている)よりは効率がよさそうです。

さて、次回は、そろそろブロックチェーン周りについて書こうかと思います。
サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す