среда, 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, как мы и ожидали.

суббота, 21 июня 2014 г.

Шаблонное метапрограммирование в C++: генерация списков с произвольными полями

Я приведу псевдокод, который отразит смысл задачи.
list  Person1;

Person1.add_record( Name: "Paul" );
Person1.add_record( Age: 45 );
Список Person1 из этого примера можно реализовать с помощью структуры с полями Name и Age. Но в следующем определении мы, возможно, захотим изменить поля списка и нам понадобится новая структура. Как обойтись без явного определения структур-контейнеров и использовать одно обобщенное определение? Слово обобщенный (generic) содержит ответ — нужно привлечь к решению этой задачи шаблоны C++. Нам нужны линейные комбинации полей списков. Публичное наследование в C++ — отличный инструмент для создания линейных комбинаций! Я приведу код, добавляющий новую запись в список, а затем объясню его смысл. Имя файла — generator.hh. Здесь и далее стражи включения пропущены ради лаконичности.
#define DECLARE_RECORD( Name, Type, Value ) \
    template < typename  Parent >           \
    struct  Rec##Name : public Parent       \
    {                                       \
        Rec##Name() : Name( Value ) {}      \
        Type  Name;                         \
    };                                      \
    template <>                             \
    struct  Rec##Name< Generator::End >     \
    {                                       \
        Rec##Name() : Name( Value ) {}      \
        Type  Name;                         \
    };

namespace  Generator
{
    typedef int  End;
}
Макрос DECLARE_RECORD объявляет шаблоны структур с именем Rec##Name, которые содержат одну единственную запись, соответствующую одной из записей в формируемом списке: он реализует функцию add_record() из приведенного выше псевдокода. Структура может наследовать типу Parent — такой же структуре с другой единственной записью, либо замыкаться с помощью объявления Generator::End. Тип этого объявления может быть достаточно произвольным, я выбрал int. Формирование списка должно представлять собой создание списка типов из типов, объявленных с помощью DECLARE_RECORD. Давайте создадим простую базу персональных данных: файл persons.hh.
#include "generator.hh"

#define DECLARE_PERSON( Name, Id ) \
    Persons::Id::RecName< Persons::Id::RecAge< Generator::End > >  Name;

namespace  Persons
{
    namespace  Person1
    {
        DECLARE_RECORD( Name, const char *, "Paul" )
        DECLARE_RECORD( Age, size_t, 45 )
    }

    namespace  Person2
    {
        DECLARE_RECORD( Name, const char *, "Helen" )
        DECLARE_RECORD( Age, size_t, 38 )
    }
}
Макрос DECLARE_PERSON — это и есть механизм для формирования списка с записями имени (Name) и возраста (Age) персоналий. Список формируется нанизыванием имен структур, объявленных макросами DECLARE_RECORD ниже и заканчивается объявлением Generator::End. Для различения персоналий используются пространства имен Person1 и Person2. Заметьте, что здесь нет ни объектов времени исполнения, ни объектов времени компиляции: объявления DECLARE_RECORD разворачивают объявления шаблонов на стадии препроцессорной обработки, инстанцирование же этих шаблонов будет происходить во время применения макроса DECLARE_PERSON. Давайте напишем простой тест (файл test.cc).
#include <iostream>
#include "persons.hh"

int  main( void )
{
    DECLARE_PERSON( rec1, Person1 )
    std::cout << "1. Name: " << rec1.Name << ", Age: " << rec1.Age << std::endl;

    DECLARE_PERSON( rec2, Person2 )
    std::cout << "2. Name: " << rec2.Name << ", Age: " << rec2.Age << std::endl;
}
После компиляции запустим программу на исполнение.
g++ -o test test.cc
./test
1. Name: Paul, Age: 45
2. Name: Helen, Age: 38
Работает. А теперь создадим базу данных книг в файле books.hh. В каждой записи будет три поля: название (Name), автор (Author) и год выпуска (Year).
#include "generator.hh"

#define DECLARE_BOOK( Name, Id ) \
    Books::Id::RecName< Books::Id::RecAuthor< \
                                Books::Id::RecYear< Generator::End > > >  Name;

namespace  Books
{
    namespace  Book1
    {
        DECLARE_RECORD( Name, const char *, "War and Peace" )
        DECLARE_RECORD( Author, const char *, "Lev Tolstoy" )
        DECLARE_RECORD( Year, int, 1873 )
    }
}
Макрос DECLARE_BOOK выполняет ту же роль, что и DECLARE_PERSON при создании списка персональных данных, то есть создает список данных одной книги путем нанизывания типов, объявленных ниже с помощью макросов DECLARE_RECORD, в список типов, замыкаемый объявлением Generator::End. Добавим нашу новую книгу в тестовую программу. Для этого в файле test.cc нужно включить файл books.hh
#include "books.hh"
и в конце функции main() поместить строки
    DECLARE_BOOK( rec3, Book1 )
    std::cout << "3. Name: " << rec3.Name << ", Author: " << rec3.Author <<
                 ", Year: " << rec3.Year << std::endl;
Компилируем и запускаем на исполнение.
g++ -o test test.cc
./test
1. Name: Paul, Age: 45
2. Name: Helen, Age: 38
3. Name: War and Peace, Author: Lev Tolstoy, Year: 1873

вторник, 10 июня 2014 г.

malloc и значения VSZ и RSS в выводе программ для мониторинга процессов

Disclaimer. Заявления о том, что утечки памяти следует контролировать исключительно по значению VSZ, а RSS не возвращается в систему после удаления объектов, не верны или, по крайней мере, часто не верны. Вы не сможете контролировать утечки в случае, если в программе работает кастомный аллокатор, выделяющий память из заранее выделенного пула. В случае анализа RSS, пример в статье оказался банально некорректным, потому что для выделения физической памяти мало выделить ее функцией malloc(), нужно с ней еще что-нибудь сделать (например, вызвать memset(), спасибо моим коллегам за подсказку): в этом случае RSS будет реально выделяться и возвращаться в систему после удаления объектов. Я не буду удалять или редактировать эту статью, поскольку она демонстрирует технику работы с программой pidstat и опциями malloc и, кроме этого, является хорошим уроком мне самому, почему порой не следует делать слишком громких заявлений. Я приведу небольшой пример, в котором будет показано различие в природе этих значений и возможные способы управления ими (по крайней мере, значением VSZ). Как ни странно, несмотря на очевидное смысловое различие между виртуальной памятью процесса (VSZ) и резидентной (или физической) памятью процесса (RSS), находятся люди, которые судят об утечке памяти в программе на основании роста (а иногда неосвобождения) последней. Обычно программист может контролировать только виртуальную память (собственно, адресное пространство, доступное в программе относится к виртуальной памяти). Управлением физической памятью занимается ядро операционной системы, и ее рост и неосвобождение могут быть связаны с особенностями реализации в нем (ядре) управления страницами физической памяти. Соответственно, рост RSS может лишь косвенно свидетельствовать о возможной утечке виртуальной памяти, а ее неосвобождение так и вообще ни о чем таком не свидетельствует, поскольку ядро может возвращать в систему аллоцированные процессу страницы когда ему вздумается. Единственный относительно надежный способ судить об утечке памяти в программе — это следить за величиной VSZ. Почему относительно надежный: ну, во-первых, VSZ включает адресные пространства разделяемых библиотек, которые тоже могут, в принципе, течь, а во-вторых … И тут мы переходим к анонсированному примеру.
#include <unistd.h>
#include <iostream>

int  main( void )
{
    const size_t  ncycles( 100 );

    for ( int  i( ncycles ); i > 0; --i )
    {
        int *  a( new int[ 1024 * i ] );
        std::cout << "Cycle " << ncycles - i + 1 <<
                     ", addr: " << a << std::endl;
        usleep( 500000 );
        delete[] a;
    }

    return 0;
}
Простая программа на C++. В цикле выделяем большие куски памяти: сначала самые большие, затем все меньше и меньше. Внутри цикла устанавливаем задержку в пол-секунды для того, чтобы можно было удобно мониторить программу с помощью утилиты pidstat. Скомпилируем программу и дадим ей название malloc_test.
g++ -o malloc_test main.cc
Запустим программу и мониторинг в разных терминалах. Мы ожидаем, что VSZ будет постепенно уменьшаться, предположений о поведении RSS пока выдвигать не будем. А вот что показал pidstat на самом деле:
pidstat -r -p $(pgrep -x malloc_test) 2 30
Linux 3.14.5-200.fc20.x86_64 (localhost.localdomain)    10.06.2014  _x86_64_    (4 CPU)

18:53:44      UID       PID  minflt/s  majflt/s     VSZ    RSS   %MEM  Command
18:53:46     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:53:48     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:53:50     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:53:52     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:53:54     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:53:56     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:53:58     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:54:00     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:54:02     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:54:04     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:54:06     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:54:08     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:54:10     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:54:12     1000     20532      2,00      0,00   13032   1152   0,01  malloc_test
18:54:14     1000     20532      2,00      0,00   13032   1412   0,02  malloc_test
18:54:16     1000     20532      2,00      0,00   13032   1412   0,02  malloc_test
18:54:18     1000     20532      2,00      0,00   13032   1412   0,02  malloc_test
18:54:20     1000     20532      2,00      0,00   13032   1412   0,02  malloc_test
18:54:22     1000     20532      2,00      0,00   13032   1412   0,02  malloc_test
18:54:24     1000     20532      2,00      0,00   13032   1412   0,02  malloc_test
18:54:26     1000     20532      2,00      0,00   13032   1412   0,02  malloc_test
18:54:28     1000     20532      2,00      0,00   13032   1412   0,02  malloc_test
18:54:30     1000     20532      2,00      0,00   13032   1412   0,02  malloc_test
18:54:32     1000     20532      2,00      0,00   13032   1412   0,02  malloc_test
Значение VSZ не падает, а RSS вообще растет. О, ужас! Неужели утечка памяти? Но программа-то вроде простая, явных ошибок в ней нет. Открываем man mallopt и читаем о способах управления параметрами malloc. Комбинация M_TOP_PAD и M_TRIM_THRESHOLD выглядит заманчиво, листаем чуть ниже и узнаем, что их можно задать через переменные окружения MALLOC_TOP_PAD_ и MALLOC_TRIM_THRESHOLD_ без перекомпиляции программы! Зануляем эти значения и запускаем malloc_test снова.
MALLOC_TRIM_THRESHOLD_=0 MALLOC_TOP_PAD_=0 ./malloc_test
На этот раз вывод pidstat выглядит так:
19:07:18      UID       PID  minflt/s  majflt/s     VSZ    RSS   %MEM  Command
19:07:20     1000     32224      2,00      0,00   12876   1164   0,01  malloc_test
19:07:22     1000     32224      2,00      0,00   12860   1164   0,01  malloc_test
19:07:24     1000     32224      2,00      0,00   12844   1164   0,01  malloc_test
19:07:26     1000     32224      2,00      0,00   12828   1164   0,01  malloc_test
19:07:28     1000     32224      2,00      0,00   12812   1164   0,01  malloc_test
19:07:30     1000     32224      2,00      0,00   12796   1164   0,01  malloc_test
19:07:32     1000     32224      2,00      0,00   12780   1164   0,01  malloc_test
19:07:34     1000     32224      1,99      0,00   12764   1164   0,01  malloc_test
19:07:36     1000     32224      2,00      0,00   12748   1164   0,01  malloc_test
19:07:38     1000     32224      2,00      0,00   12732   1164   0,01  malloc_test
19:07:40     1000     32224      2,00      0,00   12716   1164   0,01  malloc_test
19:07:42     1000     32224      2,00      0,00   12700   1164   0,01  malloc_test
19:07:44     1000     32224      2,00      0,00   12684   1164   0,01  malloc_test
19:07:46     1000     32224      2,00      0,00   12668   1164   0,01  malloc_test
19:07:48     1000     32224      2,00      0,00   12652   1164   0,01  malloc_test
19:07:50     1000     32224      2,00      0,00   12636   1164   0,01  malloc_test
19:07:52     1000     32224      4,00      0,00   12620   1180   0,01  malloc_test
19:07:54     1000     32224      2,00      0,00   12604   1180   0,01  malloc_test
19:07:56     1000     32224      2,00      0,00   12588   1180   0,01  malloc_test
19:07:58     1000     32224      2,00      0,00   12572   1180   0,01  malloc_test
19:08:00     1000     32224      2,00      0,00   12564   1180   0,01  malloc_test
19:08:02     1000     32224      2,00      0,00   12564   1180   0,01  malloc_test
19:08:04     1000     32224      2,00      0,00   12564   1180   0,01  malloc_test
Круто, объем виртуальной памяти уменьшается! Но RSS по-прежнему увеличивается скачком под конец работы программы. Давайте посмотрим на вывод malloc_test:
Cycle 1, addr: 0x7fbbeda72010
Cycle 2, addr: 0x7fbbeda73010
Cycle 3, addr: 0x7fbbeda74010
Cycle 4, addr: 0x7fbbeda75010
Cycle 5, addr: 0x7fbbeda76010
Cycle 6, addr: 0x7fbbeda77010
    ...
Cycle 65, addr: 0x7fbbedaea010
Cycle 66, addr: 0x7fbbedaeb010
Cycle 67, addr: 0x7fbbedaec010
Cycle 68, addr: 0x7fbbedaed010
Cycle 69, addr: 0x7fbbedaee010
Cycle 70, addr: 0x10e3010
Cycle 71, addr: 0x10e3010
Cycle 72, addr: 0x10e3010
Cycle 73, addr: 0x10e3010
    ...
Cycle 96, addr: 0x10e3010
Cycle 97, addr: 0x10e3010
Cycle 98, addr: 0x10e3010
Cycle 99, addr: 0x10e3010
Cycle 100, addr: 0x10e3010
Заметили качественный скачок в значениях адресов выделенной памяти на 70-ом шаге, то есть когда размер выделяемой виртуальной памяти стал меньше примерно 1024 x 30 x sizeof(int), то есть 128 килобайт? Давайте заново запустим программу и проследим карту ее памяти до и после скачка. До:
cat /proc/$(pgrep -x malloc_test)/maps
00400000-00401000 r-xp 00000000 08:02 5907699                            /home/lyokha/tmp/28/malloc_test
00600000-00601000 r--p 00000000 08:02 5907699                            /home/lyokha/tmp/28/malloc_test
00601000-00602000 rw-p 00001000 08:02 5907699                            /home/lyokha/tmp/28/malloc_test
3748800000-3748820000 r-xp 00000000 08:01 353673                         /usr/lib64/ld-2.18.so
3748a1f000-3748a20000 r--p 0001f000 08:01 353673                         /usr/lib64/ld-2.18.so
3748a20000-3748a21000 rw-p 00020000 08:01 353673                         /usr/lib64/ld-2.18.so
3748a21000-3748a22000 rw-p 00000000 00:00 0 
3748c00000-3748db4000 r-xp 00000000 08:01 353674                         /usr/lib64/libc-2.18.so
3748db4000-3748fb4000 ---p 001b4000 08:01 353674                         /usr/lib64/libc-2.18.so
3748fb4000-3748fb8000 r--p 001b4000 08:01 353674                         /usr/lib64/libc-2.18.so
3748fb8000-3748fba000 rw-p 001b8000 08:01 353674                         /usr/lib64/libc-2.18.so
3748fba000-3748fbf000 rw-p 00000000 00:00 0 
3749800000-3749905000 r-xp 00000000 08:01 353677                         /usr/lib64/libm-2.18.so
3749905000-3749b05000 ---p 00105000 08:01 353677                         /usr/lib64/libm-2.18.so
3749b05000-3749b06000 r--p 00105000 08:01 353677                         /usr/lib64/libm-2.18.so
3749b06000-3749b07000 rw-p 00106000 08:01 353677                         /usr/lib64/libm-2.18.so
374a000000-374a015000 r-xp 00000000 08:01 353678                         /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374a015000-374a214000 ---p 00015000 08:01 353678                         /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374a214000-374a215000 r--p 00014000 08:01 353678                         /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374a215000-374a216000 rw-p 00015000 08:01 353678                         /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374d400000-374d4e9000 r-xp 00000000 08:01 353679                         /usr/lib64/libstdc++.so.6.0.19
374d4e9000-374d6e9000 ---p 000e9000 08:01 353679                         /usr/lib64/libstdc++.so.6.0.19
374d6e9000-374d6f1000 r--p 000e9000 08:01 353679                         /usr/lib64/libstdc++.so.6.0.19
374d6f1000-374d6f3000 rw-p 000f1000 08:01 353679                         /usr/lib64/libstdc++.so.6.0.19
374d6f3000-374d708000 rw-p 00000000 00:00 0 
7fad31adf000-7fad31b44000 rw-p 00000000 00:00 0 
7fad31b77000-7fad31b79000 rw-p 00000000 00:00 0 
7fff3eddb000-7fff3edfd000 rw-p 00000000 00:00 0                          [stack]
7fff3edfe000-7fff3ee00000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
После:
00400000-00401000 r-xp 00000000 08:02 5907699                            /home/lyokha/tmp/28/malloc_test
00600000-00601000 r--p 00000000 08:02 5907699                            /home/lyokha/tmp/28/malloc_test
00601000-00602000 rw-p 00001000 08:02 5907699                            /home/lyokha/tmp/28/malloc_test
01ba4000-01bbd000 rw-p 00000000 00:00 0                                  [heap]
3748800000-3748820000 r-xp 00000000 08:01 353673                         /usr/lib64/ld-2.18.so
3748a1f000-3748a20000 r--p 0001f000 08:01 353673                         /usr/lib64/ld-2.18.so
3748a20000-3748a21000 rw-p 00020000 08:01 353673                         /usr/lib64/ld-2.18.so
3748a21000-3748a22000 rw-p 00000000 00:00 0 
3748c00000-3748db4000 r-xp 00000000 08:01 353674                         /usr/lib64/libc-2.18.so
3748db4000-3748fb4000 ---p 001b4000 08:01 353674                         /usr/lib64/libc-2.18.so
3748fb4000-3748fb8000 r--p 001b4000 08:01 353674                         /usr/lib64/libc-2.18.so
3748fb8000-3748fba000 rw-p 001b8000 08:01 353674                         /usr/lib64/libc-2.18.so
3748fba000-3748fbf000 rw-p 00000000 00:00 0 
3749800000-3749905000 r-xp 00000000 08:01 353677                         /usr/lib64/libm-2.18.so
3749905000-3749b05000 ---p 00105000 08:01 353677                         /usr/lib64/libm-2.18.so
3749b05000-3749b06000 r--p 00105000 08:01 353677                         /usr/lib64/libm-2.18.so
3749b06000-3749b07000 rw-p 00106000 08:01 353677                         /usr/lib64/libm-2.18.so
374a000000-374a015000 r-xp 00000000 08:01 353678                         /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374a015000-374a214000 ---p 00015000 08:01 353678                         /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374a214000-374a215000 r--p 00014000 08:01 353678                         /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374a215000-374a216000 rw-p 00015000 08:01 353678                         /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374d400000-374d4e9000 r-xp 00000000 08:01 353679                         /usr/lib64/libstdc++.so.6.0.19
374d4e9000-374d6e9000 ---p 000e9000 08:01 353679                         /usr/lib64/libstdc++.so.6.0.19
374d6e9000-374d6f1000 r--p 000e9000 08:01 353679                         /usr/lib64/libstdc++.so.6.0.19
374d6f1000-374d6f3000 rw-p 000f1000 08:01 353679                         /usr/lib64/libstdc++.so.6.0.19
374d6f3000-374d708000 rw-p 00000000 00:00 0 
7fad31b3f000-7fad31b44000 rw-p 00000000 00:00 0 
7fad31b77000-7fad31b79000 rw-p 00000000 00:00 0 
7fff3eddb000-7fff3edfd000 rw-p 00000000 00:00 0                          [stack]
7fff3edfe000-7fff3ee00000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
Видите в чем разница? После скачка появилась именованная секция [heap], в которой находится диапазон адресов, соответствующий адресам, выделенным программой начиная с 70-ого шага итерации (в данных выводах диапазоны немного отличаются, поскольку я запустил программу второй раз). В то же время адреса, которые выделялись до 70-ого шага, находятся в одной (а может быть в обеих) из двух неименованных секций перед секцией [stack]. Совершенно очевидно, что ядру пришлось выделить дополнительную страницу физической памяти для создания секции [heap] начиная с 70-ого шага: этим и объясняется скачок RSS. Но почему же поменялась стратегия выделения памяти? Открываем man malloc и читаем:
Обычно, malloc() распределяет память из кучи и подгоняет размер кучи
соответствующим образом с помощью sbrk(2). Если распределяемый блок памяти
больше чем MMAP_THRESHOLD байт, то реализация malloc() в glibc распределяет
память с помощью mmap(2) в виде частного анонимного отображения. По умолчанию,
значение MMAP_THRESHOLD равно 128 КБ, но его можно изменить с помощью
mallopt(3). На распределения, выполняемые с помощью mmap(2), не влияет
ограничитель ресурса RLIMIT_DATA (смотрите getrlimit(2)).
Вот и объяснение, откуда взялось значение 128 килобайт. И, кстати, значением MMAP_THRESHOLD тоже можно управлять через переменную окружения MALLOC_MMAP_THRESHOLD_, а значит в данном случае мы сможем остановить скачок RSS. Проверим.
MALLOC_TRIM_THRESHOLD_=0 MALLOC_TOP_PAD_=0 MALLOC_MMAP_THRESHOLD_=0 ./malloc_test
Вывод pidstat:
19:40:13      UID       PID  minflt/s  majflt/s     VSZ    RSS   %MEM  Command
19:40:15     1000     28448      2,00      0,00   12876   1164   0,01  malloc_test
19:40:17     1000     28448      2,00      0,00   12860   1164   0,01  malloc_test
19:40:19     1000     28448      2,00      0,00   12844   1164   0,01  malloc_test
19:40:21     1000     28448      2,00      0,00   12828   1164   0,01  malloc_test
19:40:23     1000     28448      2,00      0,00   12812   1164   0,01  malloc_test
19:40:25     1000     28448      2,00      0,00   12796   1164   0,01  malloc_test
19:40:27     1000     28448      2,00      0,00   12780   1164   0,01  malloc_test
19:40:29     1000     28448      2,00      0,00   12764   1164   0,01  malloc_test
19:40:31     1000     28448      2,00      0,00   12748   1164   0,01  malloc_test
19:40:33     1000     28448      2,00      0,00   12732   1164   0,01  malloc_test
19:40:35     1000     28448      2,00      0,00   12716   1164   0,01  malloc_test
19:40:37     1000     28448      2,00      0,00   12700   1164   0,01  malloc_test
19:40:39     1000     28448      2,00      0,00   12684   1164   0,01  malloc_test
19:40:41     1000     28448      2,00      0,00   12668   1164   0,01  malloc_test
19:40:43     1000     28448      2,00      0,00   12652   1164   0,01  malloc_test
19:40:45     1000     28448      2,00      0,00   12636   1164   0,01  malloc_test
19:40:47     1000     28448      2,00      0,00   12620   1164   0,01  malloc_test
19:40:49     1000     28448      2,00      0,00   12604   1164   0,01  malloc_test
19:40:51     1000     28448      2,00      0,00   12588   1164   0,01  malloc_test
19:40:53     1000     28448      2,00      0,00   12572   1164   0,01  malloc_test
19:40:55     1000     28448      2,00      0,00   12556   1164   0,01  malloc_test
19:40:57     1000     28448      2,00      0,00   12540   1164   0,01  malloc_test
19:40:59     1000     28448      2,00      0,00   12524   1164   0,01  malloc_test
Работает! Объем RSS не увеличился, как мы и предположили. Этот последний пример хорошо демонстрирует природу RSS. Однако то, что нам удалось программно повлиять на динамику ее изменения — скорее исключение из правила, нежели правило. Программист не обязан контролировать величину RSS, поскольку ею управляет ядро операционной системы по своему усмотрению. Задача программиста — следить за изменением величины VSZ, однако и тут полно нюансов из-за вклада разделяемых библиотек и латентности при освобождении памяти в случае настройки malloc по умолчанию.

четверг, 24 апреля 2014 г.

nginx: эмуляция сложных операций в условии if с помощью регулярных выражений

Это продолжение статьи об эмуляции вложенных if в nginx с помощью регулярных выражений. На этот раз будем решать другую задачу: проверим, находится ли значение какого-нибудь объекта, например куки a, в некотором заданном списке. Пусть в этом списке, назовем его a_list, находятся значения, разделенные точкой с запятой. При равенстве куки a одному из значений в списке будем выполнять специальное действие, для простоты — выводить строку PASSED, в противном случае — выводить строку FAILED. Вот конфигурация nginx.
events {
    worker_connections  1024;
}

http {
    server {
        listen       80;
        server_name  localhost;

        set $a_list " foo; bar ;  1qwerty;a";

        location / {
            set $check_a $a_list::$cookie_a;
            if ($check_a ~* "(?:[^;]+\s*;\s*)*(?<=;|^)\s*([^;]+)\s*(?=;|::).*::\1$") {
                echo "PASSED";
                break;
            }
            echo "FAILED";
        }
    }
}
Я намеренно расставил разное количество пробелов вокруг точек с запятыми в списке a_list, чтобы показать, что это не будет являться проблемой при правильно составленном регулярном выражении в условии if. Как и в предыдущей статье, здесь создается проверочная переменная check_a, состоящая из двух частей — списка a_list и значения куки a, разделенных двумя двоеточиями. Внутри регулярного выражения в условии if два двоеточия соответствуют этому разделению двух переменных. В правой части выражения, которая соответствует значению куки a, находится обратная ссылка \1 на значение, выделенное круглыми скобками в левой части — в них мы ожидаем одно из значений из списка a_list. Таким образом, если значение куки a совпадет с одним из значений в списке a_list, то переменная check_a будет соответствовать этому регулярному выражению и условие if окажется верным. Самое сложное здесь — это составить выражение для левой части регулярного выражения в условии if. Его центральная часть — атом ([^;]+), который будет соответствовать обратной ссылке \1 из правой части. Этот атом может заканчиваться некоторым количеством пробельных символов (\s*) и точкой с запятой, либо двумя двоеточиями ((?=;|::)), если значение справа окажется равным последнему элементу из списка a_list. Перед центральным атомом могут находиться другие элементы, разделенные точкой с запятой ((?:[^;]+\s*;\s*)*), которые нас не интересуют, а также некоторое количество пробельных символов (\s*). Важно проверить, что перед центральным атомом в левой части, включая возможные пробельные символы вначале, стоит точка с запятой, либо это начало строки ((?<=;|^)), иначе хвосты элементов из списка a_list, такие как oo для foo или даже y для 1qwerty приведут к срабатыванию всего регулярного выражения. Давайте проверим конфигурацию.
curl -b 'a=foo' 'http://localhost:80/'
PASSED
curl -b 'a=oo' 'http://localhost:80/'
FAILED
curl -b 'a=foo1' 'http://localhost:80/'
FAILED
curl -b 'a=a' 'http://localhost:80/'
PASSED
curl -b 'a=' 'http://localhost:80/'
FAILED
curl -b 'a=qwert' 'http://localhost:80/'
FAILED
curl -b 'a=1qwerty' 'http://localhost:80/'
PASSED
Работает! А теперь усложним задачу. Будем сравнивать не на точное соответствие значения переменной справа, а на присутствие в ней слова из списка a_list. Это еще не всё. Пусть при этом значение некоторой другой переменной (например, cookie_b) будет равно SUCCESS — опять эмуляция вложенных if! Где это можно применить? Подставьте вместо cookie_b имя ssl_client_verify, а вместо cookie_a — имя ssl_client_i_dn. Получаем проверку клиентского SSL сертификата с дополнительной проверкой того, что SSL issuer входит в заготовленный нами список a_list (теперь мы можем назвать этот список более осмысленно, например valid_ssl_issuers). Итак, добавим в нашу конфигурацию новый локейшн.
        location /2 {
            set $check_a $cookie_b::$a_list::$cookie_a;
            if ($check_a ~* "^SUCCESS::(?:[^;]+\s*;\s*)*(?<=;|::)\s*([^;]+)\s*(?=;|::).*::.*\b\1(?:\b|$)") {
                echo "PASSED";
                break;
            }
            echo "FAILED";
        }
Что изменилось? Переменная check_a теперь состоит из трех частей — первой частью является значение куки b, которое мы будем проверять на соответствие значению SUCCESS. Оставшиеся две части, как и раньше — элементы списка a_list и значение куки a. Соответственно изменилось регулярное выражение в условии if. Теперь оно начинается с SUCCESS::. Вторая часть, за исключением look-behind атома (?<=;|::), который теперь проверяет, что центральный атом центральной части выражения начинается с точки с запятой либо с двух двоеточий, возможно дополненных пробельными символами, не изменилась. Наконец, последняя часть, которая соответствует значению куки a, претерпела небольшие изменения — в ней проверяется, что где-либо внутри значения куки a находится какое-либо слово из списка a_list (.*\b\1(?:\b|$)). Проверяем.
curl -b 'b=SUCCESS; a=foo' 'http://localhost:80/2'
PASSED
curl -b 'b=SUCCES; a=foo' 'http://localhost:80/2'
FAILED
curl -b 'b=SUCCESS; a=rty' 'http://localhost:80/2'
FAILED
curl -b 'b=SUCCESS; a=1qwerty' 'http://localhost:80/2'
PASSED
curl -b 'b=SUCCESS; a=--1qwerty' 'http://localhost:80/2'
PASSED
curl -b 'b=SUCCESS; a=7-1qwerty:7' 'http://localhost:80/2'
PASSED
Все правильно. Напомню, что символы - и : являются границами слова, поэтому два последних случая удовлетворяют нашему регулярному выражению.

воскресенье, 13 апреля 2014 г.

Несколько трюков с пакетом Latex Listings

Здесь я хочу показать три трюка с пакетом Listings, которые мне очень пригодились при определении “языка” для подсветки действий пользователя внутри командных оболочек и REPLов. Этот виртуальный язык, который я назвал shelloutput, может быть использован в плагине publish-helper. Некоторые сведения о реализации подсветки shelloutput в Listings приведены в разделе Highlighting shells and REPLs на главной странице проекта в github, там же приведен пример с картинками. Итак, определение псевдоязыка shelloutput — это и есть первый трюк, о котором я расскажу. Постановка задачи такова: обеспечить декоративное выделение блоков текста, изображающих действия пользователя внутри любых командных оболочек и REPLов, а также разделение текста с вводом пользователя (промпта) и выводом оболочки. Первая часть задачи решается без определения свойств языка и, соответственно, без участия пакета Listings. Простейший способ — определить новое окружение, внутри которого использовать какой-либо рамочный пакет, например пакет Framed.
\usepackage{framed}
\newenvironment{Leftbar}{
  \setlength\parskip{0pt}
  \setlength\OuterFrameSep{4pt}
  \setlength\partopsep{-\topsep}
  \begin{leftbar}
    \scriptsize
}{\end{leftbar}}
Это окружение, которое я назвал Leftbar, поскольку оно использует стиль leftbar из пакета Framed, должно находиться в преамбуле документа. Теперь, чтобы выделить блок текста с действиями пользователя в оболочке, нужно обернуть этот текст в тэги \begin{Leftbar} ... \end{Leftbar}. Перейдем к решению второй части задачи. Нам нужно определить новый язык, который мы сможем использовать в окружении lstlisting, которое, в свою очередь, мы будем помещать внутрь окружения Leftbar. Пусть основной стиль (basicstyle) языка shelloutput будет относиться к выводу оболочки, а стиль для ввода оболочки мы определим отдельно. Самое сложное здесь — это понять, как мы (и Listings) будем отличать ввод оболочки от вывода: промпты у всех оболочек разные и могут отсутствовать вообще. Поэтому мы не будем ориентироваться на промпт, а введем собственный виртуальный промпт, который будем вставлять в начало каждой строки, соответствующей вводу оболочки — ведь мы то знаем, где у нее ввод и вывод! В Listings есть опция moredelim, с помощью которой мы определим внешний вид ввода оболочки, передав ей наш виртуальный промпт (пусть это будет три бара и пробел — ‘||| ’) с флагами il, которые устанавливают, что Listings не должен показывать виртуальный промпт (i), и установки будут действовать в пределах всей строки (l). Вот как это может выглядеть в преамбуле документа:
\usepackage{listings}
\definecolor{shellpromptcolor}{HTML}{000000}
\definecolor{shelloutputcolor}{HTML}{666666}
\lstdefinelanguage{shelloutput}
  {basicstyle=\color{shelloutputcolor}
    \scriptsize
    \ttfamily\itshape,
   moredelim=[il][\color{shellpromptcolor}\upshape]{|||\ }}
В данном случае вывод оболочки будет подсвечен серым цветом и иметь наклонное начертание, в то время как ввод будет черным и прямым. Давайте добавим сюда небольшой, но красивый бонус — декоративные переносы строк. Для этого в преамбулу документа нужно добавить
\usepackage{MnSymbol}
\lstset{prebreak=\raisebox{0ex}[0ex][0ex]
  {\ensuremath{\rhookswarrow}}}
\lstset{postbreak=\raisebox{0ex}[0ex][0ex]
  {\ensuremath{\rcurvearrowse\space}}}
Пример использования подсветки виртуального языка shelloutput можно найти в разделе An example на странице разработки проекта vim-publish-helper. В этом примере документ, представленный на втором изображении, генерируется не напрямую из документа TeX, а опосредованно, через pandoc flavoured markdown, но результат будет совпадать независимо от языка исходного документа. Второй трюк. Listings преобразует символ переноса (-) в знак минус. Это разумно для многих языков, однако для нашего псевдоязыка shelloutput это выглядит некрасиво, во всяком случае в опциях команд оболочки, таких как ls -l --inode. Вернуть исходный символ переноса достаточно просто — определив опцию literate={-}{-}1 в преамбуле документа или внутри окружения lstlisting, однако этот простой подход ломает настройки alsoletter={-} и breakatwhitespace для символов переноса, что приводит к возможности обрыва строк при переносе на этих символах, а это выглядит очень некрасиво. Лучше всего определить макрос lst@CCPutMacro для символа переноса (фактически переопределить его, так как он уже определен — и в этом-то и проблема — в исходном коде listings.sty). Определение следует поместить в нашем окружении Leftbar, чтобы оно не повлияло на другие языки. Теперь Leftbar будет выглядеть так:
\makeatletter
\newenvironment{Leftbar}{
  \setlength\parskip{0pt}
  \setlength\OuterFrameSep{4pt}
  \setlength\partopsep{-\topsep}
  \lst@CCPutMacro\lst@ProcessLetter {"2D}{\lst@ttfamily{-{}}{-{}}}
  \@empty\z@\@empty
  \begin{leftbar}
    \scriptsize
}{\end{leftbar}}
\makeatother
Третий трюк. Русские символы (и вообще UTF-8) в выводе (и вводе) языка shelloutput. Оказывается, Listings до сих пор не поддерживает нормальную обработку национальных символов в формате UTF. Если в shelloutput встретятся русские символы, то они будут переставлены всевозможными способами с удалением пробелов и добавлением неожиданных новых строк. Проблема известная и есть вариант ее решения с помощью опции literate (см. здесь и здесь). Однако, этот способ очень verbose — лично мне он не нравится. Более простое и рабочее решение — использовать опцию escapeinside=|| в объявлении lstlisting, а внутри блока с текстом обрамлять участки с проблемными символами соответствующими эскейп-символами, в данном случае барами (|) и, возможно, внутри них тэгом \verb, который не позволит слипнуться множественным пробелам.

суббота, 5 апреля 2014 г.

Простой фильтр pandoc для преобразования параграфов HTML в блоки спанов

Вот исходный код фильтра (файл paraToSpanBlock.hs).
-- paraToSpanBlock.hs
import Text.Pandoc.JSON

paraToSpanBlock :: Maybe Format -> Block -> IO Block
paraToSpanBlock (Just (Format "html")) (Para contents) =
    return $ Plain [Span ("", [], [("style", style)]) contents]
    where style = "display: block; margin-bottom: 16px;\
                 \ font-family: Arial, Helvetica, sans-serif;"
paraToSpanBlock _ b = return b

main :: IO ()
main = toJSONFilter paraToSpanBlock
Фильтр преобразует блоки параграфов <p> ... </p> в блоки спанов <span style="STYLE"> ... </span>, где STYLE — значение, возвращаемое функцией style, объявленной внутри исходного кода фильтра и устанавливающей внешний вид этих блоков — величину отступа снизу и шрифт. Этот фильтр позволяет с легкостью генерировать код HTML для непосредственной вставки в блог на blogspot.com. Да, этот текст написан на pandoc flavoured markdown и преобразован в HTML с помощью команды
pandoc -thtml -FparaToSpanBlock -Fvimhl -S src.md
Здесь src.md — файл с этим исходным текстом, paraToSpanBlock и vimhl — фильтры paraToSpanBlock, о котором идет речь, и vimhl, о котором я писал здесь. Для компиляции фильтра paraToSpanBlock в командной строке нужно ввести
ghc --make paraToSpanBlock
Скомпилированную программу paraToSpanBlock следует поместить в директорию, объявленную в переменной окружения PATH. Плюсы в использовании простого текстового языка разметки для создания контента блога очевидны. Самое главное — это возможность редактирования контента в простом текстовом редакторе. Второе — возможность генерации документов в разных форматах, например в формате PDF, и здесь такой замечательный инструмент как Pandoc проявляет себя во всей красе. Третье (в том числе как следствие использования автоматических инструментов) — последовательное применение стилей к разным элементам текста, не допускающее случайных ошибок автора документа.