ابزارسازی CPython با DTrace و SystemTap

نویسنده:

دیوید مالکوم

نویسنده:

Łukasz Langa

DTrace و SystemTap ابزارهای پایش هستند که هر کدام راهی برای بررسی کارهایی که فرایندهای یک سیستم کامپیوتری انجام می‌دهند، فراهم می‌کنند. هر دوی آن‌ها از زبان‌های خاص‌منظوره استفاده می‌کنند که به کاربر امکان می‌دهند اسکریپت‌هایی بنویسد که:

  • فیلتر کردن فرایندهایی که باید مشاهده شوند

  • جمع‌آوری داده‌ها از فرآیندهای مورد نظر

  • تولید گزارش درباره‌ی داده‌ها

از پایتون 3.6 به بعد، می‌توان CPython را با نشانگرهای تعبیه‌شده‌ای ساخت که با نام «پروب‌ها» (probes) نیز شناخته می‌شوند و می‌توان آن‌ها را با یک اسکریپت DTrace یا SystemTap مشاهده کرد؛ این امر پایش آنچه را که فرآیندهای CPython بر روی یک سیستم انجام می‌دهند، آسان‌تر می‌کند.

نشانگرهای DTrace جزئیات پیاده‌سازی مفسر CPython هستند. هیچ تضمینی درباره‌ی سازگاری پروب (probe) بین نسخه‌های CPython داده نمی‌شود. اسکریپت‌های DTrace ممکن است هنگام تغییر نسخه‌های CPython بدون هشدار از کار بیفتند یا نادرست کار کنند.

فعال‌سازی نشانگرهای ایستا

macOS دارای پشتیبانی توکار از DTrace است. در لینوکس، برای ساخت CPython با نشانگرهای تعبیه‌شده برای SystemTap، باید ابزارهای توسعه SystemTap نصب‌شده باشند.

در یک ماشین لینوکسی، این کار را می‌توان از این طریق انجام داد:

$ yum install systemtap-sdt-devel

یا:

$ sudo apt-get install systemtap-sdt-dev

سپس CPython باید با گزینه --with-dtrace پیکربندی شود:

بررسی برای --with-dtrace... بله

در macOS، می‌توانید پروب‌های DTrace در دسترس را با اجرای یک فرایند پایتون در پس‌زمینه و فهرست کردن تمام پروب‌هایی که فراهم‌کننده پایتون در دسترس قرار داده است، فهرست کنید:

$ python3.6 -q &
$ sudo dtrace -l -P python$!  # or: dtrace -l -m python3.6

   ID   PROVIDER            MODULE                          FUNCTION NAME
29564 python18035        python3.6          _PyEval_EvalFrameDefault function-entry
29565 python18035        python3.6             dtrace_function_entry function-entry
29566 python18035        python3.6          _PyEval_EvalFrameDefault function-return
29567 python18035        python3.6            dtrace_function_return function-return
29568 python18035        python3.6                           collect gc-done
29569 python18035        python3.6                           collect gc-start
29570 python18035        python3.6          _PyEval_EvalFrameDefault line
29571 python18035        python3.6                 maybe_dtrace_line line

در لینوکس، می‌توانید با بررسی اینکه آیا دودویی ساخته‌شده شامل بخش ".note.stapsdt" است، وجود نشانگرهای ایستای SystemTap را تأیید کنید.

$ readelf -S ./python | grep .note.stapsdt
[30] .note.stapsdt        NOTE         0000000000000000 00308d78

اگر پایتون را به‌عنوان یک کتابخانه اشتراکی (با گزینه‌ی پیکربندی --enable-shared) ساخته‌اید، باید به‌جای آن درون کتابخانه اشتراکی را بررسی کنید. برای مثال:

$ readelf -S libpython3.3dm.so.1.0 | grep .note.stapsdt
[29] .note.stapsdt        NOTE         0000000000000000 00365b68

readelf به‌اندازه کافی جدید می‌تواند فراداده را چاپ کند:

$ readelf -n ./python

Displaying notes found at file offset 0x00000254 with length 0x00000020:
    Owner                 Data size          Description
    GNU                  0x00000010          NT_GNU_ABI_TAG (ABI version tag)
        OS: Linux, ABI: 2.6.32

Displaying notes found at file offset 0x00000274 with length 0x00000024:
    Owner                 Data size          Description
    GNU                  0x00000014          NT_GNU_BUILD_ID (unique build ID bitstring)
        Build ID: df924a2b08a7e89f6e11251d4602022977af2670

Displaying notes found at file offset 0x002d6c30 with length 0x00000144:
    Owner                 Data size          Description
    stapsdt              0x00000031          NT_STAPSDT (SystemTap probe descriptors)
        Provider: python
        Name: gc__start
        Location: 0x00000000004371c3, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bf6
        Arguments: -4@%ebx
    stapsdt              0x00000030          NT_STAPSDT (SystemTap probe descriptors)
        Provider: python
        Name: gc__done
        Location: 0x00000000004374e1, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bf8
        Arguments: -8@%rax
    stapsdt              0x00000045          NT_STAPSDT (SystemTap probe descriptors)
        Provider: python
        Name: function__entry
        Location: 0x000000000053db6c, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6be8
        Arguments: 8@%rbp 8@%r12 -4@%eax
    stapsdt              0x00000046          NT_STAPSDT (SystemTap probe descriptors)
        Provider: python
        Name: function__return
        Location: 0x000000000053dba8, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bea
        Arguments: 8@%rbp 8@%r12 -4@%eax

فراداده‌ی بالا شامل اطلاعاتی برای SystemTap است که توضیح می‌دهد چگونه می‌تواند دستورالعمل‌های کد ماشین را که به‌طور راهبردی قرار داده شده‌اند وصله کند تا قلاب‌های ردگیری (tracing hooks) مورد استفاده‌ی یک اسکریپت SystemTap فعال شوند.

پروب‌های ایستای DTrace

می‌توان از اسکریپت نمونه DTrace زیر برای نمایش سلسله‌مراتب فراخوانی/بازگشت یک اسکریپت پایتون استفاده کرد، به‌طوری‌که فقط درون فراخوانی تابعی به نام "start" ردگیری انجام می‌شود. به بیان دیگر، فراخوانی‌های تابع در زمان ایمپورت فهرست نخواهند شد:

self int indent;

python$target:::function-entry
/copyinstr(arg1) == "start"/
{
        self->trace = 1;
}

python$target:::function-entry
/self->trace/
{
        printf("%d\t%*s:", timestamp, 15, probename);
        printf("%*s", self->indent, "");
        printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);
        self->indent++;
}

python$target:::function-return
/self->trace/
{
        self->indent--;
        printf("%d\t%*s:", timestamp, 15, probename);
        printf("%*s", self->indent, "");
        printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);
}

python$target:::function-return
/copyinstr(arg1) == "start"/
{
        self->trace = 0;
}

می‌توان آن را به این صورت فراخوانی کرد:

$ sudo dtrace -q -s call_stack.d -c "python3.6 script.py"

خروجی به این شکل است:

156641360502280  function-entry:call_stack.py:start:23
156641360518804  function-entry: call_stack.py:function_1:1
156641360532797  function-entry:  call_stack.py:function_3:9
156641360546807 function-return:  call_stack.py:function_3:10
156641360563367 function-return: call_stack.py:function_1:2
156641360578365  function-entry: call_stack.py:function_2:5
156641360591757  function-entry:  call_stack.py:function_1:1
156641360605556  function-entry:   call_stack.py:function_3:9
156641360617482 function-return:   call_stack.py:function_3:10
156641360629814 function-return:  call_stack.py:function_1:2
156641360642285 function-return: call_stack.py:function_2:6
156641360656770  function-entry: call_stack.py:function_3:9
156641360669707 function-return: call_stack.py:function_3:10
156641360687853  function-entry: call_stack.py:function_4:13
156641360700719 function-return: call_stack.py:function_4:14
156641360719640  function-entry: call_stack.py:function_5:18
156641360732567 function-return: call_stack.py:function_5:21
156641360747370 function-return:call_stack.py:start:28

نشانگرهای ایستای SystemTap

روش سطح پایین برای استفاده از یکپارچه‌سازی SystemTap، استفاده مستقیم از نشانگرهای ایستا است. این کار مستلزم آن است که پرونده دودویی حاوی آن‌ها را به‌صراحت مشخص کنید.

برای مثال، می‌توان از این اسکریپت SystemTap برای نمایش سلسله‌مراتب فراخوانی/بازگشت یک اسکریپت پایتون استفاده کرد:

probe process("python").mark("function__entry") {
     filename = user_string($arg1);
     funcname = user_string($arg2);
     lineno = $arg3;

     printf("%s => %s in %s:%d\\n",
            thread_indent(1), funcname, filename, lineno);
}

probe process("python").mark("function__return") {
    filename = user_string($arg1);
    funcname = user_string($arg2);
    lineno = $arg3;

    printf("%s <= %s in %s:%d\\n",
           thread_indent(-1), funcname, filename, lineno);
}

می‌توان آن را به این صورت فراخوانی کرد:

$ stap \
  show-call-hierarchy.stp \
  -c "./python test.py"

خروجی به این شکل است:

11408 python(8274):        => __contains__ in Lib/_abcoll.py:362
11414 python(8274):         => __getitem__ in Lib/os.py:425
11418 python(8274):          => encode in Lib/os.py:490
11424 python(8274):          <= encode in Lib/os.py:493
11428 python(8274):         <= __getitem__ in Lib/os.py:426
11433 python(8274):        <= __contains__ in Lib/_abcoll.py:366

که ستون‌ها عبارت‌اند از:

  • زمان بر حسب میکروثانیه از آغاز اسکریپت

  • نام پرونده اجرایی

  • شناسه فرایند (PID)

و بخش باقی‌مانده سلسله‌مراتب فراخوانی/بازگشت را در حین اجرای اسکریپت نشان می‌دهد.

برای ساخت CPython با --enable-shared، نشانگرها درون کتابخانه‌ی اشتراکی libpython قرار دارند و مسیر نقطه‌دار پروب (probe) باید این موضوع را منعکس کند. برای مثال، این خط از مثال بالا:

probe process("python").mark("function__entry") {

در عوض باید این‌گونه خوانده شود:

probe process("python").library("libpython3.6dm.so.1.0").mark("function__entry") {

(با فرض ساخت اشکال‌زدایی از CPython 3.6)

نشانگرهای ایستای در دسترس

function__entry(str filename, str funcname, int lineno)

این نشانگر نشان می‌دهد که اجرای یک تابع پایتون آغاز شده است. این نشانگر فقط برای توابع پایتون خالص (bytecode) فعال می‌شود.

نام پرونده، نام تابع و شماره خط به اسکریپت ردگیری به‌عنوان آرگومان‌های جایگاهی بازگردانده می‌شوند، که باید با استفاده از $arg1، $arg2 و $arg3 به آن‌ها دسترسی یافت:

  • $arg1 : (const char *) نام پرونده، قابل دسترسی با استفاده از user_string($arg1)

  • $arg2 : (const char *) نام تابع، قابل دسترسی با user_string($arg2)

  • $arg3 : int شماره‌ی خط

function__return(str filename, str funcname, int lineno)

این نشانگر، معکوس function__entry() است و نشان می‌دهد که اجرای یک تابع پایتون به پایان رسیده است (چه از طریق return و چه از طریق یک استثنا). این نشانگر فقط برای توابع پایتون خالص (بایت‌کد) فعال می‌شود.

آرگومان‌ها همانند function__entry() هستند

line(str filename, str funcname, int lineno)

این نشانگر نشان می‌دهد که یک خط پایتون در آستانه‌ی اجراست. این معادل ردگیری خط‌به‌خط با یک پروفایل‌گیر پایتون (Python profiler) است. این نشانگر درون توابع C فعال نمی‌شود.

آرگومان‌ها همان آرگومان‌های function__entry() هستند.

gc__start(int generation)

هنگامی که مفسر پایتون یک چرخه‌ی زباله‌روبی را آغاز می‌کند، رخ می‌دهد. arg0 نسلی است که باید پویش شود، مانند gc.collect().

gc__done(long collected)

هنگامی که مفسر پایتون یک چرخه زباله‌روبی را به پایان می‌رساند، راه‌اندازی می‌شود. arg0 تعداد اشیاء جمع‌آوری‌شده است.

import__find__load__start(str modulename)

پیش از تلاش importlib برای یافتن و بارگذاری ماژول، شلیک می‌شود. arg0 نام ماژول است.

اضافه شده در نسخه‌ی 3.7.

import__find__load__done(str modulename, int found)

پس از فراخوانی تابع find_and_load در importlib رخ می‌دهد. arg0 نام ماژول است و arg1 نشان می‌دهد که آیا ماژول با موفقیت بارگذاری شده است.

اضافه شده در نسخه‌ی 3.7.

audit(str event, void *tuple)

هنگامی که sys.audit() یا PySys_Audit() فراخوانی شود، رخ می‌دهد. arg0 نام رویداد به‌صورت یک رشته C است، و arg1 یک اشاره‌گر PyObject به یک شیء تاپل است.

اضافه شده در نسخه‌ی 3.8.

نقاط ورود C

برای ساده‌سازی فعال‌سازی نشانگرهای DTrace، C API پایتون شامل تعدادی تابع کمکی است که با هر نشانگر ایستا متناظر هستند. در ساخت‌های پایتون که DTrace در آن‌ها فعال نیست، این توابع هیچ کاری انجام نمی‌دهند.

به‌طور کلی، لازم نیست این‌ها را خودتان فراخوانی کنید، زیرا پایتون این کار را برای شما انجام خواهد داد.

تابع API C

نشانگر ایستا

یادداشت‌ها

void PyDTrace_LINE(const char *arg0, const char *arg1, int arg2)

line()

void PyDTrace_FUNCTION_ENTRY(const char *arg0, const char *arg1, int arg2)

function__entry()

void PyDTrace_FUNCTION_RETURN(const char *arg0, const char *arg1, int arg2)

function__return()

void PyDTrace_GC_START(int arg0)

gc__start()

void PyDTrace_GC_DONE(Py_ssize_t arg0)

gc__done()

void PyDTrace_INSTANCE_NEW_START(int arg0)

instance__new__start()

توسط پایتون استفاده نمی‌شود

void PyDTrace_INSTANCE_NEW_DONE(int arg0)

instance__new__done()

توسط پایتون استفاده نمی‌شود

void PyDTrace_INSTANCE_DELETE_START(int arg0)

instance__delete__start()

توسط پایتون استفاده نمی‌شود

void PyDTrace_INSTANCE_DELETE_DONE(int arg0)

instance__delete__done()

توسط پایتون استفاده نمی‌شود

void PyDTrace_IMPORT_FIND_LOAD_START(const char *arg0)

import__find__load__start()

void PyDTrace_IMPORT_FIND_LOAD_DONE(const char *arg0, int arg1)

import__find__load__done()

void PyDTrace_AUDIT(const char *arg0, void *arg1)

audit()

بررسی‌های کاوشی C

int PyDTrace_LINE_ENABLED(void)
int PyDTrace_FUNCTION_ENTRY_ENABLED(void)
int PyDTrace_FUNCTION_RETURN_ENABLED(void)
int PyDTrace_GC_START_ENABLED(void)
int PyDTrace_GC_DONE_ENABLED(void)
int PyDTrace_INSTANCE_NEW_START_ENABLED(void)
int PyDTrace_INSTANCE_NEW_DONE_ENABLED(void)
int PyDTrace_INSTANCE_DELETE_START_ENABLED(void)
int PyDTrace_INSTANCE_DELETE_DONE_ENABLED(void)
int PyDTrace_IMPORT_FIND_LOAD_START_ENABLED(void)
int PyDTrace_IMPORT_FIND_LOAD_DONE_ENABLED(void)
int PyDTrace_AUDIT_ENABLED(void)

همه فراخوانی‌های توابع PyDTrace باید با فراخوانی یکی از این توابع محافظت شوند. این امر به پایتون امکان می‌دهد هنگام غیرفعال بودن پایش (probing)، تأثیر بر کارایی را به حداقل برساند.

در ساخت‌هایی که DTrace در آن‌ها فعال نیست، این توابع هیچ کاری انجام نمی‌دهند و 0 را برمی‌گردانند.

Tapsetهای SystemTap

روش سطح بالاتر برای استفاده از یکپارچه‌سازی SystemTap، استفاده از یک «tapset» است: معادل یک کتابخانه در SystemTap، که برخی از جزئیات سطح پایین‌تر نشانگرهای ایستا را می‌پوشاند.

در اینجا یک پرونده‌ی tapset، بر پایه‌ی یک ساخت غیراشتراکی از CPython، آمده است:

/*
   Provide a higher-level wrapping around the function__entry and
   function__return markers:
 \*/
probe python.function.entry = process("python").mark("function__entry")
{
    filename = user_string($arg1);
    funcname = user_string($arg2);
    lineno = $arg3;
    frameptr = $arg4
}
probe python.function.return = process("python").mark("function__return")
{
    filename = user_string($arg1);
    funcname = user_string($arg2);
    lineno = $arg3;
    frameptr = $arg4
}

اگر این پرونده در پوشه‌ی tapset متعلق به SystemTap نصب شده باشد (برای مثال /usr/share/systemtap/tapset)، آنگاه این نقاط پروب (probepoints) اضافی در دسترس قرار می‌گیرند:

python.function.entry(str filename, str funcname, int lineno, frameptr)

این نقطه پروب (probe point) نشان می‌دهد که اجرای یک تابع پایتون آغاز شده است. این فقط برای توابع پایتون خالص (بایت‌کد) فعال می‌شود.

python.function.return(str filename, str funcname, int lineno, frameptr)

این نقطه‌ی پروب (probe point)، نقطه‌ی مقابل python.function.return است و نشان می‌دهد که اجرای یک تابع پایتون به پایان رسیده است (چه از طریق return و چه از طریق یک استثنا). این نقطه فقط برای توابع پایتون خالص (bytecode) فعال می‌شود.

مثال‌ها

این اسکریپت SystemTap از tapset بالا استفاده می‌کند تا مثال ارائه‌شده در بالا برای ردگیری سلسله‌مراتب فراخوانی تابع پایتون را به‌صورت تمیزتری پیاده‌سازی کند، بدون نیاز به نام‌گذاری مستقیم نشانگرهای ایستا:

probe python.function.entry
{
  printf("%s => %s in %s:%d\n",
         thread_indent(1), funcname, filename, lineno);
}

probe python.function.return
{
  printf("%s <= %s in %s:%d\n",
         thread_indent(-1), funcname, filename, lineno);
}

اسکریپت زیر از tapset بالا برای ارائه نمایی مشابه top از تمام کدهای CPython در حال اجرا استفاده می‌کند و هر ثانیه، ۲۰ فریم بایت‌کد با بیشترین تعداد ورود در سراسر سیستم را نشان می‌دهد:

global fn_calls;

probe python.function.entry
{
    fn_calls[pid(), filename, funcname, lineno] += 1;
}

probe timer.ms(1000) {
    printf("\033[2J\033[1;1H") /* clear screen \*/
    printf("%6s %80s %6s %30s %6s\n",
           "PID", "FILENAME", "LINE", "FUNCTION", "CALLS")
    foreach ([pid, filename, funcname, lineno] in fn_calls- limit 20) {
        printf("%6d %80s %6d %30s %6d\n",
            pid, filename, lineno, funcname,
            fn_calls[pid, filename, funcname, lineno]);
    }
    delete fn_calls;
}