Информация о верхней отметке ОЗУ для процесса уже собрана ядром (из man proc
):
/proc/[pid]/status
Provides much of the information in /proc/[pid]/stat and /proc/[pid]/statm in a format that's easier for humans to parse.
(...)
* VmHWM: Peak resident set size ("high water mark").
(...)
Сложность в том, что это значение следует прочитать за мгновение до завершения процесса.
Я пробовал разные подходы (подробнее об этом в конце ответа), и тот, который работал для меня, был реализацией в C:
logmemory
вызывает fork()
для создания дочернего процесса.
Дочерний процесс вызывает ptrace()
так что родительский процесс (который является logmemory
) уведомляется каждый раз, когда дочерний процесс выполняет системный вызов.
Дочерний процесс использует execvp()
для запуска mycmd
.
logmemory
терпеливо ждет уведомления. В этом случае он проверяет, вызвал ли mycmd
exit_group
. Если это так, он читает /proc/<pid>/status
, копирует значения в mem.log
и отсоединяет от дочернего. В противном случае logmemory
позволяет mycmd
продолжить работу и ожидает следующего уведомления.
Недостатком является то, что ptrace()
замедляет отслеживаемую программу, ниже я приведу некоторые сравнения.
Эта версия logmemory
не только регистрирует VmHWM
но также:
VmPeak
(пиковый размер виртуальной памяти, который включает в себя весь код, данные и общие библиотеки, а также страницы, которые были выгружены, и страницы, которые были отображены, но не использовались)
отметка времени
имя команды и аргументы
Это код, который, безусловно, можно улучшить - я не обладаю достаточными знаниями в C. Он работает, как и предполагалось (протестировано на 32-битной Ubuntu 12.04 и 64-битной SuSE Linux Enterprise Server 10 SP4):
// logmemory.c
#include <stdio.h>
#include <sys/ptrace.h>
#include <unistd.h>
#include <syscall.h>
#include <sys/reg.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define STRINGLENGTH 2048
int main(int argc, char **argv)
{
pid_t child_pid;
long syscall;
int status, index;
FILE *statusfile, *logfile;
char opt, statusfile_path[STRINGLENGTH], line[STRINGLENGTH], command[STRINGLENGTH], logfile_path[STRINGLENGTH] = "";
time_t now;
extern char *optarg;
extern int optind;
// Error checking
if (argc == 1) {
printf("Error: program to execute is missing. Exiting...\n");
return 0;
}
// Get options
while ((opt = getopt (argc, argv, "+o:")) != -1)
switch (opt) {
case 'o':
strncpy(logfile_path, optarg, 2048);
break;
case ':':
fprintf (stderr, "Aborting: argument for option -o is missing\n");
return 1;
case '?':
fprintf (stderr, "Aborting: only valid option is -o\n");
return 1;
}
// More error checking
if (!strcmp(logfile_path, "")) {
fprintf(stderr, "Error: log filename can't be empty\n");
return 1;
}
child_pid = fork();
// The child process executes this:
if (child_pid == 0) {
// Trace child process:
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
// Execute command using $PATH
execvp(argv[optind], (char * const *)(argv+optind));
// The parent process executes this:
} else {
// Loop until child process terminates
do {
// Set ptrace to stop when syscall is executed
ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL);
wait(&status);
// Get syscall number
syscall = ptrace(PTRACE_PEEKUSER, child_pid,
#ifdef __i386__
4 * ORIG_EAX,
#else
8 * ORIG_RAX,
#endif
NULL);
} while (syscall != SYS_exit_group);
// Construct path to status file and check whether status and log file can be opened
snprintf(statusfile_path, STRINGLENGTH, "/proc/%d/status", child_pid);
if ( !(logfile = fopen(logfile_path, "a+")) || !(statusfile = fopen(statusfile_path, "r")) ) {
ptrace(PTRACE_DETACH, child_pid, NULL, NULL);
return 1;
}
// Copy timestamp and command to logfile
now = time(NULL);
fprintf(logfile, "Date: %sCmd: ", asctime(localtime(&now)));
for (index = optind; index < argc; index++)
fprintf(logfile, " %s", argv[index]);
fprintf(logfile, "\n");
// Read status file line by line and copy lines containing VmPeak and VmHWM to logfile
while (fgets(line, STRINGLENGTH, statusfile)) {
if (strstr(line,"VmPeak") || strstr(line,"VmHWM"))
fprintf(logfile, "%s", line);
}
fprintf(logfile, "\n");
// Close files
fclose(statusfile);
fclose(logfile);
// Detach from child process
ptrace(PTRACE_DETACH, child_pid, NULL, NULL);
}
return 0;
}
Сохраните его как logmemory.c
и скомпилируйте так:
$ gcc logmemory.c -o logmemory
Запустите это так:
$ ./logmemory
Error: program to execute is missing. Exiting...
$ ./logmemory -o mem.log ls -l
(...)
$ ./logmemory -o mem.log free
total used free shared buffers cached
Mem: 1025144 760660 264484 0 6644 143980
-/+ buffers/cache: 610036 415108
Swap: 1046524 544228 502296
$ ./logmemory -o mem.log find /tmp -name \*txt
(...)
$ cat mem.log
Date: Mon Feb 11 21:17:55 2013
Cmd: ls -l
VmPeak: 5004 kB
VmHWM: 1284 kB
Date: Mon Feb 11 21:18:01 2013
Cmd: free
VmPeak: 2288 kB
VmHWM: 448 kB
Date: Mon Feb 11 21:18:26 2013
Cmd: find /tmp -name *txt
VmPeak: 4700 kB
VmHWM: 908 kB
Я написал эту программу на C для проверки logmemory
:
// bigmalloc.c
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define ITERATIONS 200
int main(int argc, char **argv)
{
int i=0;
for (i=0; i<ITERATIONS; i++) {
void *m = malloc(1024*1024);
memset(m,0,1024*1024);
}
return 0;
}
Скомпилируйте как обычно и запустите его внутри logmemory
:
$ gcc bigmalloc.c -o bigmalloc
$ ./logmemory -o mem.log ./bigmalloc
$ tail mem.log
Date: Mon Feb 11 21:26:01 2013
Cmd: ./bigmalloc
VmPeak: 207604 kB
VmHWM: 205932 kB
который правильно сообщает о 200 МБ.
Как примечание: time
(по крайней мере, в Ubuntu 12.04) неожиданно выдает значение, которое сильно отличается от того, что сообщает ядро:
$ /usr/bin/time --format %M ./bigmalloc
823872
где M
(от man time
):
M Maximum resident set size of the process during its lifetime, in Kilobytes.
Как упомянуто выше, это приводит к цене, потому что logmemory
замедляет выполнение отслеживаемой программы, например:
$ time ./logmemory -o mem.log ./bigmalloc
real 0m0.288s
user 0m0.000s
sys 0m0.004s
$ time ./bigmalloc
real 0m0.104s
user 0m0.008s
sys 0m0.092s
$ time find /var -name \*log
(...)
real 0m0.036s
user 0m0.000s
sys 0m0.032s
$ time ./logmemory -o mem.log find /var -name \*log
(...)
real 0m0.124s
user 0m0.000s
sys 0m0.052s
Другие подходы, которые я (безуспешно) попробовал, были:
Сценарий оболочки, который создает фоновый процесс для чтения /proc/<pid>/status
во время работы mycmd
.
Программа AC, которая разветвляет и mycmd
но делает паузу, пока ребенок не станет зомби, поэтому избегает ptrace
и накладных расходов, которые он создает. Хорошая идея, подумал я, к сожалению, VmHWM
и VmPeak
больше не доступны из /proc/<pid>/status
для зомби.