3流プログラマのメモ書き

元開発職→社内SE→派遣で営業支援→開発戻り浦島太郎状態の三流プログラマのIT技術メモ書き。 このメモが忘れっぽい自分とググってきた技術者の役に立ってくれれば幸いです。

(.Net)ターミナルサーバでユーザのプロセスのメモリ使用量を取りたい。

WindowsServer2003上でターミナルサーバを運用してますが、どうやらメモリを使いすぎているユーザがいるようです。 それで、数日間どのユーザがどのプロセスでメモリを使いすぎているのか経過調査を行うことにしました。

当初はパフォーマンスログで取ろうかなと思っていたんですが、パフォーマンスログではそのプロセスを使っているのがどのユーザなのかがわかりません。

仕方が無いので .Net Framework で現在のプロセス情報を取得し、ユーザと紐付けてCSVに落とすアプリケーションを作ることにしました。 それをOSのタスクスケジューラーに仕込んで、一定間隔で動かす運用です。

CSVには全プロセスの情報と、ユーザ単位でメモリ使用量を集計した情報を別々に出力します。

現在動いているプロセスの情報は System.Diagnostics.Process.GetProcesses() で取れるんですが、そのプロセスがどのユーザがオーナーとなっているかがわかりません。 ココは(.Net)現在のユーザが起動した特定のプロセスを終了するで書いた WMI を使った方法を取ることにしました。

コードしては以下のような感じです。(C# .NetFramework2.0)

try{
    //ユーザとプロセスIDを取得。(WMI使用)
    ManagementScope scope = new ManagementScope("\\\\.\\ROOT\\CIMV2");

    //プロセス情報取得
    System.Diagnostics.Process[] ps = System.Diagnostics.Process.GetProcesses();

    scope.Connect();
    ObjectQuery query = new ObjectQuery(@"SELECT * FROM Win32_Process");
    ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query);
    ManagementObjectCollection col = searcher.Get();

    //プロセスIDとユーザ名のハッシュテーブル[プロセスID,ユーザ名]
    Hashtable htPrcUsrMap = new Hashtable();
    foreach (ManagementObject o in col){
        string pid = o["ProcessId"].ToString();

        Object[] UserInfo = new object[2];
        //見つからない時があるので、try - catch
        try{
            o.InvokeMethod("GetOwner", UserInfo);
        }catch (Exception){
            UserInfo[0] = "unkwon";
        }

        string UserName = (string)UserInfo[0]; //実行ユーザ名称取得 
        //string DomainName = (string)UserInfo[1]; //実行ユーザのドメイン名称取得 

        //配列にプロセスIDとユーザ名格納
        htPrcUsrMap.Add(pid, UserName);
    }

    //現在日時取得
    DateTime dt = DateTime.Now;
    //データを格納するためのテーブルインスタンス作成
    DataSet1.tblProcessDataDataTable tbl = new DataSet1.tblProcessDataDataTable();

    //出力ファイル名
    string strExportFile_Process = @"C:\PerfLogs\processCounter_Process.csv";
    string strExportFile_User = @"C:\PerfLogs\processCounter_User.csv";

    StringBuilder strBld_Process = new StringBuilder();

    foreach (System.Diagnostics.Process p in ps){
        try{
            DataSet1.tblProcessDataRow row = tbl.NewtblProcessDataRow();

            //タイムスタンプ出力(日時分)
            string timestamp = dt.ToString("yyyy-MM-dd HH:mm:00");
            strBld_Process.Append(timestamp);
            strBld_Process.Append(",");
            row.timestamp = timestamp;

            //ユーザ名出力
            string user = (string)htPrcUsrMap[p.Id.ToString()];
            strBld_Process.Append(user);
            strBld_Process.Append(",");
            row.user = user;

            //プロセスID
            string processId = p.Id.ToString();
            strBld_Process.Append(processId);
            strBld_Process.Append(",");
            row.process_id = processId;

            //プロセス名
            string processName = p.ProcessName;
            strBld_Process.Append(processName);
            strBld_Process.Append(",");
            row.process_name = processName;

            //CPU使用率(未実装。とりあえず0にしておく)
            double cpuPercent = 0;
            strBld_Process.Append(cpuPercent);
            strBld_Process.Append(",");
            row.cpu = cpuPercent;

            //CPU時間
            TimeSpan cpuTime = p.TotalProcessorTime;
            strBld_Process.Append(cpuTime.TotalSeconds);
            strBld_Process.Append(",");
            row.cpu_time = (decimal)cpuTime.TotalSeconds;

            //workingset(物理メモリ使用量)
            long workingset = p.WorkingSet64;
            strBld_Process.Append(workingset);
            strBld_Process.Append(",");
            row.workingset = workingset;

            //PrivateMemory(物理メモリ+スワップ使用量)
            long privateMemory = p.PrivateMemorySize64;
            strBld_Process.Append(privateMemory);
            strBld_Process.Append(",");
            row.privatememorysize = privateMemory;

            //最大workingset
            long perkworkingset = p.PeakWorkingSet64;
            strBld_Process.Append(perkworkingset);
            strBld_Process.Append(",");
            row.peak_workingset = perkworkingset;

            //プロセスパス
            string processPath = p.MainModule.FileName;
            strBld_Process.Append(processPath);
            strBld_Process.Append(",");
            row.process_path = processPath;

            /*
            //メインウィンドウキャプション(実行ユーザでしか出ない)
            string windowCaption = p.MainWindowTitle;
            strBld_Process.Append(windowCaption);
            strBld_Process.Append(",");
            */

            strBld_Process.Append(Environment.NewLine);
            tbl.AddtblProcessDataRow(row);
        }catch (Exception ex){
            Console.WriteLine("エラー: {0}", ex.Message);
            strBld_Process.Append(Environment.NewLine);
        }
    }

    //ユーザ毎の統計データ生成
    //重複を除去するため DataView を使う
    DataView vw = new DataView(tbl);
    //重複除去を第二引数に指定。第三引数で一意とすべき列を指定。(複数列でも可能)
    DataTable tblRes = vw.ToTable("DistinctTable", true, new string[] { "user" });
    //合計の列を追加
    tblRes.Columns.Add("sum_cpu", Type.GetType("System.Double"));
    tblRes.Columns.Add("sum_workingset", Type.GetType("System.Int64"));
    tblRes.Columns.Add("sum_privatememorysize", Type.GetType("System.Int64"));
    tblRes.Columns.Add("sum_processcount", Type.GetType("System.Int64"));
    tblRes.Columns.Add("sum_cputime", Type.GetType("System.Decimal"));

    //重複除いたDataTableをループし、元のDataTableから集計値を求める
    foreach (DataRow row in tblRes.Rows) {
        row["sum_cpu"] = tbl.Compute("SUM(cpu)", "user = '" + row["user"] + "'");
        row["sum_cputime"] = tbl.Compute("SUM(cpu_time)", "user = '" + row["user"] + "'");
        row["sum_workingset"] = tbl.Compute("SUM(workingset)", "user = '" + row["user"] + "'");
        row["sum_privatememorysize"] = tbl.Compute("SUM(privatememorysize)", "user = '" + row["user"] + "'");
        row["sum_processcount"] = tbl.Compute("Count(user)", "user = '" + row["user"] + "'");
    }

    //sum_workingset の降順でソートをかける
    DataRow[] srtRows = (DataRow[])tblRes.Select("" , "sum_workingset DESC").Clone();
    DataTable tblSrt = new DataTable();
    tblSrt = tblRes.Clone();
    foreach (DataRow row in srtRows){
        tblSrt.ImportRow(row);
    }

    //テキスト生成
    StringBuilder strBld_User = new StringBuilder();
    strBld_User = new StringBuilder();
    foreach (DataRow row in tblSrt.Rows){
        strBld_User.Append(dt.ToString("yyyy-MM-dd HH:mm:00"));
        strBld_User.Append(",");
        strBld_User.Append(row["user"]);
        strBld_User.Append(",");
        strBld_User.Append(row["sum_cpu"]);
        strBld_User.Append(",");
        strBld_User.Append(row["sum_cputime"]);
        strBld_User.Append(",");
        strBld_User.Append(row["sum_workingset"]);
        strBld_User.Append(",");
        strBld_User.Append(row["sum_privatememorysize"]);
        strBld_User.Append(",");
        strBld_User.Append(row["sum_processcount"]);
        strBld_User.Append(",");
        strBld_User.Append(Environment.NewLine);
    }

    //ファイル出力
    System.Text.Encoding enc = System.Text.Encoding.GetEncoding("shift_jis");
    if (!File.Exists(strExportFile_Process)){
        string strHead = "日時,ユーザ,プロセスID,プロセス名,CPU使用率(未実装)"
            + ",CPU時間,物理メモリ使用量,物理+スワップ使用量,最大物理メモリ使用量,プロセスパス";//,ウィンドウ名";
        File.AppendAllText(strExportFile_Process, strHead + Environment.NewLine, enc);
    }
    if (!File.Exists(strExportFile_User)){
        string strHead = "日時,ユーザ,CPU使用率合計(未実装)"
            + ",CPU時間合計(秒),物理メモリ使用量合計,物理+スワップ使用量合計,プロセス数";
        File.AppendAllText(strExportFile_User, strHead + Environment.NewLine, enc);
    }
    File.AppendAllText(strExportFile_Process, strBld_Process.ToString(), enc);
    File.AppendAllText(strExportFile_User, strBld_User.ToString(), enc);

}catch (Exception ex){
    File.AppendAllText(@"C:\PerfLogs\err.txt" , DateTime.Now.ToString() + " " +  ex.Message + " Trace:" + ex.StackTrace  + Environment.NewLine);

}

ユーザの統計に使う DataTable は以下のような構成にしてます。

データセット名:DataSet
テーブル名:tblProcessData
列:timestamp :System.String
列:user :System.String
列:process_id :System.String
列:process_name :System.String
列:cpu :System.Double
列:workingset :System.Int64
列:privatememorysize :System.Int64
列:peak_workingset :System.Int64
列:process_path :System.String
列:cpu_time :System.Decimal

実際に動かすと、WMIを使ってプロセスとユーザ情報を取得する部分が非常に時間がかかります。 60ユーザくらいで800-900くらいのプロセスが動いている状態で、数分はかかります。 また、この部分、CPUも結構食うようで1コア専有しちゃうんですよね。

WMI以外にユーザとプロセスの情報を取る方法が知りたいと思う今日この頃です。

参考: MSDN:Process メンバ (System.Diagnostics).aspx)

VB/C# 実行中プロセスの各種情報を取得

【C#】プロセス実行ユーザ名称の取得API - Insider.NET - @IT