(Mac OS X / LINUX での) 外部コマンドの消費メモリのモニタリング

(Mac OS X での) プロセスの消費メモリの測り方 - ny23の日記でしばらくごにょごにょやってたけど,Mac では rusage / task_info の連携がどこかで腐っており,getrusage/wait3/wait4 などは (>2GB では) まともに動かないため,現時点で root 権限なしに 2GB 以上のメモリを計測することは簡単ではない.task port を親子のプロセス間でやりとりすればできるようだが,中の人的にオススメではないようだこの手順を頑張って解釈した人がいたのでその結果をそのまま拝借して,以下のようにすれば動く.

// run.cc (tested on Mac OS X 10.5/10.6)
#include <mach/mach.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <csignal>

#define SLEEP_NSEC 10000 // 0.01s
// smaller value => fine-grained memory monitoring (overhead increases runtime)

struct mach_send_port_msg {
  mach_msg_header_t          header;
  mach_msg_body_t            body;
  mach_msg_port_descriptor_t task_port;
};

struct mach_recv_port_msg {
  mach_send_port_msg m;
  mach_msg_trailer_t trailer;
};

static int setup_recv_port (mach_port_t *recv_port) {
  mach_port_t   port = MACH_PORT_NULL;
  if (KERN_SUCCESS !=
      mach_port_allocate (mach_task_self (), MACH_PORT_RIGHT_RECEIVE, &port))
    std::fprintf (stderr, "mach_port_allocate failed.\n");
  
  if (KERN_SUCCESS !=
      mach_port_insert_right (mach_task_self (), port, port, MACH_MSG_TYPE_MAKE_SEND))
    std::fprintf (stderr, "mach_port_insert_right failed.\n");
  *recv_port = port;
  return 0;
}

static int send_port (mach_port_t remote_port, mach_port_t port) {
  mach_send_port_msg msg;

  msg.header.msgh_remote_port = remote_port;
  msg.header.msgh_local_port  = MACH_PORT_NULL;
  msg.header.msgh_bits        = MACH_MSGH_BITS (MACH_MSG_TYPE_COPY_SEND, 0) |
                                MACH_MSGH_BITS_COMPLEX;
  msg.header.msgh_size        = sizeof (msg);

  msg.body.msgh_descriptor_count = 1;
  msg.task_port.name             = port;
  msg.task_port.disposition      = MACH_MSG_TYPE_COPY_SEND;
  msg.task_port.type             = MACH_MSG_PORT_DESCRIPTOR;

  if (KERN_SUCCESS != mach_msg_send (&msg.header))
    std::fprintf (stderr, "mach_msg_send failed.\n");

  return 0;
}

static int recv_port (mach_port_t recv_port, mach_port_t *port) {
  mach_recv_port_msg msg;
  
  if (KERN_SUCCESS !=
      mach_msg (&msg.m.header, MACH_RCV_MSG, 0, sizeof (msg), recv_port,
                MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL))
    std::fprintf (stderr, "mach_msg failed.\n");
  
  *port = msg.m.task_port.name;
  return 0;
}

int main (int argc, char **argv) {
  char ** cmd = &argv[1];

  struct timeval start, end;
  task_t         task             = MACH_PORT_NULL;
  mach_port_t    parent_recv_port = MACH_PORT_NULL;
  mach_port_t    child_recv_port  = MACH_PORT_NULL;
  
  // setup port as bootstrap port
  if (setup_recv_port (&parent_recv_port) != 0) return -1;
  if (KERN_SUCCESS !=
      task_set_bootstrap_port (mach_task_self (), parent_recv_port))
    std::fprintf (stderr, "task_set_bootstrap_port failed.\n");
  
  // set timer
  gettimeofday (&start, 0);
  // create new task
  pid_t pid = fork ();
  if (pid < 0)  {
    if (KERN_SUCCESS !=
        mach_port_deallocate (mach_task_self(), parent_recv_port))
      std::fprintf (stderr, "mach_port_deallocate failed.\n");
    std::fprintf (stderr, "cannot fork.\n"); std::exit (1);
  } else if (pid == 0) { // child process
    if (KERN_SUCCESS !=
        task_get_bootstrap_port (mach_task_self (), &parent_recv_port))
      std::fprintf (stderr, "task_get_bootstrap_port failed.\n");
    if (setup_recv_port (&child_recv_port) != 0)              return -1;
    if (send_port (parent_recv_port, mach_task_self ()) != 0) return -1;
    if (send_port (parent_recv_port, child_recv_port)   != 0) return -1;
    if (recv_port (child_recv_port,  &bootstrap_port)   != 0) return -1;
    if (KERN_SUCCESS
        != task_set_bootstrap_port (mach_task_self (), bootstrap_port))
      std::fprintf (stderr, "task_set_bootstrap_port failed.\n");
    execvp (cmd[0], cmd);
    std::fprintf (stderr, "cannot run %s\n", cmd[0]);
    _exit (errno == ENOENT ? 127 : 126);
  }
  // parent process
  std::signal (SIGINT,  SIG_IGN);
  std::signal (SIGQUIT, SIG_IGN);
  if (KERN_SUCCESS !=
      task_set_bootstrap_port (mach_task_self (), bootstrap_port))
    std::fprintf (stderr, "task_set_bootstrap_port failed.\n");
  if (recv_port (parent_recv_port, &task)            != 0) return -1;
  if (recv_port (parent_recv_port, &child_recv_port) != 0) return -1;
  if (send_port (child_recv_port,  bootstrap_port)   != 0) return -1;
  if (KERN_SUCCESS
      != mach_port_deallocate (mach_task_self(), parent_recv_port))
    std::fprintf (stderr, "mach_port_deallocate failed.\n");
  
  // monitor memory
  size_t mem = 0;
  struct task_basic_info t_info;
  mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT;
  do {
    if (KERN_SUCCESS !=
        task_info (task, TASK_BASIC_INFO, (task_info_t) &t_info, &t_info_count))
      break;
    if (mem < t_info.resident_size) mem = t_info.resident_size;
  } while (! usleep (SLEEP_NSEC));
  
  // stop timer
  int status;
  if (waitpid (pid, &status, 0) == -1) // 
    std::fprintf (stderr, "error waiting for child process.\n");
  if ((status & 0xff) == 0x7f)
    std::fprintf (stderr, "Command stopped by signal %d\n", (status >> 8) & 0xff);
  else if ((status & 0xff) != 0)
    std::fprintf (stderr, "Command terminated by signal %d\n", status & 0xff);
  else if ((status >> 8) & 0xff)
    std::fprintf (stderr, "Command exited with non-zero status %d\n", (status >> 8) & 0xff);
  gettimeofday (&end, 0);

  std::fprintf (stderr, "elapsed (real): %.3fs; RSS=%.1fM\n",
                end.tv_sec - start.tv_sec + (end.tv_usec - start.tv_usec) * 1e-6,
                static_cast <double> (mem) / (1024.0 * 1024.0));
  std::signal (SIGINT,  SIG_DFL);
  std::signal (SIGQUIT, SIG_DFL);
  return 0;
}

task_for_pid を呼べないせいでここまで複雑になるか(最初,root 権限でコマンドを実行するソースを公開してしまっていたよ).LINUX では /proc があるので同様のプログラムは簡単に書ける.

#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <csignal>

#include <sys/resource.h>
#define SLEEP_NSEC 500 // 0.0005s

int main (int argc, char **argv) {
  char ** cmd = &argv[1];
  // set timer
  struct timeval start, end;
  gettimeofday (&start, 0);
  // fork
  pid_t pid = fork ();
  if (pid < 0)  {
    std::fprintf (stderr, "cannot fork.\n"); std::exit (1);
  } else if (pid == 0) { // child process
    // must not delete this
    execvp (cmd[0], cmd);
    std::fprintf (stderr, "cannot run %s\n", cmd[0]);
    _exit (errno == ENOENT ? 127 : 126);
  }
  // dislable signals
  signal (SIGINT,  SIG_IGN);
  signal (SIGQUIT, SIG_IGN);
  // parent process
  size_t mem = 0;
  int status;
  char statm[32];
  std::sprintf (&statm[0], "/proc/%d/statm", static_cast <int> (pid));
  do {
    FILE *reader = fopen (statm, "r");
    size_t dummy (0), vm (0);
    std::fscanf (reader, "%ld %ld ", &dummy, &vm); // get resident (see procps)
    if (mem < vm) mem = vm;
    std::fclose (reader);
    if (! vm) break;
  } while (! usleep (SLEEP_NSEC));
  mem *= getpagesize ();
  // stop timer
  if (waitpid (pid, &status, 0) == -1)
    std::fprintf (stderr, "error waiting for child process.\n");
  if ((status & 0xff) == 0x7f)
    std::fprintf (stderr, "Command stopped by signal %d\n", (status >> 8) & 0xff);
  else if ((status & 0xff) != 0)
    std::fprintf (stderr, "Command terminated by signal %d\n", status & 0xff);
  else if ((status >> 8) & 0xff)
    std::fprintf (stderr, "Command exited with non-zero status %d\n", (status >> 8) & 0xff);
  gettimeofday (&end, 0);

  std::fprintf (stderr, "elapsed (real): %.3fs; RSS=%.1fM\n",
                end.tv_sec - start.tv_sec + (end.tv_usec - start.tv_usec) * 1e-6,
                static_cast <double> (mem) / (1024.0 * 1024.0));
  // enable signals
  signal (SIGINT,  SIG_DFL);
  signal (SIGQUIT, SIG_DFL);
  return 0;
}

で,適当にメモリを確保するプログラムを作成.

// mem.cc
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>

int main (int argc, char** argv) {
  
  size_t size = 1;
  char &uni = argv[1][std::strlen (argv[1])-1];
  switch (uni) {
    case 'G': size <<= 10;
    case 'M': size <<= 10;
    case 'K': size <<= 10; uni = '\0';
    default:
      size *= std::strtol (argv[1], NULL, 10);
  }
  std::fprintf (stderr, "allocating memory (%ld)..", size);
  char * mem = new char[size];        // *
  std::fill (&mem[0], &mem[size], 0); // not to optimize out *
  std::fprintf (stderr, "done.\n");
  delete [] mem;
  return 0;
}

実行してみる.run.cc は -m64 をつけてコンパイル(LUNUX 版の方は無くてもソースを少し変えれば動くとは思うが).

# Mac OS X (10.5; 10.6 でも 1G では動作確認)
// > g++ -O2 -march=core2 -m64 -o mem mem.cc
> run mem 5G
allocating memory (5368709120)..done.
elapsed (real): 5.668s; RSS=5120.5M
# GNU/Linux
> run mem 5G
allocating memory (5368709120)..done.
elapsed (real): 5.117s; RSS=5120.4M

時間計測部は GNU time コマンドのソースから拝借しているので,時間の方も time コマンドの real とほぼ同様に使える.GNU time もメモリ計測に対応しているのだけど,OS ごとの情報取得方法の違いに翻弄されている感じなので,その置き換えのつもり.linux 版は /proc//statm にアクセスして resident memory size (procps で RSS として取得されるもの; /proc//status の VmRss と同じもの) を取得./proc (procfs) 周りの話は

を参照.ru_maxrss は hiwater_rss=(VmHWM) と同じだったり).
子プロセスで実行した外部コマンドのメモリ消費は,本来情報の受け渡しをすべき gerusage や wait4 (wait3) がちゃんと情報を伝えない関係で,↑みたいな方法をとらざるを得ないが,自プロセスのメモリ消費を確認する場合は,Mac OS X なら task_info を mach_task_self () (または current_task ())を第一引数にして呼べば良いし,Linux なら /proc//status の VmHWM を読むなどしてメモリ消費の確認は容易.root 権限がいるが,Mac OS X でもMacFUSE-Based Process File System (procfs)をインストールすれば,Linux と同様に /proc 経由で情報を取れると思われる.
[追記; 2011/01/29] シグナルの取り扱いを改善して,segmentation fault や bus error などの異常終了が分かるようにした.