Показаны сообщения с ярлыком linux. Показать все сообщения
Показаны сообщения с ярлыком linux. Показать все сообщения

понедельник, 16 марта 2015 г.

Мастеринг связанных процессов в linux

Это развитие темы, поднятой в этой статье. Напомню, в ней был представлен способ гарантированного перезапуска сбоящего приложения, основанный на простой модели процессов мастер + воркер. Единственной задачей главного процесса (мастера) был немедленный перезапуск дочернего процесса (воркера) в случае завершения последнего в результате посылки ядром сигнала SIGSEGV. Новая задача будет сформулирована по-другому. Пусть у нас имеется два разных приложения. Нужно гарантировать, во-первых, что оба приложения будут выполняться одновременно, во-вторых, что перезапуск одного из приложений (после нормального завершения или получения сигнала) будет приводить к перезапуску второго приложения, и в-третьих, собственно перезапуск приложений в случае нормального завершения или получения заданных сигналов (пусть это будет SIGSEGV для определенности). Первые два условия означают, что гарантируется уникальность пар экземпляров двух приложений в любой момент времени: под связанностью процессов в заголовке статьи я подразумевал именно это. Условие связанности процессов может быть востребовано в случае, если один из них играет роль бэкенда, хранящего авторизационную информацию клиента, и общающегося с другим процессом — фронтэндом, непосредственно обслуживающим соединение с клиентом, через транспорт, не предоставляющий гарантий сохранения экземпляров взаимодействующих процессов, например TCP или UNIX-сокеты. Если условие связанности не будет выполняться, то перезапуск бэкенда приведет к утере авторизационной информации, в то время как клиентские сессии на фронтэнде останутся невредимы. Перезапуск фронтэнда в этом случае позволил бы перезагрузить переставшие быть валидными клиентские сессии. Эта проблема, на первый взгляд, кажется немного надуманной, но все же может возникнуть в реальности. Например, вам может понадобиться разработать приложение-бэкенд к сервису slapd, общающееся с последним через механизм slapd-sock. В этом случае slapd будет являться фронтэндом вашего приложения, который будет обязан перезапускать клиентские сессии (в рамках нашей задачи — “перезапускаться” сам) в случае завершения или падения бэкенда. В реальности перезапуском обеих частей нашего сервиса, как и прежде, будет заниматься мастер-процесс. Ниже я привожу исходный код соответствующей реализации, построчные комментарии ниже. Многие части полностью соответствуют коду из оригинальной статьи: их я комментировать не стану. Название файла с исходным кодом — main2.cpp.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#include <unistd.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <sys/time.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>
#include <iostream>
#include <iomanip>

#ifdef MAXCYCLES
#define LOOPSTOPCOND      i < MAXCYCLES
#else
#define LOOPSTOPCOND
#endif

#ifndef PDEATHQUITSIGNAL
#define PDEATHQUITSIGNAL  SIGTERM
#endif

#ifndef CHILDLIFETIME
#define CHILDLIFETIME     4
#endif

static int  pid0( 0 );
static int  pid1( 0 );
static int  pid2( 0 );

static struct timeval  start;


static inline std::ostream &  tprint( std::ostream &  out = std::cout,
                                      const std::string &  delim = " | " )
{
    struct timeval  tv;
    gettimeofday( &tv, NULL );
    int  ms( ( tv.tv_sec - start.tv_sec ) * 1000 +
             ( tv.tv_usec - start.tv_usec ) / 1000 );
    return out << std::setw( 7 ) << float( ms ) / 1000 << delim;
}


static void  inth( int  sig )
{
    if ( getpid() != pid0 )
        exit( 0 );

    tprint() << "Master terminated by signal " << sig << std::endl;
    if ( pid1 > 0 )
    {
        tprint() << "Sending signal " << sig << " to worker 1" << std::endl;
        kill( pid1, sig );
        waitpid( pid1, NULL, 0 );
    }
    if ( pid2 > 0 )
    {
        tprint() << "Sending signal " << sig << " to worker 2" << std::endl;
        kill( pid2, sig );
        waitpid( pid2, NULL, 0 );
    }
    exit( 0 );
}


static void  setinth( void ( *handler )( int ) )
{
    struct sigaction  act;
    memset( &act, 0, sizeof( act ) );
    act.sa_handler = handler;

    int  ints[] = { SIGINT, SIGQUIT, SIGTERM, SIGHUP, 0 };

    for ( int *  s( ints ); *s != 0; ++s )
        sigaction( *s, &act, NULL );
}


int  main( int  argc, char **  argv )
{
    std::cout.precision( 3 );
    std::cout.setf( std::ios::fixed );

    gettimeofday( &start, NULL );

    pid0 = getpid();
    tprint() << "Master: " << pid0 << std::endl;

    setinth( inth );

    for ( int  i( 0 ); LOOPSTOPCOND; ++i )
    {
        if ( ( pid1 = fork() ) == 0 )    /* Worker process 1 */
        {
            pid2 = 0;
            if ( prctl( PR_SET_PDEATHSIG, PDEATHQUITSIGNAL ) == -1 )
            {
                tprint() << "Worker 1: failed to set parent death signal, "
                        "exiting" << std::endl;
                return 1;
            }
            setinth( SIG_DFL );

            tprint() << "(cycle " << i << ") Worker 1: " << getpid() <<
                    std::endl;
            sleep( CHILDLIFETIME );

            break;
        }

        if ( pid1 < 0 )
        {
            tprint() << "Failed to fork a worker 1 process, exiting" <<
                    std::endl;
            return 1;
        }

        if ( ( pid2 = fork() ) == 0 )    /* Worker process 2 */
        {
            pid1 = 0;
            if ( prctl( PR_SET_PDEATHSIG, PDEATHQUITSIGNAL ) == -1 )
            {
                tprint() << "Worker 2: failed to set parent death signal, "
                        "exiting" << std::endl;
                return 1;
            }

            tprint() << "(cycle " << i << ") Worker 2: " << getpid() <<
                    std::endl;

            char * const  cmd[] = { ( char * )"test_slapd",
                                    ( char * )"-d",
                                    ( char * )"0",
                                    ( char * )"-h",
                                    ( char * )"ldap://localhost:3333/",
                                    ( char * )"-f",
                                    ( char * )"nullslapd.conf",
                                    NULL };
            execve( "/usr/sbin/slapd", cmd, NULL );

            tprint() << "Failed to exec slapd process, exiting" << std::endl;
            return 1;
        }

        if ( pid2 < 0 )
        {
            tprint() << "Failed to fork a worker 2 process, exiting" <<
                    std::endl;
            return 1;
        }

        bool       respawn( false );
        siginfo_t  siginfo;

        if ( waitid( P_ALL, 0, &siginfo, WEXITED | WSTOPPED ) == -1 )
        {
            tprint() << "waitid() error '" << strerror( errno ) <<
                    "', exiting" << std::endl;
            return 1;
        }

        int        cpid( siginfo.si_pid == pid1 ? 1 :
                         ( siginfo.si_pid == pid2 ? 2 : -1 ) );

        if ( cpid == -1 )
        {
            tprint() << "Bad child pid " << siginfo.si_pid <<
                    ", exiting" << std::endl;
            return 1;
        }

        int *      ppid( cpid == 1 ? &pid1 : &pid2 );

        *ppid = 0;

        tprint() << "Worker " << cpid;

        if ( siginfo.si_code == CLD_KILLED || siginfo.si_code == CLD_DUMPED )
        {
            int  sig( siginfo.si_status );

            std::cout << " was signaled " << sig << std::endl;

            switch ( sig )
            {
            case SIGSEGV:
                respawn = true;
            default:
                break;
            }
        }
        else
        {
            std::cout << " exited with status " << siginfo.si_status <<
                    std::endl;
            respawn = true;
        }

        cpid = cpid == 1 ? 2 : 1;

        tprint() << "Sending quit signal to worker " << cpid << std::endl;

        ppid = cpid == 1 ? &pid1 : &pid2;
        kill( *ppid, PDEATHQUITSIGNAL );
        waitpid( *ppid, NULL, 0 );
        *ppid = 0;

        if ( ! respawn )
            break;
    }

    return 0;
}
В строках 26–28 объявлены глобальные переменные pid0, pid1 и pid2, которые в дальнейшем, в функции main(), будут инициализированы значениями PID мастер-процесса, воркера-бэкенда и воркера-фронтэнда соответственно. Их необходимо сделать глобальными, поскольку обработчик сигнала inth(), о котором речь пойдет ниже, нуждается в доступе к ним. Если вам не хочется засорять глобальное пространство имен, то поместите объявления этих переменных внутрь анонимного namespace — все же таки на C++ пишем! В строке 30 объявлена еще одна глобальная переменная start, которая будет инициализирована в функции main() текущим значением времени. Она объявлена глобальной, поскольку к ней требуется доступ из функции tprint(), расположенной в строках 33–41. Функция tprint() очень полезна, она выводит в выходной поток (предположительно std::cout или std::cerr) время, прошедшее с начала старта программы. В строках 44–63 определена функция inth()обработчик прерывания мастер-процесса. Эта функция посылает тот же сигнал прерывания sig, которым был прерван мастер-процесс обоим дочерним процессам. Но предварительно она проверяет, что вызвавший ее процесс является мастером, сравнивая вызов getpid() с pid0. Ниже вы увидите, что воркер-бэкенд устанавливает все сигналы, которые обрабатываются в inth() в значение по умолчанию SIG_DFL, а воркер-фронтэнд вызывает execve(), которая в конечном итоге делает то же самое. Спрашивается, зачем тогда нужна эта проверка? Как известно, новый процесс после вызова fork() наследует обработчики сигналов родителя, соответственно существует очень короткий промежуток времени между рождением процесса и установкой его собственных обработчиков сигналов, в течение которого, если этот новый процесс будет прерван, в нем будет вызван родительский обработчик inth(), а это очень плохо. Поэтому проверка на pid0 в обработчике inth() необходима. Собственно обработчик прерывания настраивается в функции setinth(), определенной в строках 66–76. Этот код оформлен в виде отдельной функции, поскольку нашему воркеру-бэкенду понадобится вернуть обработчики прерываний в исходные значения SIG_DFL. Переходим к функции main(). В строках 81–82 настраивается форматирование потока cout для вывода времени. В строке 84 инициализируется значение глобальной переменной start, которая будет использоваться для вычисления времени, прошедшего с начала старта программы, в функции tprint(). В строке 89 настраиваются обработчики прерывания мастер-процесса. В строках 93–109 внутри цикла for, перезапускающего воркер-процессы (см. оригинальную статью), находится код воркера-бэкенда. Его задача простая — установить сигнал смерти родителя с помощью вызова функции prctl(), восстановить обработчики прерывания по умолчанию с помощью вызова setinth(), вывести сообщение о своем старте и просто заснуть на время, определенное в секундах в макросе CHILDLIFETIME, который по умолчанию равен 4 и может быть задан во время компиляции. Волшебный вызов prctl() нужен для гарантированного завершения воркера в случае смерти мастера. Зачем, спросите вы. Ведь мы и так посылаем сигнал прерывания воркерам из обработчика inth(). Верно, но если мастер будет убит сигналом SIGKILL, этот обработчик вызван не будет, и воркеры перейдут процессу init. Данный вызов prctl() гарантирует посылку заданного сигнала, который настраивается нашим макросом PDEATHQUITSIGNAL, в случае смерти родителя, даже если тот был убит сигналом SIGKILL. В строках 111–116 — банальная проверка на правильную отработку fork(). Далее, в строках 118–143 идет код воркера-фронтэнда, который запускает экземпляр исполняемого файла /usr/sbin/slapd с помощью вызова execve(). Перед этим, как и в случае с воркером-бэкендом, устанавливается сигнал смерти родителя и выводится сообщение о старте. Переустановка обработчиков сигналов не требуется, поскольку вызов execve() устанавливает обработчики в значения по умолчанию. Функция execve() принимает список строк cmd, который будет передан как массив строк argv в функцию main() нового исполняемого кода. Если execve() будет выполнен успешно, то код, следующий за ним (строки 141–142), выполняться не будет, другими словами в этих строках находится код, отвечающий за обработку ошибки execve(). Итак, в массиве cmd находится список опций командной строки исполняемого файла /usr/sbin/slapd. Первый элемент — это имя процесса. Если бы мы запустили slapd из командной строки оболочки, оно бы соответствовало slapd, в нашем случае оно будет test_slapd. Остальные опции подобраны таким образом, чтобы slapd можно было запустить без отрыва от терминала (-d 0) обычному пользователю (-h ldap://localhost:3333/ -f nullslapd.conf). Пустой файл nullslapd.conf необходимо предварительно создать в текущей директории. Есть одна интересная тонкость. Если в опции cmd добавить -u <user>, то сигнал смерти родителя сбросится. То есть в случае посылки мастеру сигнала SIGKILL процесс test_slapd не завершится, а поменяет родителя на процесс init, а это не то, что мы ожидаем. Это связано с тем, что опция -u приводит к вызовам setgid() и setuid() внутри кода slapd, а это приводит к сбросу сигнала смерти родителя (см. man prctl). Единственный способ предотвратить это — пропатчить исходный код slapd. В строках 145–150 — банальная проверка на правильную обработку fork() для воркера-фронтэнда. Обратите внимание, что между двумя воркерами нет никакого взаимодействия: данный пример просто не рассчитан на такие подробности. Зато ниже идет код, который будет обрабатывать завершение одного из воркеров вследствие нормального выхода или прерывания сигналом. Главную работу выполняет функция waitid(), которая ожидает завершения любого из потомков мастер-процесса (строки 155–160). Переменная cpid инициализируется значением 1, если был завершен воркер-бэкенд (воркер 1), или 2, если был завершен воркер-фронтэнд (воркер 2). После определения завершившегося процесса соответствующей глобальной переменной pid1 или pid2 присваивается значение 0 для того, чтобы не возникло проблем в обработчике прерывания мастер-процесса inth(). В строках 178–197 идет обработка информации о завершившемся процессе подобная той, которая была в оригинальной статье. Только на этот раз мы присваиваем переменной respawn значение true и в том случае, если процесс завершился нормально. Кроме этого, макросы WIFSIGNALED и WTERMSIG не работают правильно с waitid(), поэтому вместо них производится прямая проверка полей si_code и si_status переменной siginfo. В строках 199–206 мы идентифицируем второй воркер, посылаем ему сигнал, установленный в макросе PDEATHQUITSIGNAL, ожидаем его завершения и присваиваем соответствующей глобальной переменной pid1 или pid2 значение 0. Понятно, зачем первый воркер-бэкенд просто завершает свою работу после заданного времени? Я хочу протестировать, что второй воркер-фронтэнд получит сигнал завершения и оба процесса будут перезапущены. Соберем программу test2 с числом перезапусков 4
g++ -g -DMAXCYCLES=4 -o test2 main2.cpp
, и запустим ее без прерываний.
./test2
  0.000 | Master: 28165
  0.000 | (cycle 0) Worker 2: 28167
  0.002 | (cycle 0) Worker 1: 28166
  4.002 | Worker 1 exited with status 0
  4.002 | Sending quit signal to worker 2
  4.007 | (cycle 1) Worker 1: 28221
  4.007 | (cycle 1) Worker 2: 28222
  8.007 | Worker 1 exited with status 0
  8.007 | Sending quit signal to worker 2
  8.011 | (cycle 2) Worker 1: 28276
  8.012 | (cycle 2) Worker 2: 28277
 12.012 | Worker 1 exited with status 0
 12.012 | Sending quit signal to worker 2
 12.018 | (cycle 3) Worker 1: 28331
 12.019 | (cycle 3) Worker 2: 28332
 16.019 | Worker 1 exited with status 0
 16.019 | Sending quit signal to worker 2
Все верно. Каждые четыре секунды воркер-бэкенд завершал работу, мастер посылал сигнал прерывания фронтэнду и перезапускал их обоих. Давайте на каком-либо этапе прервем мастер-процесс.
./test2
  0.000 | Master: 325
  0.000 | (cycle 0) Worker 1: 326
  0.001 | (cycle 0) Worker 2: 327
  4.001 | Worker 1 exited with status 0
  4.001 | Sending quit signal to worker 2
  4.008 | (cycle 1) Worker 1: 383
  4.009 | (cycle 1) Worker 2: 384
^C  5.465 | Master terminated by signal 2
  5.465 | Sending signal 2 to worker 1
  5.465 | Sending signal 2 to worker 2
Работает. А теперь давайте запустим test2, перейдем во второй терминал, узнаем PID воркера-бэкенда, и пошлем ему сигнал SIGSEGV. Только сначала пересоберем test2 с другим значением CHILDLIFETIME, а то я не буду успевать переключаться между терминалами.
g++ -g -DMAXCYCLES=4 -DCHILDLIFETIME=10 -o test2 main2.cpp
./test2
  0.000 | Master: 9834
  0.000 | (cycle 0) Worker 1: 9835
  0.001 | (cycle 0) Worker 2: 9836
 10.001 | Worker 1 exited with status 0
 10.001 | Sending quit signal to worker 2
 10.005 | (cycle 1) Worker 1: 9973
 10.005 | (cycle 1) Worker 2: 9974
Во втором терминале быстро, как только появилась запись о старте cycle 1, вводим
ps -ef | grep [t]est
lyokha    9834 27454  0 22:47 pts/4    00:00:00 ./test2
lyokha    9973  9834  0 22:48 pts/4    00:00:00 ./test2
lyokha    9974  9834  1 22:48 pts/4    00:00:00 test_slapd -d 0 -h ldap://localhost:3333/ -f nullslapd.conf
kill -SEGV 9973
Возвращаемся в первый терминал и смотрим остаток вывода test2.
 18.545 | Worker 1 was signaled 11
 18.545 | Sending quit signal to worker 2
 18.551 | (cycle 2) Worker 2: 10122
 18.554 | (cycle 2) Worker 1: 10121
 28.555 | Worker 1 exited with status 0
 28.555 | Sending quit signal to worker 2
 28.567 | (cycle 3) Worker 2: 10272
 28.570 | (cycle 3) Worker 1: 10271
 38.571 | Worker 1 exited with status 0
 38.571 | Sending quit signal to worker 2
Все верно. Ручные переключение во второй терминал, определение PID воркера-бэкенда и посылка ему сигнала заняли 8.5 секунд, так что четыре секунды мне бы явно не хватило. Можно еще поиграть разными способами. Например, послать сигнал прерывания или сигнал SIGKILL одному из воркеров, или убить мастер-процесс сигналом SIGKILL. В обоих случаях и мастер, и оба воркера должны благополучно завершиться.

пятница, 28 ноября 2014 г.

Перезапуск падучего приложения из самого приложения (спасение утопающих ...)

Представьте себе ситуацию, когда у вас есть некоторое серверное приложение, которое потенциально может падать (я не буду рассуждать о причинах: допустим, это новое, написанное вами приложение, которое вы хотите считать абсолютно надежным, но это не так). Предположим, что вам ни в коем случае нельзя допускать простоя службы, реализуемой этим приложением. Есть разные способы следить за работой программ и перезапускать их в случае надобности извне (например, cron). Но … Спасение утопающих — дело рук самих утопающих. Именно этот подход я и хочу продемонстрировать в самых общих чертах. Разумеется, вы должны обладать исходным кодом программы. В этом случае достаточно выделить исходный полезный код в отдельный дочерний процесс (worker), а исходный процесс превратить в надсмотрщика (master или supervisor), который будет перезапускать полезный дочерний процесс в случае его падения. Вот код, а пояснения ниже.
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <iostream>

#ifdef MAXCYCLES
#define LOOPSTOPCOND i < MAXCYCLES
#else
#define LOOPSTOPCOND
#endif


int  main( int  argc, char **  argv )
{
    int  pid( 0 );

    std::cout << "Master: " << getpid() << std::endl;

    for ( int  i( 0 ); LOOPSTOPCOND; ++i )
    {
        if ( ( pid = fork() ) == 0 )    /* Worker process */
        {
            std::cout << "(cycle " << i << ") Worker: " << getpid() <<
                    std::endl;

            /* do segv if option -s was specified in command line */
            if ( argc > 1 && ! strcmp( argv[ 1 ], "-s" ) )
                int  a( *( ( int * )0 ) );

            break;
        }
        else                            /* Master process */
        {
            if ( pid < 0 )
            {
                std::cout << "Failed to fork a worker process, exiting" <<
                        std::endl;
                break;
            }

            int   status( 0 );
            bool  respawn( false );

            waitpid( pid, &status, 0 );

            if ( ! WIFSIGNALED( status ) )
                break;

            int  sig( WTERMSIG( status ) );

            std::cout << "Worker process was signaled " << sig <<  std::endl;

            switch ( sig )
            {
            case SIGSEGV:
                respawn = true;
            default:
                break;
            }

            if ( ! respawn )
                break;
        }
    }

    return 0;
}
Прежде всего нужно отметить, что код написан на C++, хотя ничто не мешает переписать его на C, если понадобится. Макросы MAXCYCLES и LOOPSTOPCOND нужны здесь только в демонстрационных целях: дабы ограничить число перезапусков воркеров величиной, заданной во время компиляции (MAXCYCLES). В релизной версии приложения цикл for скорее всего примет вид for ( ; ; ), так что эти макросы окажутся не нужны. Итак, цикл for нужен для перезапуска воркер-процессов в случае их падения. Внутри цикла for с помощью вызова fork() производится расщепление основного процесса, связанного с приложением, на две части — новый процесс (воркер) и старый (мастер). Как известно, fork() возвращает 0 в новом процессе, и какое-либо значение большее нуля — в старом. В новом процессе (блок после if ( ( pid = fork() ) == 0 )) мы выводим сообщение о старте воркера, вызываем его полезный код (в этом примере полезный код присутствует только в случае, когда в программу была передана опция командной строки -s: он приводит к посылке ядром сигнала SIGSEGV из-за разыменования нулевого указателя), и в конце обязательно ставим break;, поскольку воркер по-прежнему находится в цикле, и без его прерывания продолжит запускать новые воркеры, притворившись мастером! Внутри кода, относящегося к мастер-процессу (блок после else), прежде всего проверяется, что вызов функции fork() завершился успешно. Затем мастер ожидает завершения воркера с помощью вызова waitpid() и проверяет, каким образом тот завершился. Макрос WIFSIGNALED() позволяет установить, был ли воркер остановлен сигналом ядра или вышел самостоятельно. Во втором случае цикл for прерывается и мастер завершает свою работу. В случае, если воркер был остановлен сигналом, мы хотим узнать каким именно. Для этого предназначен макрос WTERMSIG(). Если это был сигнал SIGSEGV, то локальной переменной respawn присваивается истинное значение. Если ее значение остается ложным (когда воркер был прерван любым другим сигналом), то цикл завершается. Давайте посмотрим, как это работает (исходный файл я назвал main.cpp). Компилируем.
g++ -g -DMAXCYCLES=4 -o test main.cpp
Я ограничил число перезапусков четырьмя. Запускаем программу с нормальным завершением воркера.
./test 
Master: 6061
(cycle 0) Worker: 6062
Завершился воркер — завершился мастер. А теперь запустим программу с падучим воркером.
./test -s
Master: 6085
(cycle 0) Worker: 6086
Worker process was signaled 11
(cycle 1) Worker: 6152
Worker process was signaled 11
(cycle 2) Worker: 6154
Worker process was signaled 11
(cycle 3) Worker: 6156
Worker process was signaled 11
Мастер-процесс четыре раза перезапустил упавшие воркер-процессы, как и ожидалось. Я не стал касаться вопросов наследования ресурсов мастера при инициализации воркера и корректного завершения воркера при получении мастером сигнала прерывания — это отдельные интересные темы.

среда, 6 августа 2014 г.

SystemTap: пример трассировки в пространстве пользователя

SystemTap был разработан прежде всего для упрощения отладки и трассировки ядерных функций Linux (kernel space). Однако, с появлением в ядре механизма uprobes (в мэйнстриме ядра он присутствует начиная с версии 3.5, а в системах, связанных с Red Hat, таких как Fedora, CentOS и RHEL, он уже существует на протяжении нескольких лет), SystemTap можно использовать и для трассировки в пространстве пользователя (user space). На этой странице можно найти огромное количество примеров скриптов для SystemTap, которые дают представление о мощи этого инструмента. Я взял за основу один из них (уже не помню какой) и значительно переделал, чтобы показать, как работает SystemTap в пространстве пользователя. Мы не станем запускать SystemTap от имени суперпользователя, а для этого нам придется кое-что подготовить. Во-первых, необходимо установить пакет с серверной частью SystemTap. В моей системе Fedora 20 это делается так:
yum install systemtap-server
Затем нужно добавить вашу учетную запись в группу stapusr и запустить сервер.
usermod -a -G stapusr your-username
service stap-server start
Зачем такие сложности. Дело в том, что SystemTap при запуске нового скрипта строит и устанавливает в ядро Linux новый временный модуль, который задействует механизм uprobes для установки в интересующие нас точки программы интересующих нас обработчиков событий. По завершении работы SystemTap удаляет временный модуль из ядра. Очевидно, что для этого нужны привилегии суперпользователя, которыми обладает stap-server. Итак, перейдем к примеру. Возьмем простое приложение, написанное на C++ (файл test.cc).
#include <iostream>
#include <unistd.h>

namespace
{
    void  print( const char *  lbl )
    {
        std::cout << lbl << std::endl;
    }
}

class  A
{
    public:
        virtual void  print( void ) const
        {
            ::print( "A" );
        }
};

class  B : public A
{
    public:
        void  print( void ) const
        {
            ::print( "B" );
        }
};

namespace
{
    void  cycle( const A *  a, const A *  b, int  i )
    {
        if ( i % 3 )
            a->print();
        else
            b->print();

        sleep( i / 2 );
    }
}

int  main( void )
{
    A *  a( new A );
    A *  b( new B );

    for ( int  i( 0 ); i < 10; ++i )
        cycle( a, b, i );

    delete a;
    delete b;

    return 0;
}
Имеются два класса: A и его наследник B. В классе A определена виртуальная фукция print(), которая с помощью свободной функции print(), объявленной в анонимном пространстве имен, выводит на экран строку A. В классе B виртуальная функция print() переопределена: она выводит на экран строку B с помощью той же свободной функции print(). В функции main() создаются два полиморфных объекта a и b типов A и B соответственно. Затем в цикле от 1 до 10 эти объекты передаются в свободную функцию cycle(), которая вызывает виртуальный метод print() попеременно для a и b в зависимости от номера цикла, причем общее число вызовов a->print() будет превышать число вызовов b->print() в два раза. Кроме этого, функция cycle() умеет зависать, причем чем больше номер цикла, тем дольше длительность зависания. Все эти сложности я ввел с той целью, чтобы показать, как будет работать типичный сценарий SystemTap. Итак, давайте посмотрим, сколько раз будет вызвана виртуальная функция A::print(), сколько времени займут эти вызовы (мы соберем статистику и даже выведем гистограмму!), а также будем фиксировать зависания функции cycle() выше заданного порога. Для написания скрипта нам понадобятся имена функций, которые в C++ обычно не соответствуют именам, объявленным в исходном коде. Чтобы выяснить их значения, можно просто скомпилировать программу test.
g++ -o test test.cc
Заметьте, что мы скомпилировали программу без отладочной информации — SystemTap она не нужна! Теперь, с помощью SystemTap, можно вывести значения имен функций.
stap -l 'process("./test").function("*")' -c ./test
process("/home/lyokha/tmp/stap-example/test").function("_GLOBAL__sub_I_main")
process("/home/lyokha/tmp/stap-example/test").function("_Z41__static_initialization_and_destruction_0ii")
process("/home/lyokha/tmp/stap-example/test").function("_ZN12_GLOBAL__N_15cycleEPK1AS2_i")
process("/home/lyokha/tmp/stap-example/test").function("_ZN12_GLOBAL__N_15printEPKc")
process("/home/lyokha/tmp/stap-example/test").function("_ZN1AC2Ev")
process("/home/lyokha/tmp/stap-example/test").function("_ZN1BC1Ev")
process("/home/lyokha/tmp/stap-example/test").function("_ZNK1A5printEv")
process("/home/lyokha/tmp/stap-example/test").function("_ZNK1B5printEv")
process("/home/lyokha/tmp/stap-example/test").function("_ZNSt8ios_base4InitD1Ev@@GLIBCXX_3.4")
process("/home/lyokha/tmp/stap-example/test").function("_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@@GLIBCXX_3.4")
process("/home/lyokha/tmp/stap-example/test").function("_ZdlPv@@GLIBCXX_3.4")
process("/home/lyokha/tmp/stap-example/test").function("__do_global_dtors_aux")
process("/home/lyokha/tmp/stap-example/test").function("__libc_csu_fini")
process("/home/lyokha/tmp/stap-example/test").function("__libc_csu_init")
process("/home/lyokha/tmp/stap-example/test").function("_fini")
process("/home/lyokha/tmp/stap-example/test").function("_init")
process("/home/lyokha/tmp/stap-example/test").function("_start")
process("/home/lyokha/tmp/stap-example/test").function("deregister_tm_clones")
process("/home/lyokha/tmp/stap-example/test").function("frame_dummy")
process("/home/lyokha/tmp/stap-example/test").function("main")
process("/home/lyokha/tmp/stap-example/test").function("register_tm_clones")
Отлично, функции _ZNK1A5printEv и _ZNK1B5printEv — это и есть наши A::print() и B::print(), а функция _ZN12_GLOBAL__N_15cycleEPK1AS2_i — это свободная функция cycle() из анонимного пространства имен. Вот исходный код скрипта (файл test.stp, объяснения ниже).
#!/usr/bin/env stap

global startA, startCycle, intervals, hdr = "<---> "

probe process(@1).function("_ZNK1A5printEv")
{
  startA[tid()] = gettimeofday_us()
}
probe process(@1).function("_ZNK1A5printEv").return
{
  t = gettimeofday_us()
  old_t = startA[tid()]
  if (old_t) intervals <<< t - old_t
  delete startA[tid()]
}
probe process(@1).function("_ZN12_GLOBAL__N_15cycleEPK1AS2_i")
{
  startCycle[tid()] = gettimeofday_us()
}
probe process(@1).function("_ZN12_GLOBAL__N_15cycleEPK1AS2_i").return
{
  delete startCycle[tid()]
}
function dump_intervals(print_hist)
{
  if (@count(intervals))
  {
    printf("%s Intervals min:%dus avg:%dus max:%dus count:%d\n", hdr,
           @min(intervals), @avg(intervals), @max(intervals),
           @count(intervals))
    if (print_hist)
    {
      print(@hist_log(intervals))
    }
  }
}
probe timer.s(2)
{
  foreach (tid in startCycle)
  {
    old_t = startCycle[tid]
    if (old_t > 0)
    {
      t = gettimeofday_us()
      if (t - old_t > $2 * 1000000)
      {
        printf("%s Pending call detected: %dus (tid %d)\n", hdr, t - old_t, tid)
      }
    }
  }
  dump_intervals(0)
}
probe end
{
  println()
  dump_intervals(1)
}
Первая строка — shebang — понятно. Далее идет объявление четырех глобальных переменных: startA, startCycle, intervals и hdr, которые будут доступны из всех объявлений probe и function в скрипте. Переменная startA — это массив, параметризованный значениями идентификаторов программного потока, возвращаемыми встроенной функцией tid(). Мы будем использовать startA как временну́ю метку входа в функцию A::print() в соответствующем программном потоке. На самом деле, в программе всего лишь один поток, а параметризация переменных-массивов startA и startCycle по идентификаторам потоков сделана просто для обобщения подхода. Переменная startCycle — тоже временна́я метка входа в функцию cycle(). Кстати, как SystemTap определяет, что переменные startA и startCycle являются массивами? Ведь в их объявлении это никак не указано. Дело в том, что парсер SystemTap способен выводить (infer) тип переменной при ее первом использовании. В переменной intervals мы будем собирать временну́ю статистику пребывания внутри функции A::print(). Переменная hdr — простой префикс для строк, выводимых SystemTap, она инициализируется в месте объявления. Итак, первое объявление probe process(@1).function("_ZNK1A5printEv") читается так: для имени процесса, переданного SystemTap в первом аргументе командной строки, при входе в функцию _ZNK1A5printEv, выполнить тело данного probe, которое находится ниже, внутри фигурных скобок. Мы уже применяли подобные объявления, когда выводили список функций в программе test. Тогда это было process("./test").function("*"), то есть мы использовали имя процесса ./test. Поскольку опция -l не требовала использования сервера SystemTap, мы просто указали в имени процесса текущую директорию. Однако, когда мы запустим наш скрипт, сервер должен будет знать полный путь к программе test, поэтому мы передадим его через аргумент командной строки @1 (в SystemTap есть переменные вида $N, где N — некоторое число, которое, в отличие от bash, может превышать 9 — они подставляют N-ый аргумент командной строки как есть, и @1 — они оборачивают N-ый аргумент командной строки в двойные кавычки). Внутри probe process(@1).function("_ZNK1A5printEv") мы присваиваем элементу массива startA[tid()] значение gettimeofday_us(), которое соответствует числу микросекунд, прошедших с начала Эпохи UNIX. В свою очередь, probe process(@1).function("_ZNK1A5printEv").return устанавливает обработчик на выходе из A::print(). В нем, при помощи оператора <<<, в массив intervals добавляется новый элемент, равный разности между текущим значением gettimeofday_us() и значением startA[tid()]. Оператор <<< можно рассматривать как аналог оператора вывода в поток << из C++. На выходе из обработчика элемент startA[tid()] удаляется. Следующие два probe соответствуют входу и выходу в/из cycle(): они, соответственно, устанавливают и удаляют элемент массива startCycle[tid()]. В probe timer.s(2) каждые 2 секунды осуществляется проход по элементам startCycle и в случае, если разность между текущим значением gettimeofday_us() и значением данного элемента превышает значение в секундах, заданное во втором аргументе командной строки ($2), на экран выводится предупреждающее сообщение, в котором указано время пребывания внутри функции cycle() (собственно, эта разность) и идентификатор потока. В конце данного обработчика вызывается function dump_intervals(print_hist), которая выводит статистику, накопленную в intervals, на экран. В случае, когда аргумент print_hist не нулевой, на экран выводится гистограмма распределения событий из intervals. Мы не собираемся выводить гистограмму каждые две секунды, мы ее выведем в завершающем обработчике probe end. Итак, запускаем программу test под SystemTap.
stap test.stp -c ./test `pwd`/test 3
B
A
A
B
<--->  Intervals min:6us avg:7us max:8us count:2
A
<--->  Intervals min:6us avg:14us max:28us count:3
A
<--->  Intervals min:6us avg:17us max:28us count:4
B
<--->  Intervals min:6us avg:17us max:28us count:4
A
<--->  Intervals min:6us avg:21us max:37us count:5
<--->  Intervals min:6us avg:21us max:37us count:5
A
<--->  Intervals min:6us avg:21us max:37us count:6
<--->  Pending call detected: 3977735us (tid 4949)
<--->  Intervals min:6us avg:21us max:37us count:6
B
<--->  Intervals min:6us avg:21us max:37us count:6
<--->  Pending call detected: 3977592us (tid 4949)
<--->  Intervals min:6us avg:21us max:37us count:6

<--->  Intervals min:6us avg:21us max:37us count:6
value |-------------------------------------------------- count
    1 |                                                   0
    2 |                                                   0
    4 |@                                                  1
    8 |@                                                  1
   16 |@@@                                                3
   32 |@                                                  1
   64 |                                                   0
  128 |                                                   0
Опция -c ./test говорит о том, что SystemTap должен запустить отдельный процесс и выполнить в нем программу test. (SystemTap также поддерживает опцию -x PID, которая позволяет прикрепиться к уже существующему процессу: такой механизм лучше подходит для трассировки в продакшн системе, но я не буду на этом останавливаться.) Первый свободный аргумент командной строки — `pwd`/test — это полный путь к исполняемому файлу test, второй свободный аргумент — таймаут пребывания внутри тела функции cycle(). Вывод программы не принес неожиданностей: функция A::print() вызывалась 6 раз, B::print() — 4 раза, это видно из вывода собственно программы test. SystemTap выводил статистику по intervals каждые 2 секунды, в завершении была выведена соответствующая гистограмма распределения времен пребывания внутри функции A::print() в микросекундах. Кроме того, обработка функции cycle() два раза превышала установленный предел в 3 секунды и SystemTap вывел соответствующие сообщения. Давайте усложним задачу. Теперь нас дополнительно будет интересовать количество вызовов свободной функции print() из A::print() (их должно оказаться 6), а также значение переменной i на входе в функцию cycle(). Первая задача подразумевает использование механизма process().function().callee() (эта запись условна, так как формально во всех круглых скобках нужно указывать какие-либо строковые значения). Механизм callee требует, чтобы программа test содержала символы DWARF, то есть была скомпилирована с опцией -g. Да и вторая задача тоже требует DWARF. Вот какие изменения нужно внести в test.stp.
...

global startA, startCycle, intervals, printcalls = 0, hdr = "<---> "

...

probe process(@1).function("_ZN12_GLOBAL__N_15cycleEPK1AS2_i")
{
  startCycle[tid()] = gettimeofday_us()
  if (@defined($i))
  {
    printf("%s Stack variable i = %d\n", hdr, $i)
  }
}

...

probe process(@1).function("_ZNK1A5printEv").callee("*print*") ?
{
  printcalls += 1
}

...

probe end
{
  println()
  dump_intervals(1)
  printf("%s Function print() was called from A %d times\n", hdr, printcalls)
}
Добавлена новая глобальная переменная printcalls, которая будет увеличиваться на единицу при вызове функции print() из A::print(): за это будет отвечать новый обработчик process(@1).function("_ZNK1A5printEv").callee("*print*") ?. Вопросительный знак в конце имени обработчика говорит о том, что он опционален: мы хотим использовать новый скрипт в том числе и когда программа test не будет содержать отладочной информации. Я не стал записывать полное имя функции в значение аргумента callee, а ограничился глоб-паттерном: они прекрасно работают в скриптах SystemTap. В обработчик входа в функцию cycle() добавлен вывод значения стековой переменной i, а в probe end — вывод значения printcalls. Компилируем с опцией -g и запускаем SystemTap.
g++ -g -o test test.cc
stap test.stp -c ./test `pwd`/test 3
B
A
A
B
<--->  Intervals min:8us avg:9us max:10us count:2
A
<--->  Intervals min:8us avg:14us max:25us count:3
A
<--->  Intervals min:8us avg:17us max:26us count:4
B
<--->  Intervals min:8us avg:17us max:26us count:4
A
<--->  Intervals min:8us avg:20us max:32us count:5
<--->  Intervals min:8us avg:20us max:32us count:5
A
<--->  Intervals min:8us avg:21us max:32us count:6
<--->  Pending call detected: 3977681us (tid 1682)
<--->  Intervals min:8us avg:21us max:32us count:6
B
<--->  Intervals min:8us avg:21us max:32us count:6
<--->  Pending call detected: 3977528us (tid 1682)
<--->  Intervals min:8us avg:21us max:32us count:6

<--->  Intervals min:8us avg:21us max:32us count:6
value |-------------------------------------------------- count
    2 |                                                   0
    4 |                                                   0
    8 |@@                                                 2
   16 |@@@                                                3
   32 |@                                                  1
   64 |                                                   0
  128 |                                                   0

<--->  Function print() was called from A 0 times
Сообщения о переменной i не выводятся, printcalls не увеличивается. Что-то не так. Посмотрим как теперь называются наши функции.
stap -l 'process("./test").function("*")' -c ./test
process("/home/lyokha/tmp/stap-example/test").function("A@/home/lyokha/tmp/stap-example/test.cc:30")
process("/home/lyokha/tmp/stap-example/test").function("B@/home/lyokha/tmp/stap-example/test.cc:39")
process("/home/lyokha/tmp/stap-example/test").function("_GLOBAL__sub_I_main@<unknown source>")
process("/home/lyokha/tmp/stap-example/test").function("_ZNSt8ios_base4InitD1Ev@@GLIBCXX_3.4")
process("/home/lyokha/tmp/stap-example/test").function("_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@@GLIBCXX_3.4")
process("/home/lyokha/tmp/stap-example/test").function("_ZdlPv@@GLIBCXX_3.4")
process("/home/lyokha/tmp/stap-example/test").function("__do_global_dtors_aux")
process("/home/lyokha/tmp/stap-example/test").function("__libc_csu_fini")
process("/home/lyokha/tmp/stap-example/test").function("__libc_csu_init")
process("/home/lyokha/tmp/stap-example/test").function("__static_initialization_and_destruction_0@<unknown source>")
process("/home/lyokha/tmp/stap-example/test").function("_fini")
process("/home/lyokha/tmp/stap-example/test").function("_init")
process("/home/lyokha/tmp/stap-example/test").function("_start")
process("/home/lyokha/tmp/stap-example/test").function("cycle@/home/lyokha/tmp/stap-example/test.cc:50")
process("/home/lyokha/tmp/stap-example/test").function("deregister_tm_clones")
process("/home/lyokha/tmp/stap-example/test").function("frame_dummy")
process("/home/lyokha/tmp/stap-example/test").function("main@/home/lyokha/tmp/stap-example/test.cc:61")
process("/home/lyokha/tmp/stap-example/test").function("print@/home/lyokha/tmp/stap-example/test.cc:24")
process("/home/lyokha/tmp/stap-example/test").function("print@/home/lyokha/tmp/stap-example/test.cc:33")
process("/home/lyokha/tmp/stap-example/test").function("print@/home/lyokha/tmp/stap-example/test.cc:42")
process("/home/lyokha/tmp/stap-example/test").function("register_tm_clones")
Ага, функции стали называться иначе. Давайте я сразу приведу готовый скрипт с учетом всех переименований и возможностью запуска с отладочными символами в программе test и без них.
#!/usr/bin/env stap

global startA, startCycle, intervals, printcalls = 0, hdr = "<---> "

probe process(@1).function("_ZNK1A5printEv")
{
  startA[tid()] = gettimeofday_us()
}
probe process(@1).function("_ZNK1A5printEv").return
{
  t = gettimeofday_us()
  old_t = startA[tid()]
  if (old_t) intervals <<< t - old_t
  delete startA[tid()]
}
probe process(@1).function("_ZN12_GLOBAL__N_15cycleEPK1AS2_i") ?
{
  startCycle[tid()] = gettimeofday_us()
}
probe process(@1).function("_ZN12_GLOBAL__N_15cycleEPK1AS2_i").return ?
{
  delete startCycle[tid()]
}
probe process(@1).function("cycle") ?
{
  startCycle[tid()] = gettimeofday_us()
  if (@defined($i))
  {
    printf("%s Stack variable i = %d\n", hdr, $i)
  }
}
probe process(@1).function("cycle").return ?
{
  delete startCycle[tid()]
}
probe process(@1).function("print@*test.cc:33").callee("*print*") ?
{
  printcalls += 1
}
function dump_intervals(print_hist)
{
  if (@count(intervals))
  {
    printf("%s Intervals min:%dus avg:%dus max:%dus count:%d\n", hdr,
           @min(intervals), @avg(intervals), @max(intervals),
           @count(intervals))
    if (print_hist)
    {
      print(@hist_log(intervals))
    }
  }
}
probe timer.s(2)
{
  foreach (tid in startCycle)
  {
    old_t = startCycle[tid]
    if (old_t > 0)
    {
      t = gettimeofday_us()
      if (t - old_t > $2 * 1000000)
      {
        printf("%s Pending call detected: %dus (tid %d)\n", hdr, t - old_t, tid)
      }
    }
  }
  dump_intervals(0)
}
probe end
{
  println()
  dump_intervals(1)
  printf("%s Function print() was called from A %d times\n", hdr, printcalls)
}
Я добавил опциональные probe с function(“cycle”) и function(“print@*test.cc:33”). Поскольку существует единственная функция cycle(), уточнение адреса не требуется. Другое дело — имя print, которое соответствует сразу трем функциям. Для разрешения адреса функции A::print() я воспользовался глоб-паттерном print@*test.cc:33. Важно отметить, что имена функций в случае введения отладочной информации могут сильно зависеть как от опций оптимизации gcc, так и от его версии. Ситуация осложняется наличием багов (например, вот этот), возможностью встраивания функций при оптимизации (inlining) и другими подобными факторами. В общем, правильно определить имя функции для использования в скрипте SystemTap и в итоге получить нужную трассировку может оказаться непростой задачей. Запустим скрипт test.stp снова.
stap test.stp -c ./test `pwd`/test 3
WARNING: For probing a particular line, use a .statement() probe, not .function(): keyword at test.stp:36:1
 source: probe process(@1).function("print@*test.cc:33").callee("*print*") ?
         ^
B
A
A
<--->  Stack variable i = 0
<--->  Stack variable i = 1
<--->  Stack variable i = 2
B
<--->  Stack variable i = 3
<--->  Intervals min:12us avg:13us max:14us count:2
A
<--->  Stack variable i = 4
<--->  Intervals min:12us avg:17us max:27us count:3
A
<--->  Stack variable i = 5
<--->  Intervals min:12us avg:19us max:27us count:4
B
<--->  Stack variable i = 6
<--->  Intervals min:12us avg:19us max:27us count:4
A
<--->  Stack variable i = 7
<--->  Intervals min:12us avg:21us max:28us count:5
<--->  Intervals min:12us avg:21us max:28us count:5
A
<--->  Stack variable i = 8
<--->  Intervals min:12us avg:21us max:28us count:6
<--->  Pending call detected: 3977342us (tid 32523)
<--->  Intervals min:12us avg:21us max:28us count:6
B
<--->  Stack variable i = 9
<--->  Intervals min:12us avg:21us max:28us count:6
<--->  Pending call detected: 3977157us (tid 32523)
<--->  Intervals min:12us avg:21us max:28us count:6

<--->  Intervals min:12us avg:21us max:28us count:6
value |-------------------------------------------------- count
    2 |                                                   0
    4 |                                                   0
    8 |@@                                                 2
   16 |@@@@                                               4
   32 |                                                   0
   64 |                                                   0

<--->  Function print() was called from A 6 times
Прикольно. Сам выводил print@…test.cc:33 как имя функции, а теперь говорит используйте .statement(). В данном случае мы не можем использовать statement, поскольку callee можно присоединить только к function. Ну да это просто предупреждение. Главное, все заработало! Мы стали получать значение переменной i и printcalls оказалась равной 6, как мы и ожидали.