Адладжваем разгортванне ПЗ са strace

Адладжваем разгортванне ПЗ са strace

Мая асноўная праца - гэта, па большай частцы, разгортванне сістэм ПА, гэта значыць процьму часу я марную, спрабуючы адказаць на такія вось пытанні:

  • У распрацоўніка гэтае ПА працуе, а ў мяне няма. Чаму?
  • Учора гэтае ПЗ у мяне працавала, а сёння няма. Чаму?

Гэта - свайго роду адладка, якая крыху адрозніваецца ад звычайнай адладкі ПЗ. Звычайная адладка - гэта пра логіку кода, а вось адладка разгортвання - гэта пра ўзаемадзеянне кода і асяроддзя. Нават калі корань праблемы – лагічная памылка, той факт, што на адной машыне ўсё працуе, а на іншай – не, азначае, што справа нейкім чынам у асяроддзі.

Таму замест звычайных прылад для адладкі накшталт gdb у мяне ёсць іншы набор інструментаў для адладкі разгортвання. І мая любімая прылада для барацьбы з праблемай тыпу «Чаму гэтае ПЗ у мяне не арэ?» называецца страйс.

Што ж такое strace?

страйс - Гэта інструмент для «трасіроўкі сістэмнага выкліку». Першапачаткова ствараўся пад Linux, але тыя ж фішкі па адладцы можна праварочваць і з прыладамі для іншых сістэм (DTrace або ktrace).

Асноўнае прымяненне вельмі проста. Трэба толькі запусціць strace з любой камандай і ён адправіць у дамп усе сістэмныя выклікі (праўда, спачатку, мусіць, прыйдзецца ўсталяваць сам страйс):

$ strace echo Hello
...Snip lots of stuff...
write(1, "Hellon", 6)                  = 6
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

Што гэта за сістэмныя выклікі? Гэта нешта накшталт API для ядра аперацыйнай сістэмы. Даўным-даўно ў ПЗ быў прамы доступ да "жалеза", на якім яно працавала. Калі, напрыклад, трэба было адлюстраваць што-небудзь на экране, яно гуляла з партамі ці адлюстроўваюцца ў памяць рэгістрамі для відэа-прылад. Калі сталі папулярныя шматзадачныя кампутарныя сістэмы, зацараваў хаос, таму што розныя прыкладанні біліся за «жалеза». Памылкі ў адным дадатку маглі абрынуць працу іншых, калі не ўсю сістэму цалкам. Тады ў ЦПУ з'явіліся рэжымы прывілеяў (або "кольцавая абарона"). Самым прывілеяваным станавілася ядро: яно атрымлівала поўны доступ да «жалезу», плодзячы меней прывілеяваныя прыкладанні, якім ужо даводзілася запытваць доступ у ядра для ўзаемадзеяння з «жалезам» – праз сістэмныя выклікі.

На бінарным узроўні сістэмны выклік крыху адрозніваецца ад простага выкліку функцыі, аднак большасць праграм выкарыстоўваюць абгортку ў стандартнай бібліятэцы. Г.зн. стандартная бібліятэка POSIX C змяшчае выклік функцыі напісаць (), якая змяшчае ўвесь які залежыць ад архітэктуры код для сістэмнага выкліку запіс.

Адладжваем разгортванне ПЗ са strace

Калі сцісла, то любое ўзаемадзеянне прыкладання са сваім асяроддзем (кампутарнымі сістэмамі) ажыццяўляецца праз сістэмныя выклікі. Таму калі ПЗ на адно машыне працуе, а на іншай не, добра б зазірнуць у вынікі трасіроўкі сістэмных выклікаў. Калі ж дакладней, то вось вам спіс тыповых момантаў, якія можна прааналізаваць пры дапамозе трасіроўкі сістэмнага выкліку:

  • Кансольны ўвод-вывад
  • Сеткавы ўвод-вывад
  • Доступ да файлавай сістэмы і файлавы ўвод-вывад
  • Кіраванне тэрмінам жыцця працэсуструменю
  • Нізкаўзроўневае кіраванне памяццю
  • Доступ да драйвераў спецыяльных прылад

Калі выкарыстоўваць strace?

У тэорыі, страйс выкарыстоўваецца з любымі праграмамі ў карыстацкай прасторы, бо любая праграма ў карыстацкай прасторы павінна рабіць сістэмныя выклікі. Ён больш эфектыўна працуе з кампіляванымі, нізкаўзроўневымі праграмамі, але і з высокаўзроўневымі мовамі накшталт Python таксама працуе, калі атрымаецца прадзерціся праз дадатковы шум ад асяроддзя выканання і інтэрпрэтатара.

Ва ўсім бляску страйс выяўляе сябе падчас адладкі ПА, якое добра працуе на адной машыне, а на іншай раптам перастае працаваць, выдаючы невыразныя паведамленні аб файлах, дазволах ці няўдалых спробах выканаць нейкія каманды ці яшчэ што… Шкада, але не так добра ён спалучаецца з высокаўзроўневымі праблемамі тыпу памылкі праверкі сертыфіката. Звычайна тут патрабуецца камбінацыя страйс, часам ltrace і інструментаў больш высокага ўзроўню (накшталт інструмента каманднага радка OpenSSL для адладкі сертыфіката).

Для прыкладу бярэм працу на ізаляваным серверы, але трасіроўку сістэмных выклікаў часцяком можна выканаць і на больш складаных платформах разгортвання. Проста трэба падабраць прыдатны інструментар.

Прыклад простай адладкі

Скажам, жадаеце вы запусціць узрушаючае сервернае прыкладанне foo, а атрымліваецца вось што:

$ foo
Error opening configuration file: No such file or directory

Відавочна, у яго не атрымалася знайсці напісаны вамі канфігурацыйны файл. Так адбываецца, таму што часам, калі менеджэры пакетаў, кампілюючы дадатак, перавызначаюць чаканае размяшчэнне файлаў. І калі прытрымлівацца кіраўніцтву па ўсталёўцы для аднаго дыстрыбутыва, у іншым знаходзіш файлы зусім не тамака, дзе чакаў. Вырашыць праблему можна было б за пару секунд, калі б паведамленне пра памылку казала, дзе варта шукаць канфігурацыйны файл, але ж яно ж не кажа. Дык дзе шукаць?

Калі ёсць доступ да зыходнага кода, можна прачытаць яго і ўсё высветліць. Добры запасны план, але не самае хуткае рашэнне. Можна звярнуцца да пакрокавага адладчыку накшталт gdb і паглядзець, што робіць праграма, але куды больш эфектыўна выкарыстоўваць інструмент, які спецыяльна спраектаваны паказваць узаемадзеянне з асяроддзем: страйс.

Выснова страйс можа здацца залішнім, але добрыя навіны ў тым, што большую яго частку можна смела ігнараваць. Часта карысна выкарыстоўваць аператар -o, каб захоўваць вынікі трасіроўкі ў асобны файл:

$ strace -o /tmp/trace foo
Error opening configuration file: No such file or directory
$ cat /tmp/trace
execve("foo", ["foo"], 0x7ffce98dc010 /* 16 vars */) = 0
brk(NULL)                               = 0x56363b3fb000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=25186, ...}) = 0
mmap(NULL, 25186, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f2f12cf1000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 > 1 260A2 "..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1824496, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2f12cef000
mmap(NULL, 1837056, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f2f12b2e000
mprotect(0x7f2f12b50000, 1658880, PROT_NONE) = 0
mmap(0x7f2f12b50000, 1343488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7f2f12b50000
mmap(0x7f2f12c98000, 311296, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x16a000) = 0x7f2f12c98000
mmap(0x7f2f12ce5000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b6000) = 0x7f2f12ce5000
mmap(0x7f2f12ceb000, 14336, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f2f12ceb000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f2f12cf0500) = 0
mprotect(0x7f2f12ce5000, 16384, PROT_READ) = 0
mprotect(0x56363b08b000, 4096, PROT_READ) = 0
mprotect(0x7f2f12d1f000, 4096, PROT_READ) = 0
munmap(0x7f2f12cf1000, 25186)           = 0
openat(AT_FDCWD, "/etc/foo/config.json", O_RDONLY) = -1 ENOENT (No such file or directory)
dup(2)                                  = 3
fcntl(3, F_GETFL)                       = 0x2 (flags O_RDWR)
brk(NULL)                               = 0x56363b3fb000
brk(0x56363b41c000)                     = 0x56363b41c000
fstat(3, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x8), ...}) = 0
write(3, "Error opening configuration file"..., 60) = 60
close(3)                                = 0
exit_group(1)                           = ?
+++ exited with 1 +++

Прыкладна ўся першая старонка вываду страйс - Гэта звычайна нізкаўзроўневая падрыхтоўка да запуску. (Шмат выклікаў mmap, mprotect, пяро для рэчаў тыпу выяўлення нізкаўзроўневай памяці і адлюстраванне дынамічных бібліятэк.) Наогул, падчас адладкі высновы страйс лепш чытаць з самага канца. Унізе будзе выклік запіс, які выводзіць паведамленне пра памылку. Глядзім вышэй, і бачым першы памылковы сістэмны выклік - выклік openat, які выдае памылку ENOENT («файл або каталог не знойдзены»), які спрабаваў адкрыць /etc/foo/config.json. Вось тут і павінен ляжаць канфігурацыйны файл.

Гэта быў проста прыклад, але я б сказаў, што 90% часу, што я карыстаюся страйс, нічога моцна складаней гэтага выконваць і не даводзіцца. Ніжэй - поўнае пакрокавае кіраўніцтва па адладцы:

  • Знервавацца з-за невыразнага паведамлення аб system-y памылцы ад праграмы
  • Нанова запусціць праграму са страйс
  • Знайсці ў выніках трасіроўкі паведамленне аб памылцы
  • Ісці вышэй, пакуль не уткнецеся ў першы няўдалы сістэмны выклік

Вельмі верагодна, што сістэмны выклік у 4-м кроку пакажа, што пайшло не так.

падказкі

Перш чым паказаць прыклад больш складанай адладкі, адкрыю вам некалькі прыёмаў для эфектыўнага выкарыстання страйс:

man - ваш сябар

На шматлікіх *nix сістэмах поўны спіс сістэмных выклікаў да ядра можна атрымаць, запусціўшы man syscalls. Вы ўбачыце рэчы накшталт brk(2), а значыць, больш інфармацыі можна атрымаць, запусціўшы man 2 brk.

Невялікія граблі: man 2 fork паказвае мне старонку для абалонкі відэлец () в GNU libc, якая, аказваецца, якая рэалізавана з дапамогай выкліку клон (). Семантыка выкліку відэлец застаецца той жа, калі напісаць праграму, якая выкарыстоўвае відэлец (), і запусціць трасіроўку - я не знайду выклікаў відэлец, замест іх будуць клон (). Такія вось граблі толькі блытаюць, калі пачаць параўноўваць зыходнік з высновай. страйс.

Выкарыстоўвайце -о, каб захаваць выснову ў файл

страйс можа згенераваць шырокую выснову, так што часцяком карысна захоўваць вынікі трасіроўкі ў асобных файлах (як у прыведзеным вышэй прыкладзе). А яшчэ гэта дапамагае не зблытаць праграмную выснову з высновай страйс у кансолі.

Выкарыстоўвайце -s, каб прагледзець больш дадзеных аргументу

Вы, напэўна, заўважылі, што другая палова паведамлення пра памылку не паказана ў прыведзеным вышэй прыкладзе трасіроўкі. Гэта таму, што страйс па змаўчанні паказвае толькі першыя 32 байта аргумента радка. Калі хочаце бачыць больш, дадайце што-небудзь накшталт -s 128 да выкліку страйс.

-у палягчае адсочванне файлаў сокета і інш.

"Усе - файл" азначае, што *nix сістэмы выконваюць усе ўводы-высновы, выкарыстаючы файлавыя дэскрыптары, ці дастасавальна менавіта да файла ці сеткі, ці міжпрацэсным каналам. Гэта зручна для праграмавання, але перашкаджае сачыць за тым, што адбываецца на самой справе, калі бачыце агульныя счытванне и запіс у выніках трасіроўкі сістэмнага выкліку.

Дадаўшы аператар , вы прымусіце страйс анатаваць кожны файлавы дэскрыптар у выснове з нататкай, на што ён паказвае.

Прымацуеце да ўжо запушчанага працэсу з -p**

Як будзе відаць з прыведзенага ніжэй прыкладу, часам трэба рабіць трасіроўку праграмы, якая ўжо запушчана. Калі вядома, што яна запушчана як працэс 1337 г. (скажам, з высноў ps), то можна ажыццявіць трасіроўку яе вось так:

$ strace -p 1337
...system call trace output...

Магчыма, вам патрэбны root-правы.

Выкарыстоўвайце -f, каб сачыць за даччынымі працэсамі

страйс па змаўчанні трасіруе ўсяго адзін працэс. Калі ж гэты працэс спараджае даччыныя працэсы, то можна ўбачыць сістэмны выклік для спараджэння даччынага працэсу, але сістэмныя выклікі даччынага працэсу не будуць адлюстраваны.

Калі лічыце, што памылка - у даччыным працэсе, выкарыстоўвайце аператар -f, гэта уключыць яго трасіроўку. Мінус гэтага ў тым, што выснова яшчэ больш зб'е вас з панталыку. Калі страйс трасіруе адзін працэс або адну галінку, ён паказвае адзіны паток падзей выклікаў. Калі ён трасіруе адразу некалькі працэсаў, то, вы, магчыма, убачыце пачатак выкліку, перапыненага паведамленнем , затым - кучку выклікаў для іншых галінак выканання, і толькі потым - канчатак першага з <… foocall resumed>. Або падзеліце ўсе вынікі трасіроўкі па розных файлах, выкарыстоўваючы таксама аператар -ff (дэталі - у кіраўніцтве па страйс).

Фільтруйце трасіроўку пры дапамозе -e

Як бачыце, вынік трасіроўкі - рэальная куча ўсіх магчымых сістэмных выклікаў. Флагам -e можна адфільтраваць трасіроўку (гл. кіраўніцтва па страйс). Галоўная перавага ў тым, што запускаць трасіроўку з фільтраваннем хутчэй, чым рабіць поўную трасіроўку, а потым GrepШаць. Калі сапраўды, то мне амаль заўсёды пофіг.

Не ўсе памылкі дрэнныя

Просты і распаўсюджаны прыклад — праграма, якая шукае файл адразу ў некалькіх месцах, накшталт абалонкі, якая шукае, у якой кошыкакаталогу ўтрымоўваецца выкананы файл:

$ strace sh -c uname
...
stat("/home/user/bin/uname", 0x7ffceb817820) = -1 ENOENT (No such file or directory)
stat("/usr/local/bin/uname", 0x7ffceb817820) = -1 ENOENT (No such file or directory)
stat("/usr/bin/uname", {st_mode=S_IFREG|0755, st_size=39584, ...}) = 0
...

Эўрыстыка тыпу "апошні няўдалы запыт перад паведамленнем пра памылку" добрая ў пошуку рэлевантных памылак. Як бы там ні было, лагічна пачынаць з самага канца.

Зразумець сістэмныя выклікі добра дапамагаюць кіраўніцтва па праграмаванні на мове З

Стандартныя выклікі да бібліятэкаў З - не сістэмныя выклікі, але толькі тонкі павярхоўны пласт. Так што, калі вы хоць крыху разумееце, як і што рабіць у З, вам будзе лягчэй разабрацца ў выніках трасіроўкі сістэмнага выкліку. Напрыклад, у вас бяда з адладкай выклікаў да сеткавых сістэм, праглядзіце тое ж класічнае «Кіраўніцтва па сеткавым праграмаванні» Биджа.

Прыклад адладкі складаней

Я ўжо казаў, што прыклад простай адладкі - гэта прыклад таго, з чым мне, па большай частцы, прыходзіцца мець справу ў працы са страйс. Аднак часам патрабуецца сапраўднае расследаванне, так што вось вам рэальны прыклад адладкі больш складана.

bcron - планавальнік апрацоўкі задач, яшчэ адна рэалізацыя дэмана *nix крон. Ён усталяваны на серверы, але калі нехта спрабуе рэдагаваць расклад, адбываецца вось што:

# crontab -e -u logs
bcrontab: Fatal: Could not create temporary file

Ладненька, значыць, bcron паспрабаваў напісаць нейкі файл, але ў яго не выйшла, і ён не прызнаецца чаму. Расчахляем страйс:

# strace -o /tmp/trace crontab -e -u logs
bcrontab: Fatal: Could not create temporary file
# cat /tmp/trace
...
openat(AT_FDCWD, "bcrontab.14779.1573691864.847933", O_RDONLY) = 3
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f82049b4000
read(3, "#Ansible: logsaggn20 14 * * * lo"..., 8192) = 150
read(3, "", 8192)                       = 0
munmap(0x7f82049b4000, 8192)            = 0
close(3)                                = 0
socket(AF_UNIX, SOCK_STREAM, 0)         = 3
connect(3, {sa_family=AF_UNIX, sun_path="/var/run/bcron-spool"}, 110) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f82049b4000
write(3, "156:Slogs #Ansible: logsaggn20 1"..., 161) = 161
read(3, "32:ZCould not create temporary f"..., 8192) = 36
munmap(0x7f82049b4000, 8192)            = 0
close(3)                                = 0
write(2, "bcrontab: Fatal: Could not creat"..., 49) = 49
unlink("bcrontab.14779.1573691864.847933") = 0
exit_group(111)                         = ?
+++ exited with 111 +++

Прыкладна ў самым канцы ёсць паведамленне аб памылцы запіс, Але на гэты раз сёе-тое адрозьніваецца. Па-першае, няма рэлевантнай памылкі сістэмнага выкліку, якая звычайна перад гэтым адбываецца. Па-другое, бачна, што недзе нехта ўжо прачытаў паведамленьне пра памылку. Выглядае на тое, што сапраўдная праблема — недзе ў іншым месцы, а bcrontab проста прайгравае паведамленне.

Калі паглядзець на man 2 read, то можна ўбачыць, што першы аргумент (3) - гэта файлавы дэскрыптар, які *nix і выкарыстоўвае для ўсіх апрацовак уводу-высновы. Як пазнаць, што ўяўляе файлавы дэскрыптар 3? У дадзеным канкрэтным выпадку можна запусціць страйс з аператарам (гл. вышэй), і ён аўтаматычна раскажа, аднак, каб вылічаць падобныя штукі, карысна ведаць, як чытаць і аналізаваць вынікі трасіроўкі.

Крыніцай файлавага дэскрыптара можа быць адзін з шматлікіх сістэмных выклікаў (усё залежыць ад таго, для чаго дэскрыптар - для кансолі, сеткавага сокета, уласна файла ці чагосьці іншага), але як бы там ні было, выклікі мы шукаем, вяртаючы 3 (т .е. шукаем «= 3» у выніках трасіроўкі). У гэтым выніку іх 2: openat у самым версе і гняздо пасярэдзіне. openat адкрывае файл, але блізка(3) пасля гэтага пакажа, што ён закрываецца зноў. (Граблі: файлавыя дэскрыптары могуць выкарыстоўвацца нанова, калі іх адчыняюць і зачыняюць). Выклік socket () падыходзіць, паколькі ён апошні перад чытаць (), і атрымліваецца, што bcrontab працуе з нечым праз сокет. Наступны радок паказвае, што файлавы дэскрыптар злучаны з unix domain socket па шляху /var/run/bcron-spool.

Такім чынам, трэба знайсці працэс, прывязаны да unix socket з другога боку. Для гэтай мэты ёсць парачка хупавых трукаў, і абодва спатрэбяцца для адладкі серверных разгортванняў. Першы - выкарыстоўваць NetStat ці навейшы ss (socket status). Абедзве каманды паказваюць актыўныя сеткавыя злучэнні сістэмы і бяруць аператар -l для апісання слухаючых сокетаў, а таксама аператар -p для адлюстравання праграм, падлучаных да сокету ў якасці кліента. (Карысных опцый нашмат больш, але для гэтай задачы дастаткова і гэтых двух.)

# ss -pl | grep /var/run/bcron-spool
u_str LISTEN 0   128   /var/run/bcron-spool 1466637   * 0   users:(("unixserver",pid=20629,fd=3))

Гэта кажа пра тое, што слухач - гэта каманда. inixserver, якая працуе з ID працэсу 20629. (І, па супадзенні, яна выкарыстоўвае файлавы дэскрыптар 3 у якасці сокета.)

Другі рэальна карысны інструмент для знаходжання той жа інфармацыі называецца таксама. Ён пералічвае ўсе актытыя файлы (або файлавыя дэскрыптары) у сістэме. Ці ж можна атрымаць інфармацыю аб адным канкрэтным файле:

# lsof /var/run/bcron-spool
COMMAND   PID   USER  FD  TYPE  DEVICE              SIZE/OFF  NODE    NAME
unixserve 20629 cron  3u  unix  0x000000005ac4bd83  0t0       1466637 /var/run/bcron-spool type=STREAM

Працэс 20629 – гэта доўгажывучы сервер, так што можна прымацаваць да яго страйс пры дапамозе чагосьці накшталт strace -o /tmp/trace -p 20629. Калі рэдагаваць заданне cron у іншым тэрмінале - атрымаем выснову вынікаў трасіроўкі з якая ўзнікае памылкай. А вось і вынік:

accept(3, NULL, NULL)                   = 4
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7faa47c44810) = 21181
close(4)                                = 0
accept(3, NULL, NULL)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21181, si_uid=998, si_status=0, si_utime=0, si_stime=0} ---
wait4(0, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG|WSTOPPED, NULL) = 21181
wait4(0, 0x7ffe6bc36764, WNOHANG|WSTOPPED, NULL) = -1 ECHILD (No child processes)
rt_sigaction(SIGCHLD, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, 8) = 0
rt_sigreturn({mask=[]})                 = 43
accept(3, NULL, NULL)                   = 4
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7faa47c44810) = 21200
close(4)                                = 0
accept(3, NULL, NULL)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21200, si_uid=998, si_status=111, si_utime=0, si_stime=0} ---
wait4(0, [{WIFEXITED(s) && WEXITSTATUS(s) == 111}], WNOHANG|WSTOPPED, NULL) = 21200
wait4(0, 0x7ffe6bc36764, WNOHANG|WSTOPPED, NULL) = -1 ECHILD (No child processes)
rt_sigaction(SIGCHLD, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, 8) = 0
rt_sigreturn({mask=[]})                 = 43
accept(3, NULL, NULL

(Апошні прыняць () не будзе завершаны пры трасіроўцы.) І зноў, як ні шкада, але дадзены вынік не змяшчае памылкі, якую мы шукаем. Мы не бачым ніводнага паведамлення, якія б bcrontag пасылаў сокету або прымаў ад яго. Замест іх суцэльнае кіраванне працэсам (кланаваць, пачакай4, SIGCHLD і інш.) Гэты працэс спараджае даччыны працэс, які, як можна здагадацца, і выконвае сапраўдную працу. І калі трэба злавіць яе след, дадайце да выкліку strace -f. Вось што мы знойдзем, пашукаўшы паведамленне аб памылцы ў новым выніку са strace -f -o /tmp/trace -p 20629:

21470 openat(AT_FDCWD, "tmp/spool.21470.1573692319.854640", O_RDWR|O_CREAT|O_EXCL, 0600) = -1 EACCES (Permission denied) 
21470 write(1, "32:ZCould not create temporary f"..., 36) = 36
21470 write(2, "bcron-spool[21470]: Fatal: logs:"..., 84) = 84
21470 unlink("tmp/spool.21470.1573692319.854640") = -1 ENOENT (No such file or directory)
21470 exit_group(111)                   = ?
21470 +++ exited with 111 +++

Вось, гэта ўжо сёе-тое. Працэс 21470 атрымлівае памылку "адмоўлена ў доступе" пры спробе стварыць файл па шляху tmp/spool.21470.1573692319.854640 (які адносіцца да бягучага працоўнага каталога). Калі б мы проста ведалі бягучы працоўны каталог, мы б ведалі і поўны шлях і змаглі высветлілі, чаму працэс не можа стварыць у ім свой часовы файл. На няшчасце, працэс ужо выйшаў, таму не атрымаецца проста выкарыстоўваць lsof -p 21470 для таго, каб знайсці бягучы каталог, але можна папрацаваць у зваротны бок - пашукаць сістэмныя выклікі PID 21470, якія змяняюць каталог. (Калі такіх няма, PID 21470, павінна быць, атрымалі ў спадчыну іх ад бацькі, і гэта ўжо праз lsof -p не высветліць.) Гэты сістэмны выклік - чдзір (што нескладана даведацца з дапамогай сучасных сеткавых пошукавых рухавікоў). А вось і вынік зваротных пошукаў па выніках трасіроўкі, да самага сервера PID 20629:

20629 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7faa47c44810) = 21470
...
21470 execve("/usr/sbin/bcron-spool", ["bcron-spool"], 0x55d2460807e0 /* 27 vars */) = 0
...
21470 chdir("/var/spool/cron")          = 0
...
21470 openat(AT_FDCWD, "tmp/spool.21470.1573692319.854640", O_RDWR|O_CREAT|O_EXCL, 0600) = -1 EACCES (Permission denied) 
21470 write(1, "32:ZCould not create temporary f"..., 36) = 36
21470 write(2, "bcron-spool[21470]: Fatal: logs:"..., 84) = 84
21470 unlink("tmp/spool.21470.1573692319.854640") = -1 ENOENT (No such file or directory)
21470 exit_group(111)                   = ?
21470 +++ exited with 111 +++

(Калі губляецеся, то вам, магчыма, варта прачытаць мой папярэдні пост аб кіраванні працэсам *nix і абалонках.) Такім чынам, сервер PID 20629 не атрымаў дазволу стварыць файл па шляху /var/spool/cron/tmp/spool.21470.1573692319.854640. Верагодней за ўсё, прычынай таму – класічныя налады дазволаў файлавай сістэмы. Праверым:

# ls -ld /var/spool/cron/tmp/
drwxr-xr-x 2 root root 4096 Nov  6 05:33 /var/spool/cron/tmp/
# ps u -p 20629
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
cron     20629  0.0  0.0   2276   752 ?        Ss   Nov14   0:00 unixserver -U /var/run/bcron-spool -- bcron-spool

Вось дзе сабака закапаны! Сервер працуе як карыстацкі cron, але толькі ў root ёсць дазвол запісваць у каталог /var/spool/cron/tmp/. Простая каманда chown cron /var/spool/cron/tmp/ прымусіць bcron працаваць правільна. (Калі праблема складалася не ў гэтым, то наступны найболей верагодны падазраваны — модуль бяспекі ядра тыпу SELinux ці AppArmor, так што я бы праверыў часопіс паведамленняў ядра з дапамогай dmesg.)

Разам

Пачынальнаму ў выніках трасіроўкі сістэмных выклікаў можна патануць, але я, спадзяюся, паказаў, што яны - хуткі спосаб адладкі цэлага класа распаўсюджаных праблем з разгортваннем. Уявіце, як спрабуеце адладзіць шматпрацэсны bcron, выкарыстоўваючы пакрокавы адладчык.

Разбор вынікаў трасіроўкі ў зваротным парадку ўздоўж ланцужкі сістэмных выклікаў патрабуе навыку, але як я ўжо казаў, амаль заўсёды, карыстаючыся страйс, я проста атрымліваю вынік трасіроўкі і шукаю памылкі, пачынаючы з канца. У любым выпадку, страйс дапамагае мне зэканоміць плойму часу на адладцы. Спадзяюся, і вам ён таксама будзе карысны.

Крыніца: habr.com

Дадаць каментар