Процеси і з чим їх їдять, Perl, Програмування, статті

Давним-давно в одній далекій галактиці жив-був DOS. І було в нього всього-то
всього 640 кілов пам'яті, і міг виконувати він лише тільки одну задачу. І з'явився
грізний монстр Windows, який розділив процесорний час на частини. Одну частину
відвів він сонмищу процесів, а іншу частину – сонмищу потоків. І перегукувалися вони
за допомогою мьютекс і семафорів. А дані передавалися де як: Була там і загальна
купа (heap), і пайпи кой де працювали: Загалом розповідати далі думаю не
має сенсу. Ще багато чого можна написати – все це нудно і відволікає від
основних цілей.

Так як всі тут описане пов'язано з Perl, який проектувався з
орієнтуванням на UNIX-системи (я про це не читав, це мій власний висновок –
якщо не правий, можете поправити), всі системні фішки, які тут будуть
розгляну, відносяться саме до UNIX-системам. Звичайно, реалізація Perl для
Windows приховує деякі невідповідності, але далеко не все. Так що, сильно не
засмучуйтеся, якщо щось не працює. Безвихідних ситуацій не буває. Пишіть в
форум, а ще краще побільше експериментуйте – тоді все вийде.

Отже, процес – це програма, код якої виконується незалежно від інших
програм. Може бути це і не академічне визначення, але ж і наше завдання
НЕ обізвати це правильно, а зрозуміти – як це працює і яка нам може бути від
цього користь. Щоб не заглиблюватися в системні тонкощі, представимо ситуацію на
відомих нам прикладах.

Програма, написана на Perl, містить декілька процедур. При виклику
процедури, інтерпретатор виконує послідовність операцій, які становлять
процедуру – тіло процедури. Тут важливо виділити що ця послідовність, мало
залежить від послідовності операторів іншої процедури. У процедурі можуть
бути визначені локальні змінні. Ці змінні так само не мають відношення до
змінним, використовуваним в інших процедурах, навіть якщо імена у них однакові.
При цьому, якась частина коду, яка знаходиться поза всяких процедур, управляє
роботою програми в цілому – коли потрібно викликає процедури, щось там ще
творить, і так далі.

Так от, можна привести аналогію з багатозадачного операційною системою: код
поза процедур тут буде представляти код ядра, який управляє всіма
процесами-процедурами. Але тут маленьке але важлива примітка. Коли в нашій
аналогії ядро ​​викликає процедуру це запуск процесу, але не її виконання та
очікування її завершення. Ядро десь там у себе в хеше процесів створює нову
пару ім'я-значення, ім'я якого є ім'я процедури, а значення – номер рядка
послідовності операторів процедури, на якій зупинився виконання (то
Тобто після запуску це 0). Після цього, ядро ​​перебирає всі ключі з хеша
дивиться на ім'я і номер рядка. Переходить на цей рядок в потрібній процедурі і
виконує, наприклад, наступні п'ять рядків. Далі наступна процедура, так до
кінця, а потім знову в початок хеша.

Ну от, у нас вийшла примітивна багатозадачна середу. Навіть її можна з
льоту ускладнити. Наприклад, додати поняття пріоритету процеса. У нашому прикладі,
чим вище пріоритет, тим більше рядків процедури виконуватиметься в кожній
ітерації перебору всіх процесів-процедур.

Насправді все набагато складніше. Але нам не обов'язково знати як працює
ядро операційної системи для того що б скористатися перевагами
многозадачной середовища. Достатньо знати, що процес – це програма,
виконується незалежно. Дуже важливо усвідомити, що процес це не тільки код, але
і дані. Навіть якщо запускаються дві однакові програми, дані вони, швидше
за все, будуть обробляти по різному.

З точки зору програмування, коли мова не йде про запуск іншого
програми, в Perl створення (породження) нового процесу – це створення дубліката
поточної програми. При цьому ще раз помічено, що дублюється не тільки код, але й
всі змінні з їх значеннями. Все робиться не просто, а дуже просто

fork;

і після цього оператора у вас виконуються два абсолютно однакових процесу.
Ну, звичайно від програми, що виконує оператор fork у такому вигляді, толку мало,
хіба що якщо ви хочете насолити системному адміністраторові, пожираючи системні
ресурси, запустивши цей оператор у нескінченному циклі.

Насправді, оператор fork повертає значення, яке використовується для
визначення процесу. Приміром, виходить людина з-за рогу, а йому цеглиною по
голові. Отямився, а в руці у нього на папірці написано – ти номер 1. Він встав і
пішов далі. Піти то пішов, та на його місці знову ж ця людина лежить. А у
нього в руці папірець, на якому написано – ти номер 2. Ця людина піднявся,
подумав, що зараз ось зайду за інший кут, а там знову цеглиною по голові
дадуть, і пішов в іншу сторону.

Так от, повернемося до наших баранів. Одному процесу дістається значення,
ідентифікує породжений процес. Тобто, дописуємо в папірець першим
людини фразу типу "у другого номер 2". Це буває необхідним у випадку якщо
мається на увазі взаємодія з породженим процесом. Породжений процес,
отримує від fork значення 0. Але можливі такі помилкові ситуації, коли створити
процес не вдалося. Повертаючись до нашого прикладу, людина заходить за кут і:
Нічого не відбувається. Людина думає, дивно, тут на мене повинен впасти
цегла, мабуть погода нельотна. Розгортається і йде додому. Коли fork НЕ
повертає значення (undef), значить породити процес не вдалося. Це,
безсумнівно, помилкова ситуація і її необхідно обробити. У загальному випадку виклик
fork повинен виглядати так

unless defined(fork) {
print "обробіть помилку, або замість цього викличте die"
}

Напевно вас вже перекрючіло, в марних спробах відривання пальців від улюбленої
клавіатури. Давайте спробуємо застосувати наші знання про fork на якому-небудь (як
завжди марно) прикладі.

#!/usr/bin/perl -w
# fork.pldie "Non-flying weather"
unless defined(fork);
print "I”m number $$
";

В результаті виконання ви побачите щось на зразок

[root@avalon tests]# ./fork.pl
I”m number 6773
I”m number 6774

Так як процеси повністю дублюються, кожен процес отримує власну
копію даних, включаючи файлові дескриптори і дескриптори потоків введення-виведення.
Таким чином, оператор print в обох процесах працює з одним і тим же
дескриптором. Якщо хто не знає, вбудована змінна $ $ містить ідентифікатор
поточного процесу Perl. До речі, в якості домашнього завдання, можете додати
код, що показує значення змінної $ $ до виконання fork. Тоді ви побачите,
який процес є батьківським і зробити висновок – в якій послідовності
два дубліката починають працювати.

Так, трохи не забув, важливо завершувати процес оператором exit. Ну, думаю, вистачить
з вас марної писанини. Пора зайнятися що небудь більш серйозним.

Фільтрація вихідних даних


Скільки пізнаю Perl, все не перестаю дивуватися. Стільки приємних сюрпризів
не зустрічав ще ніде. Сам мова настільки логічний (якщо можна так висловитися),
що відкриває всі свої таємниці уважному програмісту, причому без частого
звернення до документації. Ось і недавно, виникла у мене необхідність
відфільтрувати вихідні дані якоїсь CGI-програми:

Почекайте розминати пальці. Спочатку вип'ємо по гуртку кофею, а я, в цей час,
змалюю ситуацію в цілому. Що таке фільтр на вихідні дані всім зрозуміло? Ну
якщо комусь не зрозуміло, то знову ж таки – уявімо. Програма що то там виводить в
STDOUT (стандартний потік виводу), а в цей час якась інша програма тихо
і непомітно краде ці дані і робить з ними все що заманеться.
Реальний приклад? Ну найперше що прийшло мені в голову – це заміна всіх URL
на гіперпосилання. Або в допомогу расеянному програмісту, вічно забуває про
HTTP-заголовках, перевіряти наявність оних заголовків і додавати їх якщо потрібно. На
Насправді, все може бути набагато складніше. Наприклад, вирізування ненормативної
лексики (такий собі невидимий цензор) з тексту повідомлення, що відправляється за допомогою
WEB-інтерфейсу, перед тим, як воно буде передаватися на вхід SENDMAIL. Ну і в
такому дусі.

Загалом, перша наша мета, це якимось чином підсунути новий STDOUT,
який ми можемо прочитати, програмі, вивід від якої ми будемо фільтрувати. Але
тут можливі варіанти. Наприклад, може бути ми хочемо організувати вивід за типом
транзакції: або програма виконується до кінця, і виводиться весь вміст, або
ж, у разі помилки потрібно скинути дані, а вивести, наприклад, LOCATION на
сторінку обробки помилок. Тобто, все залежить від рівня контролю над
фільтровану вихідним потоком. Що б зовсім стало зрозуміло, про що я тут
розпинають, давайте напишемо простий прімерчік, що демонструє "тупий"
фільтр-нумератор рядків.

#!/usr/bin/perl -w
# nfilter.plfilter();
for ($I = 0; $I < 20; $I +){
print "Output line
";
}
sub filter{
die "Cannot fork"
unless defined($fpid = open(STDOUT,"-"));
return if ($fpid != 0);
num = 0;
while (<>) {
print "$num: $_";
$num ++;
}
exit;
}

Не здумайте запускати. Що, вже запустили? Тоді тисніть Ctrl + C. За-то тепер
назавжди запам'ятайте – потрібно закривати дескриптори (бажано все:). У чому ж
справа? Чому програма зависла? Всі породжені процеси є процесами
єдиного завдання. Потоки введення виведення автоматично закриваються, коли завершується
останній процес. Конструкція open (STDOUT, "-") неявно викликає fork. Згадайте
документацію по файлових операціях:

open (HANDLE, "$ cmd"); # направити інформацію на вхід програми

Так от, тут аналогічна ситуація, тільки в якості програми тут
створюється дочірній процес. А так як в якості дескриптора ми вказуємо
STDOUT, то в цьому процесі він перевизначається. Як і у випадку з fork,
щодо даних – дублюється їх стан на момент перед викликом fork.
Таким чином, в дочірній процес потрапляє нормальної не перевизначених
STDOUT. Зауважу, що open з вказаними аргументами неявно викликає fork, а в
як результат повертає ті ж самі значення, що і fork. Далі,
програма визначає в якому вона потоці – якщо не у породженому ($ fpid! = 0),
тоді повертається і емулює висновок рядків. Сам фільтр читає STDIN поки не
закінчаться дані. А дані закінчаться, коли потік введення буде закритий (для
батьківського процеса, це потік виводу). Батьківський процес вже завершив свою
роботу, а система чекає коли завершиться останній процес, що б закрити
потоки. І так далі, і так далі. Відчуваєте, де собака заритий? Після того, як
виведення рядків завершений, необхідно закрити потік висновку, що б фільтр,
приймає вихідні дані через потік введення вийшов з циклу

while (<>) { print "$num:	$_"; $num ++; }
Ось так то, беремо і правимо

#!/usr/bin/perl -w
# nfilter.plfilter();
for ($I = 0; $I < 20; $I +){
print "Output line
";
}close(STDOUT);
sub filter{
die "Cannot fork"
unless defined($fpid = open(STDOUT,"-"));
return if ($fpid != 0);
num = 0;
while (<>) {
print "$num: $_";
$num ++;
}
exit;
}


Ну це звичайно, дюже примітивно. Але якщо нам потрібно тільки лише фільтрувати,
потік виводу, то зійде. А от якщо ми, наприклад, пишемо супер-систему обробки
помилок, то цього все-таки замало. Уявімо, що такий собі сторож фільтрує
висновок і відразу відправляє його на справжній STDOUT. А якщо виникла фатальна
помилка? Ми виводимо повідомлення про помилку, але все це в догонку того непотребу, що вже
був відправлений у STDOUT. Така обробка помилок, як говориться, "що мертвому
припарка ". Можна, звичайно, накопичувати висновок всередині фільтру і виводити тільки
цілком. В разі чого, можна прибити породжений процес за допомогою оператора
kill. Але на жаль, потік виведення вже перевизначений безповоротно.

Для вирішення цієї проблеми ми повинні кардинально змінити свій світогляд.
Жартую, звичайно. Досить згадати про такі корисні функції як select і pipe.
Функція select підмінює STDOUT новим дескриптором, а повертає дескриптор
потоку виводу, який був актуальний на момент до виконання select, інакше кажучи
поточний STDOUT. Функція pipe пов'язує два дескриптора в режимі читання-запису,
тобто створює односторонній канал обміну даними. Звідси й назва – pipe.

Є дуже чудова властивість систем UNIX – всі потоки рівні. Прям,
комунізм якийсь. Ось дідусь Ленін би порадів. А нам яка з цього
користь? Ну як, ми, наприклад, легко можемо підсунути функції select один з
дескрипторів, пов'язаних функцією pipe. Природно, що будемо підсовувати той,
який призначений для запису, інакше я за наслідки не ручаюся. У загальному
така нехитра программуліна

#!/usr/bin/perl -w
# errfilter.plmy ($fpid,$oldout);
pipe(FOR_READ,FOR_WRITE);
select((select(FOR_READ),$ = 1)[0]);
select((select(FOR_WRITE),$ = 1)[0]);
start_filter();
for ($I = 0; $I < 20; $I +){
print "Output line
";
}
# Error("bug");
close(FOR_WRITE);
waitpid($fpid,0);
sub start_filter{
die "Cannot fork"
unless defined($fpid = fork);
unless ($fpid == 0){
close(FOR_READ);
$oldout = select(FOR_WRITE);
return;
}
close(FOR_WRITE);
my ($out,$num) = ("",0);
while() {
$out .= "$num $_"; $num ++;
}
close(FOR_READ);
print $out;
exit;
}
sub Error{
my ($error_text) = @_;
select($oldout);
kill KILL => $fpid;
close(FOR_WRITE);
print "Error: $error_text
";
exit;
}

Ну що, налили чергову порцію кави? Тоді приступимо до розбору польотів.
Насамперед, програма зв'язує два дескриптора FOR_READ і FOR_WRITE в канал з
допомогою pipe. При цьому, FOR_READ вийде тільки для читання, а FOR_WRITE,
відповідно, тільки для запису. Про наступні два рядки скажу тільки що вони
відключають буфферізациі. Для нас це поки не важливо, їх можна взагалі прибрати. Далі
викликається функція start_filter (). Ось на неї потрібно взгянуть з усією
уважністю.

Насамперед, функція створює дублікат процесу. У батьківському процесі
закривається непотрібний дескриптор для читання, а в якості STDOUT підсувається
один кінець створеного каналу, той який тільки для запису. Після цього
програма батьківський процес повертається до свого основного марної
заняття, а точніше – емулює висновок. Після того, як висновок завершено, не забуваємо
закрити дескриптор виводу, інакше все зависне через вищеописаної. Далі
чекаємо, коли дочірній потік завершиться (можете закоментувати цей рядок, і
подивитися що вийде). На цьому робота основного процесу завершена.

Повернемося до функції start_filter () в тій його частині, де виконується
безпосередньо фільтрація. Насамперед невикористовуваний в дочірньому процесі
кінець каналу закривається. Далі, процес у циклі зчитує дані з каналу і
конкатенірует їх у змінній $ out. Ну далі має бути все зрозуміло. Запустіть
програму. Працює? Принаймні повинна.

Тепер приберіть коментарі з рядка, де викликається функція Error ("bug").
Запустіть програму знову. Ну, який результат? Цього ми добивалися? (Правильний
відповідь так, якщо ні, то дивіться що ви там понаписували).

Давайте подивимося, що робить функція Error (). Насамперед відновлює
стандартний потік виводу. У цей час дочірній процес у режимі накопичення
даних нічого про це не підозрює. Наступна жорстока операція вбиває
дочірній процес. А дочірній процес ще нічого не вивів в потік виводу. Після
закривається дескриптор для запису, і виконується обробка помилки. Ну і що б
минути останні рядки головного процесу виконується exit.

От і все. Як вже ви будете застосовувати отриману інформацію на практиці, справа
ваше.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*