Процеси

Віктор Хименко, Журнал "Світ ПК"

в системі

Розповідь про життя процесів природно почати з самого початку – з їх появи на світ. Так ось, процеси розмножуються … брунькуванням: системний виклик Linux, створює новий процес, називається clone, а дочірній процес являє собою майже точну копію батьківської. Тільки далі він виконує призначену йому функцію, а вихідний процес – те, що написано в програмі після виклику clone. Потім відмінностей може стати більше, так що шляхи-дороги процесів здатні розійтися досить далеко. Але якщо нам потрібно цьому перешкодити, виклик clone дозволить поставити прапори, які вказують, що породжений процес буде мати зі своїм предком загальні:

Нитка і завдання

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

Крім процесів описаного вище виду бувають ще "ущербні", що породжуються за допомогою функції kernel_thread для внутрішніх системних потреб. У них немає параметрів командного рядка, як правило, вони не мають відкритих файлів і т. д. Оскільки, незважаючи на свою ущербність, ці процеси все одно фігурують у списку завдань, у літературі іноді розрізняють повноцінні процеси, породжені з "простору користувача" (userspace), і завдання, тобто всі процеси, включаючи внутрішні процеси ядра.

Процес і програма

Ви скажете: всі це чудово, але якщо новий процес – завжди копія існуючого, то яким чином у системі примудряються працювати різні програми? І звідки береться найперша з них?

, що виконують різні програми, утворюються завдяки застосуванню наявних в стандартній бібліотеці Unix функцій "сімейства exec": execl, execlp, execle, execv, execve, execvp. Ці функції відрізняються форматом виклику, але в кінцевому підсумку роблять одну й ту ж річ: заміщають всередині поточного процесу виконуваний код на код, що міститься у вказаному файлі. Файл може бути не тільки двійковим виконуваним файлом Linux, але і скриптом командного інтерпретатора, й двійковим файлом іншого формату (наприклад, класом java, виконуваним файлом DOS). В останньому випадку спосіб його обробки визначається налаштованим модулем ядра під назвою binfmt_misc.

Таким чином, операція запуску програми, яка в DOS і Windows виконується як єдине ціле, в Linux (і в Unix взагалі) розділена на дві: спочатку здійснюється запуск, а потім визначається, яка програма буде працювати. Чи є в цьому сенс і не занадто великі накладні витрати? Адже створення копії процесу передбачає копіювання дуже значного обсягу інформації.

Сенс у цьому підході, є. Дуже часто програма повинна зробити деякі дії ще до того, як почнеться власне її виконання. Скажімо, в розбирали вище прикладі ми запускали два програми, що передають один одному дані через неіменованим канал. Такі канали створюються системним викликом pipe; він повертає пару файлових дескрипторів, з якими в нашому випадку виявилися пов'язані стандартний потік введення (stdin) програми wc і стандартний потік виводу (stdout) програми dd. Стандартний вивід wc (як, до речі, і стандартний введення dd, хоча він ніяк не використовувався) зв'язувався з терміналом, а крім того, було потрібно, щоб командний інтерпретатор після виконання команди не втратив зв'язок з терміналом. Як вдалося цього досягти? Та дуже просто: спочатку були розплодив процеси, потім пророблені необхідні маніпуляції з дескрипторами файлів і тільки після цього викликаний exec.

Аналогічного результату (як показує, зокрема, приклад Windows NT) можна було б домогтися і при запуску програми за один крок, але більш складним шляхом. Що ж стосується накладних витрат, то вони частіше всього опиняються пренебрежимо малими: при створенні копії процесу його індивідуальні дані фізично нікуди не копіюються. Замість цього використовується техніка, відома під назвою copy-on-write (копіювання при записі): сторінки даних обох процесів особливим чином позначаються, і тільки тоді, коли один процес намагається змінити вміст будь-якої своєї сторінки, вона дублюється.

Лістинг 2. Закінчення процедури ініціалізації ядра Linux

if (execute_command)
execve(execute_command,argv_init, envp_init);
execve("/sbin/init",argv_init,envp_init);
execve("/etc/init",argv_init,envp_init);
execve("/bin/init",argv_init,envp_init);
execve("/bin/sh",argv_init,envp_init);
panic("No init found.  Try passing init= option to kernel.");}

Перший процес у системі запускається при ініціалізації ядра. Мабуть, навіть людині, не вміє програмувати, достатньо поглянути на кінець процедури ініціалізації ядра Linux (див. лістинг 2), щоб зрозуміти, як визначається виконувана в цьому процесі програма: спочатку робиться спроба "перемкнути" процес на файл, вказаний в командному рядку ядра (є й така …), потім на файли / sbin / init, / Etc / init, / bin / init й наостанок на / bin / sh.

Смерть процесу

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

Звернімося ще раз до прикладу, розглянутого вище. Коли ми натисканням <Ctrl> + C примусово завершили виконання програм dd і wc, відповідні процеси були знищені, і на екрані з'явилося запрошення командного інтерпретатора. Поки програми працювали, запрошення не було: інтерпретатор перебував у стані очікування, в яке перейшов, пославши спеціальний системний виклик (насправді таких викликів існує декілька: wait, waitpid, wait3, wait4). Після закінчення роботи програм виклик повернув управління інтерпретатору, і той видав на термінал запрошення.

Якщо батьківський процес з якоїсь причини завершиться раніше дочірнього, останній стає "сиротою" (orphaned process). "Сироти" автоматично "усиновлюються" програмою init, виконується в процесі з номером 1, яка і приймає сигнал про їх завершення.

Якщо ж нащадок вже завершив роботу, а предок не готовий прийняти від системи сигнал про цю подію, то нащадок не зникає повністю, а перетворюється на "зомбі" (zombie); у полі Stat такі процеси позначаються буквою Z. Зомбі не займає процесорного часу, але рядок у таблиці процесів залишається, і відповідні структури ядра не звільняються. Після завершення батьківського процесу "осиротілий" зомбі на короткий час також стає нащадком init, після чого вже "остаточно вмирає".

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

Про сигнали

Стривайте, але ж запрошення командного інтерпретатора з'явилося і тоді, коли ми натиснули <Ctrl> + Z, хоча програми не закінчували роботу, і, отже, виклик wait * не міг повернути управління! Видача повідомлення Stopped (процес зупинений) і потім запрошення до введення була реакцією на сигнал CHLD, який ядро посилає при натисканні <Ctrl> + Z предкам – в даному випадку одному предку – процесів, працюють з терміналом (самі процеси отримують свій сигнал).

Сигнали посилаються одними процесами іншим за допомогою команди, яка носить назву страхітливе kill, хоча в загальному випадку нікого не вбиває. Все залежить від конкретного сигналу, і практично будь-який сигнал при необхідності може бути процесом проігноровано. Виняток становлять KILL, який "без розмов" знищує процес, і STOP, який його аналогічним чином зупиняє.

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

Робота з нитками вимагає особливої техніки, оскільки одні сигнали повинні "доводитися до відома" всіх ниток, а інші – надсилатися індивідуально. У Linux 2.2 це робилося шляхом досить хитрих маніпуляцій зі спеціальною ниткою, єдиним призначенням якої було управління іншими нитками. У версії 2.4 ядро може стежити за нитками за рахунок нового прапора CLONE_PARENT (таким чином, якщо одна нитка породить іншу і закінчить роботу, то породжена нитка не залишиться "сиротою") і декількох спеціальних правил доставки сигналів, так що потреба в спеціальній нитки відпала.

Комп'ютерна демонологія

Демоном (daemon) в Unix (і в Linux) називається процес, призначений для роботи у фоновому режимі без терміналу і виконує будь-які дії для інших процесів (не обов'язково на вашій машині). Зазвичай демони тихо займаються своєю справою, і згадують про них лише у випадку яких-небудь неполадок у їх роботі: наприклад, демону починає бракувати місця, і він посилає користувачеві повідомлення про це, чи демон перестає працювати, і вам дзвонить бос з питанням, чому у нього принтер знову не друкує і коли це припиниться …

На багатьох машинах демони, обслуговуючі процеси інших комп'ютерів, потрібні досить рідко, так що тримати їх у пам'яті постійно завантаженими і тринькати на це ресурси системи нераціонально. Для управління їх роботою був створений супердемон, якого звуть зовсім не Вельзевулом (в комп'ютерних демонів взагалі мало "демонічного" – вони ближче демонам Максвелла), а куди скромніше – inetd (що, як ви здогадалися, є скороченням від Internet daemon).

У конфігураційному файлі inetd (/ etc / inetd.conf) записано, який демон обслуговує звернення до будь сервісу Internet. Звичайно за допомогою inetd викликаються програми pop3d, imap4d, ftpd, telnetd (надаю читачеві визначити, які саме сервіси вони обслуговують) і деякі інші. Ці програми не є постійно активними, а отже, не можуть вважатися демонами в строгому сенсі слова, але оскільки вони народжуються "повноцінним" демоном, їх все одно так називають.

Просунуті засоби спілкування

посилають один одному сигнали, передають дані через неіменовані і іменовані канали, а також "гнізда". Все це чудово, але як бути, якщо один процес повинен передавати іншому величезні обсяги інформації і притому швидко (це потрібно, наприклад, при відтворенні відео)? Чи можуть процеси, адресні простори яких суворо розділені, яким-небудь чином отримати в спільне користування частину пам'яті? Так, за допомогою тимчасових файлів.

Для передачі великих масивів даних між процесами служить системний виклик mmap, що представляє собою досить несподіване застосування сторінкової віртуальної пам'яті. Він дозволяє, грубо кажучи, сказати: "Я хочу звертатися до такого-то ділянці такого-то файлу як до оперативної пам'яті". Дані, які процес читає з вказаної області пам'яті, у міру потреби зчитуються з файлу, а ті, які він туди пише, коли-небудь потраплять на диск. Але процес сам не працює з диском, цим займається ядро.

Якщо два процеси звертаються таким чином до одного й того ж ділянці одного і того ж файлу, дані будуть передані безпосередньо від одного процесу до іншого. Звичайно, періодично ядро скидає дані на диск. У деяких випадках це корисно, але коли mmap забезпечує тільки спілкування процесів між собою, обмін з диском лише уповільнює роботу. Для процесів, що мають спільного предка, можна використовувати прапор MAP_ANONYMOUS, який вказує, що дані не повинні потрапляти в файл (дескриптор файлу тоді ніяк не використовується і може бути будь-яким).

Виклик mmap застосовується також для "завантаження в пам'ять" виконуваних файлів і бібліотек, так що якщо програма використовує 25 бібліотек загальним обсягом у багато десятків мегабайт, це зовсім не означає, що вона і в пам'яті буде займати таку ж кількість мегабайт.

За допомогою тимчасових файлів можна, крім того, синхронізувати роботу процесів, використовуючи можливості системи, призначені для роботи з рекомендаційними (advisory) блокуваннями файлів. Це дозволяють зробити системні виклики fcntl і його більш швидкий і простий варіант flock.

Іноді створювати тимчасові файли небажано, тому в Linux включені також функції для спілкування процесів з Unix SVR4 (Unix System V Release 4). Це shmget – створення області пам'яті для спілкування процесів, semget – створення семафора, msgget – створення черги повідомлень. У версії 2.4 до них додалися ще більш потужні функції mq_open, shm_open з SUS2 (Single Unix Specification Version 2).

Отримання інформації про процеси

Для роботи з інформацією про процеси, яку виводять на термінал програми ps і top, в Linux використовується досить незвичайний механізм: особлива файлова система procfs. У більшості дистрибутивів вона монтується при запуску системи як каталог / proc. Дані про процес з номером 1 (зазвичай це / sbin / init) містяться в підкаталозі / proc / 1, про процес з номером 364 – в / proc/364, і т. д. Всі файли, відкриті процесом, представлені у вигляді символічних посилань у каталозі / proc / <pid> / fd, а посилання на кореневий каталог процесу зберігається як / proc / <pid> / root.

З часом у файлової системи procfs з'явилися й інші функції. Наприклад, командою

echo 100000 > /proc/sys/fs/file-max

root може визначити, що в системі дозволяється відкрити до 100 000 файлів, а команда


echo 0 > /proc/sys/kernel/cap-bound

відніме у всіх процесів в системі всі додаткові права, тобто фактично позбавить систему поняття "суперкористувач".

Корисну інформацію дозволяє отримати програма lsof, яка видає список всіх файлів, що використовуються зараз процесами, включаючи каталоги, зайняті тому, що будь-який процес використовує їх як поточного або кореневого; колективні бібліотеки, завантажені в пам'ять, і т. д.

У наступній статті ми поговоримо про командний інтерпретатор, його ролі в системі і взагалі про те, як з окремих процесів і файлів складається щось єдине.

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


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

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

Ваш отзыв

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

*

*