プログラミング:メモリーリーク
今日は週初めのミーティングや事務的な仕事を済ませたら、すでに夕方だったのですが、納期まで余裕があるのと、内容が面倒なのでなかなか手をつけていなかったプログラムの変更を行いました。
半年前くらいに作った、あるシステムのデバッグ用のシミュレータなのですが、当初のテスト方法と、実際にシステムが動き始めてからやりたいテストがかなり異なってしまい、デバッグ用プログラムの仕様変更という感じになりました。
技術的にも仕様的にもかなり複雑なシミュレータで、1つのコントロール用TCP/IPコネクションを通じて5000グループ程度が起動したり終了したりし、1グループに4クライアントくらいが起動してそれぞれがサーバとTCP/IPコネクションを張ってやりとりするという感じで、さらにコントロール用プロトコルもかなり複雑です。
半年前を思い出しながら、まずはこんな感じかな、と変更してみたのですが、途中で余りに複雑になってきたので中断し、もう一度考え直してシンプルに変更できました。ところが、実験してみるとどんどんメモリーを消費し、明らかにメモリーリークしてます。半年前にも途中で何度も仕様変更がありましたし、その後、テストに応じていろいろと変更されていたりしたのと、そもそも仕様を少し勘違いしていて、シミュレータへの指示の与え方のミスもあり、メモリーを使いすぎていたのです。
プログラムのミスによるメモリーリークは、実は結構探すのが難しく、mtraceなどのデバッグ用のライブラリを使用したり、メモリーデバッグ用ツールを使ったり、あるいは地道にソースを確認したりするのですが、今回は仕様も作りも結構複雑なのと、多数のスレッドが複雑に関係しながら動くため、デバッグ用ライブラリを使ってもなかなかポイントを絞りにくいという状態です。
そこで、一番手っ取り早い方法として、メモリー確保・解放関数にラッパーを作り、そこで確保したアドレスをコメント付きで配列に格納し、解放したら解放済みマークをセットするようにして、確認したいポイントで、解放済みになっていないデータの一覧を表示させるようにしてデバッグしました。いい加減な作戦ですが、実は結構メリットもあり、確認したいあたりだけラッパーを呼ぶようにすることで、複雑に多くのスレッドが関係し合って動いていてもポイントだけ確認できるのです。
これにより、シミュレータへの指示の与え方のミスに気がついたり、数カ所の解放忘れを見つけ、無事にリークを解消できました。
応急的に使えれば良いので、以下のような非常にざっくりした作りですし、格納領域も固定配列で手抜きしていますが、このくらいなら5分もかからずに作れるでしょう。
2009/7/8 ソースを少し修正しました。
#undef malloc
#undef calloc
#undef realloc
#undef free
#undef strdup
typedef struct {
void *ptr;
char *msg;
int flag;
}T_REC;
#define T_NO 100000
T_REC Trec[T_NO];
int TrecNo=0;
pthread_mutex_t T_mutex=PTHREAD_MUTEX_INITIALIZER;
int t_add(void *ptr,char *msg)
{
register int i;
int intoNo;
pthread_mutex_lock(&T_mutex);
intoNo=-1;
for(i=0;i<TrecNo;i++){
if(Trec[i].flag==0){
intoNo=i;
break;
}
}
if(intoNo==-1){
if(TrecNo>=T_NO){
fprintf(stderr,"t_add overflown\n");
return(-1);
}
intoNo=TrecNo;
TrecNo++;
}
Trec[intoNo].ptr=ptr;
Trec[intoNo].msg=strdup(msg);
Trec[intoNo].flag=1;
pthread_mutex_unlock(&T_mutex);
return(0);
}
int t_del(void *ptr)
{
register int i;
pthread_mutex_lock(&T_mutex);
for(i=0;i<TrecNo;i++){
if(Trec[i].ptr==ptr){
free(Trec[i].msg);Trec[i].msg=NULL;
Trec[i].flag=0;
Trec[i].ptr=0;
}
}
pthread_mutex_unlock(&T_mutex);
return(0);
}
int t_out()
{
register int i;
fprintf(stderr,"t_out start\n");
for(i=0;i<TrecNo;i++){
if(Trec[i].flag==1){
fprintf(stderr,"%x:%s\n",Trec[i].ptr,Trec[i].msg);
free(Trec[i].msg);Trec[i].msg=NULL;
}
}
fprintf(stderr,"t_out end\n");
return(0);
}
void *t_malloc(size_t size,char *msg,int msg2,char *msg3)
{
void *ptr;
char buf[512];
ptr=malloc(size);
sprintf(buf,"%s:%d:%s",msg,msg2,msg3);
t_add(ptr,buf);
return(ptr);
}
void *t_calloc(size_t nelem,size_t elsize,char *msg,int msg2,char *msg3)
{
void *ptr;
char buf[512];
ptr=calloc(nelem,elsize);
sprintf(buf,"%s:%d:%s",msg,msg2,msg3);
t_add(ptr,buf);
return(ptr);
}
void *t_realloc(void *optr,size_t size,char *msg,int msg2,char *msg3)
{
void *ptr;
char buf[512];
ptr=realloc(optr,size);
sprintf(buf,"%s:%d:%s",msg,msg2,msg3);
t_del(optr);
t_add(ptr,buf);
return(ptr);
}
char *t_strdup(char *str,char *msg,int msg2,char *msg3)
{
char *ptr;
char buf[512];
ptr=strdup(str);
sprintf(buf,"%s:%d:%s",msg,msg2,msg3);
t_add(ptr,buf);
return(ptr);
}
void t_free(void *ptr)
{
free(ptr);
t_del(ptr);
}
下記のようなヘッダファイルを試験対象のソースにincludeすると簡単に試験できます。
void *t_malloc(size_t size,char *msg, int, char *);
void *t_calloc(size_t nelem,size_t elsize,char *msg, int, char *);
void *t_realloc(void *optr,size_t size,char *msg, int, char *);
char *t_strdup(char *str,char *msg, int, char *);
void t_free(void *ptr);
#define strutil_dup(a) strdup(a)
#define malloc(a) t_malloc(a,__FILE__,__LINE__,__FUNCTION__)
#define calloc(a,b) t_calloc(a,b,__FILE__,__LINE__,__FUNCTION__)
#define realloc(a,b) t_realloc(a,b,__FILE__,__LINE__,__FUNCTION__)
#define strdup(a) t_strdup(a,__FILE__,__LINE__,__FUNCTION__)
#define free(a) t_free(a)
またいつか使いそうであれば、暇なときに汎用的に整理しておけば良いのです。
プログラミングでは、スマートにやることも良いのですが、いざというときには自力で何とかする力が重要で、どんなに泥臭くても動かないものを動く状態にすることがまず優先というケースも多いものです。もっともプログラミングに限らず、仕事は全体的にそういうものだという気もします。あれこれできない理由を考えている時間があったら、試行錯誤して少しでも進めようという姿勢・行動が大切です。
ということで、何とか面倒な仕事が、夕方から夜にかけてがんばって目処がつき、また明日から他の仕事に取りかかれるということで、ホッとしながら帰りの電車でブログを書いたのでした。