Опубликован: 15.06.2004 | Доступ: свободный | Студентов: 2557 / 712 | Оценка: 4.35 / 3.96 | Длительность: 27:47:00
ISBN: 978-5-9556-0011-6
Лекция 12:

Время и работа с ним

Опрос показаний часов процессорного времени

Базовым средством для работы с часами процессорного времени является функция clock() (см. листинг 12.15). Она возвращает в качестве результата процессорное время, затраченное процессом с некоего момента, зависящего от реализации и связанного только с его (процесса) запуском. В случае ошибки результат равен ( clock_t ) (-1).

#include <time.h>
clock_t clock (void);
Листинг 12.15. Описание функции clock().

Чтобы перевести время, возвращаемое функцией clock(), в секунды, его следует поделить на константу CLOCKS_PER_SEC, которая определена в заголовочном файле <time.h> равной одному миллиону, хотя разрешающая способность часов процессорного времени в конкретной реализации может и отличаться от микросекунды.

Отметим, что если значения типа clock_t представлены как 32-разрядные целые со знаком, то менее чем через 36 минут ( процессорного времени ) наступит переполнение.

В листинге 12.16 показан пример использования функции clock(). Поскольку начало отсчета для часов процессорного времени не специфицировано, их показания опрашиваются в начале и конце измеряемого промежутка, а результатом измерения затраченного процессорного времени служит разность этих показаний.

#include <stdio.h>
#include <time.h>

int main (void) {
time_t st;
clock_t ct;
double s = 0;
double d = 1;
int i;

fprintf (stderr, "Начало выполнения цикла\n");
(void) time (&st);
ct = clock;
for (i = 1; i <= 100000000; i++) {
    s += d / i;
    d = -d;
}
fprintf (stderr, "Процессорное время выполнения цикла: %g сек.\n",
            (double) (clock - ct) / CLOCKS_PER_SEC);
fprintf (stderr, "Астрономическое время выполнения цикла: %g сек.\n",
            difftime (time (NULL), st));

return (0);
}
Листинг 12.16. Пример программы, использующей функции опроса показаний часов реального и процессорного времени.

Возможные результаты работы этой программы показаны в листинге 12.17. Процессорное время получилось больше астрономического из-за ошибок округления.

Начало выполнения цикла
Процессорное время выполнения цикла: 15.19 сек.
Астрономическое время выполнения цикла: 15 сек.
Листинг 12.17. Возможные результаты работы программы, использующей функции опроса показаний часов реального и процессорного времени.

Для измерения времени выполнения простой команды можно воспользоваться служебной программой time (не входящей, правда, в обязательную часть стандарта POSIX-2001 ):

time [-p] команда [аргумент ...]

Она выдает в стандартный протокол астрономическое и процессорное время, прошедшее от запуска до завершения команды с указанными аргументами. Если задана опция -p, время выдается в секундах в виде вещественных чисел. Процессорное время подразделяется на пользовательское (затраченное самой командой) и системное (ушедшее на оказание системных услуг команде). Если предположить, что выполнимый файл приведенной выше программы называется my_tcex, то командная строка

time -p my_tcex

выдаст в стандартный протокол примерно следующее (см. листинг 12.18):

Начало выполнения цикла
Процессорное время выполнения цикла: 15.2 сек.
Астрономическое время выполнения цикла: 15 сек.
real 15.20
user 15.20
sys 0.00
Листинг 12.18. Возможные результаты измерения времени работы программы, использующей функции опроса показаний часов реального и процессорного времени.

Если нужно измерить время выполнения конвейера или списка команд, их можно поместить в файл, превратив тем самым в простую команду, или воспользоваться конструкцией вида

time sh -c 'составная команда'

Оружием более крупного калибра является специальная встроенная в shell команда

times

Она выдает на стандартный вывод процессорное время, затраченное командным интерпретатором и порожденными им процессами. Например, после выполнения команд

my_tcex & my_tcex
times
на стандартный вывод может быть выдано:
0m0.010s 0m0.000s
0m30.410s 0m0.000s

Можно видеть, что сам командный интерпретатор практически не занимал процессор, а два порожденных им процесса в сумме потребили чуть меньше тридцати с половиной секунд ; время системных услуг в обоих случаях оказалось пренебрежимо малым.

Реализация описанных выше утилит time и times опирается на функцию times() (см. листинг 12.19). Она опрашивает данные о времени выполнения вызывающего процесса и порожденных процессов, завершение которых ожидалось с помощью функций wait() или waitpid(). В отличие от clock(), функция times() измеряет время в тактах часов, и переполнение 32-разрядного представления значений типа clock_t происходит не через полчаса, а примерно в течение года (если секунда делится на 60 - 100 тактов). Соответственно, для перевода результатов работы times() в секунды их нужно делить на sysconf ( _SC_CLK_TCK ), а не на CLOCKS_PER_SEC.

#include <sys/times.h>
clock_t times (struct tms *buffer);
Листинг 12.19. Описание функции times().

В качестве результата функция times() возвращает астрономическое время, прошедшее от некоторого момента в прошлом (например, от загрузки системы), но основные, содержательные данные, относящиеся к вызывающему процессу и его потомкам, помещаются в структуру типа tms, которая описана в заголовочном файле <sys/times.h> и, согласно стандарту POSIX-2001, должна содержать по крайней мере следующие поля:

clock_t tms_utime;  
/* Процессорное время, затраченное */
/* вызывающим процессом            */

clock_t tms_stime;  
/* Процессорное время, затраченное системой */
/* на обслуживание вызывающего процесса     */

clock_t tms_cutime; 
/* Процессорное время, затраченное 
/*завершившимися порожденными процессами    */

clock_t tms_cstime; 
/* Процессорное время, затраченное системой */
/* на обслуживание завершившихся            */
/* порожденных процессов                    */

Значения времени завершившихся порожденных процессов ( tms_utime и tms_cutime, а также tms_stime и tms_cstime ) добавляются, соответственно, к элементам tms_cutime и tms_cstime структуры родительского процесса при возврате из функций wait() или waitpid(). Это происходит рекурсивно: если порожденный процесс ожидал завершения своих потомков, то в упомянутых элементах структуры накопятся данные о времени выполнения поддерева процессов.

В качестве примера использования функции times() измерим время работы рассматривавшейся ранее программы, копирующей строки со стандартного ввода на стандартный вывод через сокеты адресного семейства AF_UNIX с помощью порожденного процесса (см. листинг 12.20).

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Программа копирует строки со стандартного ввода на стандартный вывод, */
/* "прокачивая" их через пару сокетов.                                   */
/* Используются функции ввода/вывода нижнего уровня.                     */
/* Измеряется астрономическое и процессорное время                       */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/times.h>

#define MY_PROMPT       "Вводите строки\n"
#define MY_MSG          "Вы ввели: "

int main (void) {
 int sds [2];
 char buf [1];
 int new_line = 1;     /* Признак того, что надо выдать сообщение MY_MSG   */
                              /* перед отображением очередной строки              */
 clock_t st_ct;        /* Данные об астрономическом и процессорном времени */
 struct tms st_cpt;    /* в начале работы программы                        */
 clock_t en_ct;        /* Данные об астрономическом и процессорном времени */
 struct tms en_cpt;    /* в конце работы программы                         */
 long tck_p_sec;       /* Количество тактов часов в секунде                */

 /* Опросим данные о времени начала выполнения */
 if ((st_ct = times (&st_cpt)) == (clock_t) (-1)) {
    perror ("TIMES-1");
    return (1);
 }

 /* Создадим пару соединенных безымянных сокетов */
 if (socketpair (AF_UNIX, SOCK_STREAM, 0, sds) < 0) {
    perror ("SOCKETPAIR");
    return (2);
 }

 switch (fork ()) {
    case -1:
      perror ("FORK");
      return (3);
    case 0:
      /* Чтение из сокета sds [0] и выдачу на стандартный вывод */
      /* реализуем в порожденном процессе                       */
      while (read (sds [0], buf, 1) == 1) {
    if (write (STDOUT_FILENO, buf, 1) != 1) {
            perror ("WRITE TO STDOUT");
      break;
    }
      }
      exit (0);
 }

 /* Чтение со стандартного ввода и запись в сокет sds [1] */
 /* возложим на родительский процесс                      */
 if (write (sds [1], MY_PROMPT, sizeof (MY_PROMPT) - 1) !=
                                          sizeof (MY_PROMPT) - 1) {
    perror ("WRITE TO SOCKET-1");
 }

 while (read (STDIN_FILENO, buf, 1) == 1) {
    /* Перед отображением очередной строки */
    /* нужно выдать сообщение MY_MSG       */
    if (new_line) {
      if (write (sds [1], MY_MSG, sizeof (MY_MSG) - 1) != sizeof (MY_MSG) - 1) {
         perror ("WRITE TO SOCKET-2");
         break;
      }
    }
    if (write (sds [1], buf, 1) != 1) {
      perror ("WRITE TO SOCKET-3");
      break;
    }
    new_line = (buf [0] == '\n');
 }
 shutdown (sds [1], SHUT_WR);

 (void) wait (NULL);

 /* Опросим данные о времени конца выполнения, */
 /* вычислим и выдадим результаты измерений    */
 if ((en_ct = times (&en_cpt)) == (clock_t) (-1)) {
    perror ("TIMES-2");
    return (4);
 }

 tck_p_sec = sysconf (_SC_CLK_TCK);
 fprintf (stderr, "Тактов в секунде: %ld\n", tck_p_sec);
 fprintf (stderr, "Астрономическое время работы программы: %g сек.\n",
             (double) (en_ct - st_ct) / tck_p_sec);
 fprintf (stderr, "Процессорное время, затраченное процессом: %g сек.\n",
             (double) (en_cpt.tms_utime - st_cpt.tms_utime) / tck_p_sec);
 fprintf (stderr, "Процессорное время, затраченное системой: %g сек.\n",
             (double) (en_cpt.tms_stime - st_cpt.tms_stime) / tck_p_sec);
 fprintf (stderr, "Аналогичные данные для порожденных процессов:\n");
 fprintf (stderr, "%g сек.\n%g сек.\n",
             (double) (en_cpt.tms_cutime - st_cpt.tms_cutime) / tck_p_sec,
             (double) (en_cpt.tms_cstime - st_cpt.tms_cstime) / tck_p_sec);

 return (0);
}
Листинг 12.20. Пример программы, использующей функцию times().

Если перенаправить стандартный ввод в какой-либо текстовый файл заметных размеров, а стандартный вывод - в другой файл, можно получить результаты, подобные тем, что показаны в листинге 12.21.

Тактов в секунде: 100
Астрономическое время работы программы: 1.19 сек.
Процессорное время, затраченное процессом: 0.02 сек.
Процессорное время, затраченное системой: 0.08 сек. 
Аналогичные данные для порожденных процессов:
0.09 сек.
1 сек.
Листинг 12.21. Возможные результаты работы программы, использующей функцию times().

Прямое манипулирование часами процессорного времени возможно после обращения к функции clock_getcpuclockid() (см. листинг 12.22), позволяющей выяснить их идентификатор. Оговоримся, однако, что эта функция не входит в обязательную часть стандарта POSIX-2001 (она относится к продвинутым средствам реального времени).

#include <time.h>
int clock_getcpuclockid 
      (pid_t pid, clockid_t *clock_id);
Листинг 12.22. Описание функции clock_getcpuclockid().

Идентификатор часов процессорного времени, обслуживающих заданный аргументом pid процесс, возвращается по адресу, специфицированному аргументом clock_id. Если значение pid равно нулю, имеется в виду вызывающий процесс. Признаком успешного завершения является нулевой результат; в противном случае возвращается код ошибки.

В листинге 12.23 показан пример употребления функции clock_getcpuclockid() и манипулирования часами процессорного времени. Возможные результаты выполнения этой программы - в листинге 12.24.

#include <stdio.h>
#include <time.h>
#include <sys/types.h>
#include <errno.h>

int main (void) {
 clockid_t clk_id;     /* Идентификатор часов процессорного времени */
 struct timespec tmsp;
 double s = 0;
 double d = 1;
 int i;

 if ((errno = clock_getcpuclockid ((pid_t) 0, &clk_id)) != 0) {
    perror ("CLOCK_GETCPUCLOCKID");
    return (1);
 }

 if (clock_getres (clk_id, &tmsp) == -1) {
    perror ("CLOCK_GETRES");
    return (2);
 }
 printf ("Разрешающая способность часов процессорного времени: %ld нсек.\n",
      tmsp.tv_nsec);

 /* Измерим процессорное время выполнения цикла. */
 fprintf (stderr, "Начало выполнения цикла\n");
 tmsp.tv_sec = tmsp.tv_nsec = 0;
 if (clock_settime (clk_id, &tmsp) == -1) {
    perror ("CLOCK_SETTIME");
    return (3);
 }
 for (i = 1; i <= 100000000; i++) {
    s += d / i;
    d = -d;
 }
 if (clock_gettime (clk_id, &tmsp) == -1) {
    perror ("CLOCK_GETTIME");
    return (4);
 }
 fprintf (stderr, "Процессорное время выполнения цикла: %ld сек. %ld нсек.\n",
      tmsp.tv_sec, tmsp.tv_nsec);

 return (0);
}
Листинг 12.23. Пример программы, манипулирующей часами процессорного времени.
Разрешающая способность часов процессорного времени: 2 нсек.
Начало выполнения цикла
Процессорное время выполнения цикла: 15 сек. 350313054 нсек.
Листинг 12.24. Возможные результаты работы программы, манипулирующей часами процессорного времени.

Обратим внимание на два момента. Перед началом измеряемого участка программы на часах процессорного времени были установлены нулевые показания, что, разумеется, не влияет на приоритет процесса и его планирование. Конечно, можно было обойтись без переустановки, запомнив показания часов в начале и конце измеряемого промежутка и произведя затем их вычитание, но технически это чуть сложнее; кроме того, нам хотелось применить функцию clock_settime(). Второй заслуживающий отдельного упоминания момент состоит в отсутствии необходимости выяснять идентификатор часов процессорного времени вызывающего процесса с помощью функции clock_getcpuclockid(): можно воспользоваться именованной константой CLOCK_PROCESS_CPUTIME_ID.

Антон Коновалов
Антон Коновалов

В настоящее время актуальный стандарт - это POSIX 2008 и его дополнение POSIX 1003.13
Планируется ли актуализация материалов данного очень полезного курса?