ابزارسازی 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 |
نشانگر ایستا |
یادداشتها |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
توسط پایتون استفاده نمیشود |
|
|
توسط پایتون استفاده نمیشود |
|
|
توسط پایتون استفاده نمیشود |
|
|
توسط پایتون استفاده نمیشود |
|
|
|
|
|
|
|
|
بررسیهای کاوشی 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;
}