ログ出力について

解決


TNR  2011-10-15 20:16:16  No: 72951  IP: [192.*.*.*]

お世話にになります。
VC++ 6.0 で、自作のログ出力クラスを改善したいのですが、
困っていること(=質問したいこと)が2つあります。

まず現在は、
_tfopen, _fputts, fclose
を使って、1つのファイル(固定ファイル名)にログを出力しています。

クラス化しているのですが、あちこちで利用するため、
Logger * Logger::GetInstance() {
    static Logger logger;
    return &logger;
}
のように、java でいうシングルトンパターン'のような'仕組みで、
ログ出力時にインスタンスを取得し、書き込みメソッドを呼ぶようにしています。
Logger * log = Logger::GetInstance();
log->error(ログ);
log->debug(ログ);



1、EXEが2つ以上起動したときに、
    対象のログファイルに同時に書き込まれるとファイルが
    壊れてしまいます。

    「ファイル  ロック」などで検索しても、
    利用するAPIを見つけることができませんでした。

    複数のプロセスから、安全に操作する方法を教えてください。

2、出力するファイルに制限をつけ、サイズを超えたら
    log1.log
    log2.log
    log3.log
    log1.log <- もとのファイル名に戻る
    log2.log
    ・・・
    のようにローテーションで出力したいと思っています。

    ログ出力時に毎回ファイルサイズを取得することはできるのですが、
    次に出力する先のファイルを特定する方法が思いつきません。

    EXEが2つ以上起動している場合は、
    さらに難しいのではないかと思います(汗)


    ※サイズと書きましたが、サイズが難しいようでしたら、
      行数でも構いません(ログには改行コードが含まれていない前提です)。


宜しくお願いします。

編集 削除
fuku  2011-10-16 04:00:53  No: 72952  IP: [192.*.*.*]

目的によっても解決策が異なりそうなのでいくつか提案します。
また、ログ出力オブジェクトはプロセスの開始から終了まで、
対象のログファイルを開いたままにしていると仮定して回答します。

1.
同じログファイルに書き込みを行うプロセスが複数存在する事は前提ですか?
また、プロセスが複数存在する場合、どのような結果になるべきですか?
●通常、プロセスは1つしか存在せず、複数存在する時はログが壊れなければ良い
  →「CreateFile」WinAPIであればファイルを開く時にファイルの共有モードを指定できます。
    ここで「共有を許可しない」ようにすればファイルを開くことができたプロセスしか書き込めません。
    後発のプロセスのログは保存できなくなりますが、ファイルは壊れません。
    
●プロセスは複数存在する可能性があり、一つのログファイルに保存しなければならない
  →ログファイルに書き込む専用のプロセスを起動する、
    あるいは起動しているプロセスの一つにその役割を任命し、
    全てのログはそのプロセスを介して書き込むようにします。
    「プロセス間通信」について調べてみるといいでしょう。
    多分、それなりに面倒です。
    
●プロセスは複数存在する可能性があり、別々のログファイルに保存しなければならない
  →「CreateFile」WinAPIで「共有を許可しない」でログファイルを開くようにします。
    候補となるログファイル名でログファイルを開こうとしてみて、失敗したら次の名前を試します。
    この手法ではログファイルを書き込もうとしているパスが無効、またはディスク容量がない場合などに
    永久に失敗するパターンが発生する可能性があるので、試す数に上限を設けたり、
    Mutexなどを使って他に干渉するプロセスが起動しているかチェックできる機構を備えるべきでしょう。

2.
ログファイルへの書き込み状態を記録したインデックスファイルのようなものを別に用意し、
そのファイルに「次に書き込むべきファイル」などの情報を残しておくのが簡単かと思います。
複数プロセスが起動した場合に備えるなら、
意図的にこのインデックスファイルを「共有を許可しない」で開きっぱなしにしておき、
後発のプロセスがインデックスファイルにアクセスできないようにするという手もありだと思います。

別のファイルを用意したくなければ各ログファイルの更新日時を使うという手もありそうですが、
この方法だと複数プロセスへの対応や無関係の方法で更新日時が変化した時に弱そうです。

編集 削除
TNR  2011-10-17 20:44:26  No: 72953  IP: [192.*.*.*]

fuku様

返信ありがとうございます。

> 同じログファイルに書き込みを行うプロセスが複数存在する事は前提ですか?

言い忘れてしまいましたが、作っているのは Windows プログラムで、
.exe の形式で提供しております。

.exe が何回起動されるかは、ユーザの問題でして、
複数のプロセスが存在する可能性は充分にあります。

> プロセスが複数存在する場合、どのような結果になるべきですか?

全てのログを正常に書き込みたいです。
ロック解除待ちになる場合は、書き込みが終わるまで待たせて構わないと
思っています。

> ●プロセスは複数存在する可能性があり、一つのログファイルに保存しなければならない

これ↑です。
・・・がログの出力だけで、一つのプロセスを作成するのは
現実的ではないと考えております。


そこで、
「対象のログファイルを開いたままにしていると仮定して」
これを覆せば、もっと簡単になりますでしょうか?

#一般的なアプリのログや log4j などはどうなっているのでしょうか?

私の調べた限りでは、
A、CreateFile を使ってopen/closeを繰り返す。
    (ロック解除を待つことは可能?)
B、カーネルオブジェクトを使って、同期する。
を使えばできるのではないかと考えております。
どちらが推奨(または他にもっとよい方法でも)でしょうか?

2番については、
  log1.log
  log2.log
  log3.log
  とあったときに、
  log1.log を書き込むときに、log2.log を削除、
  log2.log を書き込むときに、log3.log を削除、とすれば
  次に書き込むファイルが分かりそうな気がしています。
  
宜しくお願いします。

編集 削除
gak  2011-10-18 18:15:32  No: 72954  IP: [192.*.*.*]

> 複数のプロセスから、安全に操作する方法を教えてください。
言われているようにファイルの共有モード利用すれば良いと思うが、ログの書き込み要求がなされた順番を維持したいのならミューテックス使ってプロセス間同期をとれば良いかと。
以下 Shift-JIS 文字セットでlogを出力すると仮定してのコード

void log(LPCSTR logtext) {
    HANDLE mutex = ::CreateMutex(NULL, FALSE, _T("適当なオブジェクト名"));
    if (mutex != NULL) {
        ::WaitForSingleObject(mutex, INFINITE); // 別プロセスが処理中なら待機
        HANDLE file = ::CreateFile(_T("c:\\log.log"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        if (file != INVALID_HANDLE_VALUE) {
            ::SetFilePointer(file, 0, NULL, FILE_END);
//          const int filesizeLimit = ファイルサイズ制限値
//          checkSize(file, max(0, filesizeLimit - strlen(logtext) + strlen("\r\n"))); // 詳細後述
            DWORD written;
            ::WriteFile(file, logtext, strlen(logtext), &written, NULL);
            ::WriteFile(file, "\r\n", strlen("\r\n"), &written, NULL); // 改行
            ::CloseHandle(file);
        }
        ::ReleaseMutex(mutex);
    }
}

UIスレッドから呼ぶ場合は WaitForSingleObject ではロック中画面が固まるので MsgWaitForMultipleObjects 辺りに置き換えるべき。

> 2番については、log1.log を書き込むときに、log2.log を削除...
> (ログには改行コードが含まれていない前提です)。
在る程度古いログは自動的に削除してよいのならファイルを分けないやり方も有りじゃね?
例えば制限サイズを越える場合はその分古いログを捨てるとか。

void checkSize(HANDLE file, DWORD limitLength) {
    const DWORD lowLength = ::GetFileSize(file, NULL);
    if (lowLength >= limitLength) { // 制限越えた?
        std::vector<BYTE> v(lowLength + 1);
        ::SetFilePointer(file, 0, NULL, FILE_BEGIN);
        DWORD len;
        ::ReadFile(file, &v[0], lowLength, &len, NULL);
        const unsigned char *cutline = &v[len] - limitLength; // 切捨て境界位置
        v[len] = '\0'; // _mbschr利用のために\0終端付加
        unsigned char *p = p=_mbschr(&v[0], '\n'); // 各ログは"\r\n"(or"\n")で区切られている前提
        for (; p != NULL && p < cutline; p=_mbschr(p+1, '\n')) ;
        ::SetFilePointer(file, 0, NULL, FILE_BEGIN);
        if (p != NULL) {
            ::WriteFile(file, ++p, len - (p - &v[0]), &len, NULL);
        }
        ::SetEndOfFile(file);
    }
}

ファイルを分けるのが仕様としてあるのならば既に挙がっている以外で画期的な方法は思いつかない…
それでも挙げるとすれば、ファイルサイズが溢れた時点で
・log3.log削除、log2.log->log3.logとりネーム、log1.log->log2.logとりネーム、log1.logを初期化してログWrite
として最新ログは常にlog1.logに記録していくとか。これならばログの書き込み対象は常にlog1.logとなるので迷わない。

編集 削除
TNR  2011-10-18 21:16:26  No: 72955  IP: [192.*.*.*]

gakさま

返信ありがとうございます。

Mutexを使ったサンプルとても参考になりました。
::SetFilePointerという関数も初めて知りました。追記するときに便利ですね!

ただ、「ログの書き込み要求がなされた順番」は気にしません。
ちゃんと全てのログが(プロセス単位で正しい順で)でればいいと考えています。

「ファイルの共有モード」を利用するとどうなるのかがよく理解できていない気がしますので、
これから調べなおしてみます。
もしかすると、これだけで仕様が満たせるかもしれません。

> 自動的に削除してよいのならファイルを分けないやり方も有りじゃね?

はい、実は現在はその仕組みになっております。

過去の1Mバイトのログを記録しているのですが、
これを50Mバイトに増やさなくてはなりません。

毎回の読み込みと書き込み量が多くなりますので、改善できないかと思い、
ローテーションの仕組みに変更したいと思っております。

> ファイルを分けるのが仕様としてあるのならば

いえ、ファイル分割は仕様ではありません。

30M〜50Mバイト程度の直近ログを、複数のプロセスが、
各プロセス内で正しい順で、ログファイルに出力できれば、他の方法でも構いません。

> 最新ログは常にlog1.logに記録していくとか

いいアイディアですね!検討させて頂きたいと思います!
どうもありがとうございます。

編集 削除
gak  2011-10-19 18:11:13  No: 72956  IP: [192.*.*.*]

> もしかすると、これだけで仕様が満たせるかもしれません。
前提条件次第では満たせるかもしれないが、プロセス間同期取った方が良いように個人的には思う。

ファイルの共有モードを利用すれば
> EXEが2つ以上起動したときに、対象のログファイルに同時に書き込まれるとファイルが壊れてしまいます。
は回避可能だが実装方法は
> A、CreateFile を使ってopen/closeを繰り返す。
となり効率はよろしくない。また
> (ロック解除を待つことは可能?)
については「否」。for文内でCreateFileを繰り返し、他がログを開いていない時に(運良く)CreateFileを呼べればログ出力処理という形(↓)に多分なるので。

  for (int i=0; i < 最大試行回数; ++i) {
      HANDLE file = ::CreateFile(*, GENERIC_WRITE, FILE_SHARE_READ, *, *, *, *);
      if (file != INVALID_HANDLE_VALUE) {
          // 開けたのでログ出力、ファイルサイズチェック処理等実行
            :
          ::CloseHandle(file);
          break; // 終了
      }
      else { // 他でログファイルが開かれているので開けなかった
          ::Sleep(100); // とりあえず0.1秒待ってから再試行してみる
      }
  }

> ちゃんと全てのログが(プロセス単位で正しい順で)
ログ出力機構を呼ぶスレッドが1つであれば「プロセス単位で正しい順で」は「for文内でCreateFile」だけでも可能だと思う。
ただ、複数スレッドからログ出力が行われる場合は(スレッド間同期でも取らないと)書き込む順番が前後する可能性がある。
# 其々が順番に並んで順次ログファイルアクセス権を取っていくのでは無く、アクセス権が空いた瞬間を狙って皆で一斉に奪い合う形の実装になるので。
# 先に挙げた CreateMutex + WaitForSingleObject だとプロセス間同期と共にスレッド間同期も取ってqueue処理されるので書き込む順番は維持される。

編集 削除
TNR  2011-10-19 19:46:15  No: 72957  IP: [192.*.*.*]

gak様

返信ありがとうございます。解決です!

> (ロック解除を待つことは可能?)
> については「否」。for文内でCreateFileを繰り返し、他がログを開いていない時に(運良く)CreateFileを呼べればログ出力処理

なるほど。非常によく分かりました。
ご推奨通り、プロセス間同期を取りながら出力したいと思います。
(その際には前回教えていただいたMutexでやりたいと思います)

> ログ出力機構を呼ぶスレッドが1つであれば「プロセス単位で正しい順で」は「for文内でCreateFile」だけでも可能だと思う。

情報がまだ足りていませんでした。申し訳ございません。
アプリケーションはシングルスレッドです。

そのため大丈夫ということになると思いますが、上記記載の通りMutexで行うことにしました。


それと、(2)の件なのですが、
> >  log1.log を書き込むときに、log2.log を削除、
> >  log2.log を書き込むときに、log3.log を削除、とすれば
> >  次に書き込むファイルが分かりそうな気がしています。
という私の案はNGでした。

最初にログファイルが一つもないときに、
log1.log に1行書き、次にlog2.log に1行書き、という結果になってしまいました。

というわけで、こちらも教えていただいた
> ・log3.log削除、log2.log->log3.logとりネーム、log1.log->log2.logとりネーム、log1.logを初期化してログWrite
この方法でやることにしました。

この方法をとっているサンプルが
ttp://d.hatena.ne.jp/FunnyBunnyDizzy/20081003/1223038421
こちらにありましたので、
これを参考にしたいと思っています。

gak様、fuku様、どうもありがとうございました!

編集 削除
gak  2011-10-20 10:01:29  No: 72958  IP: [192.*.*.*]

> 前提条件次第では満たせるかもしれないが、プロセス間同期取った方が良いように個人的には思う。
今更だけどゴメン。気になったので少し言い直しておく。

「”複数プロセス/スレッドからのログファイルへのアクセスが高頻度で発生する”、若しくは”精度/信頼性を重視する”ならば
同期取った方が良いように個人的には思う」


常態で複数プロセスからのログファイルへのアクセスが起きると思い込んでいた。スレを見直してみると可能性があるというレベルだったのね。
そうであればプロセス間同期までは必要無いかもしれない(在ってマイナスにはならないとは思うけど)

編集 削除
TNR  2011-10-20 23:14:37  No: 72959  IP: [192.*.*.*]

gak様

ご丁寧にありがとうございます。

> 「”複数プロセス/スレッドからのログファイルへのアクセスが高頻度で発生する”、若しくは”精度/信頼性を重視する”ならば
> 同期取った方が良いように個人的には思う」

はい、承知致しました。
既にその方法で実装し、現在テスト中です!

「可能性があるというレベル」というよりは
可能性が充分に考えられるといった感じですので、同期することに致しました。

とても助かりました m(_ _)m

編集 削除