はじめに
私たちのチームではC#でWindowsデスクトップアプリケーションを開発しており、外部アプリケーションから開発しているアプリケーションに対して起動やいくつかの操作を行える機構を実装していました。
そこで、動いているプロセスを取得する際にゾンビプロセスが問題となっていたので備忘録も兼ねてまとめます。
ゾンビプロセスについて
一般にゾンビプロセスというと以下の定義だと思います。
Windowsのゾンビプロセス
Windowsの場合、デスクトップアプリケーションのウィンドウも通知領域のアイコンも消滅し利用者からは終了したように見えるが、システム上はプログラムが未だに実行状態にあるものをゾンビプロセスと呼ぶことがある。画面からは操作要素が消えているため通常の操作で終了させることはできず、タスクマネージャやtaskkillコマンドで強制終了を試みるか、ログオフや再起動を行うことで消滅する。
私達の判断定義としてはタスクマネージャーでは確認できず、コマンドプロンプト上でtasklistを実行して確認できるゴミプロセスであり、killできないプロセスをゾンビプロセスだと判断・定義しました。
tasklistを実行する際は、以下のコマンドで対象アプリケーションに対してフィルタすると見やすいです。(XXXにアプリケーション名)
tasklist /fi "imagename eq XXX.exe"
発生原因ですが、一般的にどのような場合に発生するか調査した限り、例えばMicrosoftのブログではI/O待ちによってドライバから解放されずにプロセスが残るとありますが、これだという原因は特定できていません。
C#アプリケーションでの発生した場合の回避策
C#ではSystem.Diagnostics.Processクラスによってゾンビプロセスも含めたプロセス一覧が取得できるため、私達は以下の判断基準からゾンビプロセスを除外した正常なプロセスのみを取得するようにしました。
以下にSystem.Diagnostics.Processを拡張するコード例を示します。(拡張対象が静的メソッドのため汎用クラスとしての定義です)
/// <summary>
    /// プロセスのユーティリティです。
    /// </summary>
    internal static class ProcessUtil
    {
        #region 公開メソッド
        /// <summary>
        /// プロセス名から起動順に並んだ利用可能なプロセス一覧を取得します。
        /// 利用可能なプロセスとはゾンビプロセスを除外した有効なアプリケーションのプロセスです。
        /// </summary>
        /// <param name="processName">取得するプロセス名。</param>
        /// <returns>利用可能なプロセス一覧を最後に起動したプロセスが先頭に来る順番で並び替え取得する</returns>
        public static IEnumerable<Process> GetAvailableProcesses(string processName)
        {
            // プロセス一覧を取得する
            var processes = Process.GetProcessesByName(processName);
            return processes.Where(IsAliveProcess).OrderByDescending(GetStartTime);
            // アプリケーションのプロセス終了時に稀にゾンビプロセスが残ってしまうため、
            // ゾンビプロセスを除外するために一部のプロパティの値の有無でプロセスを除外する。
            // ここでいうゾンビプロセスとはタスクマネージャーでは確認できないがコマンドプロンプト上で
            // tasklistを実行すると確認できるゴミプロセスである。なお、このプロセスはkillできない。
            bool IsAliveProcess(Process _p)
            {
                try
                {
                    // NonpagedSystemMemorySize64とPagedSystemMemorySize64とはOSが使用するメモリ領域。参考 http://nanoappli.com/blog/archives/2518
                    // Windowsが所有するメモリで、アプリケーションにシステム・サービスを提供するために
                    // 使用されるメモリであり、ここが0ということはゾンビプロセスだと判断する。参考 https://learn.microsoft.com/ja-jp/previous-versions/ff467974(v=msdn.10)?redirectedfrom=MSDN
                    // さらに実際に動作確認したゾンビプロセスはMainWindowTitleが空文字となっていた。
                    // したがって上記のゾンビプロセスの条件を満たなさいプロセスのみを返す。
                    return _p.MainWindowTitle != string.Empty &&
                           _p.NonpagedSystemMemorySize64 != 0 &&
                           _p.PagedSystemMemorySize64 != 0;
                }
                catch
                {
                    // フェールセーフとして万が一ゾンビプロセスがプロパティのアクセス時に例外発生した場合でも除外する
                    return false;
                }
            }
            // 複数のアプリケーションのプロセスが起動中の場合は最後に起動したアプリケーションの順となるようにする
            DateTime GetStartTime(Process _p)
            {
                try
                {
                    return _p.StartTime;
                }
                catch
                {
                    // フェールセーフとして万が一ゾンビプロセスがStartTimeを返せなかった場合でも
                    // 最初に起動したプロセスとして扱い、なるべくゾンビプロセスを返さないようにする
                    return DateTime.MinValue;
                }
            }
        }
        /// <summary>
        /// プロセス名から最後に起動した利用可能なプロセスを取得します。
        /// 利用可能なプロセスとはゾンビプロセスを除外した有効なアプリケーションのプロセスです。
        /// </summary>
        /// <param name="processName">取得するプロセス名。</param>
        /// <returns>最後に起動した利用可能なプロセス</returns>
        public static Process GetAvailableProcess(string processName)
        {
            return GetAvailableProcesses(processName).FirstOrDefault();
        }
        #endregion
    }
ゾンビプロセスは一般的に言われているようにCPUやメモリのリソースを使用しないため、取得されるProcesが持つプロパティの値も明らかにおかしい値となっているものがいくつかあります。
そのなかでもNonpagedSystemMemorySize64とPagedSystemMemorySize64はWindowsが所有するメモリで、アプリケーションにシステム・サービスを提供するために使用されるメモリであり、ここの値が0ということは有効なプロセスではないと判断できます。
さらに実際に動作確認したゾンビプロセスはMainWindowTitleが空文字となっており、開発する上でMainWindowTitleを空文字としてアプリケーションを作成することはありえないため、このプロパティも判定基準として含め、可能な限り正常なプロセスをゾンビプロセスとして誤判定してしまうことを避けます。
参考
