Хоча на перший погляд здавалося, що питання щодо переповнення стека виявляється достатньою, я розумію, з ваших коментарів, чому ви все ще можете сумніватися з цього приводу. Для мене, саме така критична ситуація пов'язана, коли дві підсистеми UNIX (процеси та файли) спілкуються.
Як ви можете знати, системи UNIX зазвичай поділяються на дві підсистеми: файлова підсистема та підсистема процесу. Тепер, якщо це не доручено інакше через системний виклик, ядро не повинно взаємодіяти між собою ці дві підсистеми. Однак є один виняток: завантаження виконуваного файлу в текстові області процесу . Звичайно, можна стверджувати, що ця операція також ініційована системним викликом ( execve
), але, як правило, це єдиний випадок, коли підсистема процесу робить неявний запит до файлової підсистеми.
Оскільки підсистема процесу, природно, не може обробляти файли (інакше не було б сенсу розділяти все на два), вона повинна використовувати все, що надає файлова підсистема для доступу до файлів. Це також означає, що підсистема процесу подається з будь-яким заходом, який приймає файлова підсистема щодо видання / видалення файлу. У цьому питанні я рекомендую прочитати відповідь Гілла на це питання U&L . Решта моєї відповіді ґрунтується на цій більш загальній від Жилля.
Перше, що слід зазначити, це те, що внутрішньо файли доступні лише через inode . Якщо ядро буде надано шлях, його першим кроком буде переведення його в inode, який буде використовуватися для всіх інших операцій. Коли процес завантажує виконуваний файл у пам'ять, він робить це через його inode, який був наданий підсистемою файлів після перекладу шляху. Вузоли можуть бути пов'язані з кількома шляхами (посиланнями), а програми можуть видаляти лише посилання. Щоб видалити файл та його inode, userland повинен видалити всі існуючі посилання на цей inode та забезпечити його повне використання. Коли ці умови будуть виконані, ядро автоматично видалить файл з диска.
Якщо ви подивитеся на заміну виконуваних файлів , що відповідає відповіді Гілла, ви побачите, що залежно від того, як ви редагуєте / видаляєте файл, ядро буде реагувати / адаптуватися по-різному, завжди через механізм, реалізований у файловій підсистемі.
- Якщо ви спробуєте стратегію одну ( відкрити / скоротити до нуля / записати або відкрити / записати / скоротити до нового розміру ), ви побачите, що ядро не буде турбувати обробку вашого запиту. Ви отримаєте помилку 26: Текстовий файл зайнятий (
ETXTBSY
). Ніяких наслідків немає.
- Якщо ви спробуєте стратегію другу, перший крок - видалити свій виконуваний файл. Однак, оскільки він використовується в процесі, файлова підсистема запустить і запобіжить справжньому видаленню файлу (та його inode) з диска. З цього моменту єдиний спосіб отримати доступ до вмісту старого файлу - це зробити це через його inode. Це те, що робить підсистема процесу, коли потрібно завантажувати нові дані в текстові розділи (внутрішньо, немає сенсу використовувати шляхи, за винятком при перекладі їх у вузли). Незважаючи на те, що ви від’єдналисяфайл (видалив усі його шляхи), процес все ще може використовувати його так, як ніби ви нічого не зробили. Створення нового файлу зі старим шляхом нічого не змінює: новому файлу буде наданий абсолютно новий inode, про який запущений процес не знає.
Стратегії 2 і 3 є безпечними і для виконуваних файлів: хоча запущені виконавчі файли (і динамічно завантажені бібліотеки) не є відкритими файлами в сенсі дескриптора файлів, вони поводяться дуже схожим чином. Поки якась програма працює з кодом, файл залишається на диску навіть без запису каталогу.
- Стратегія три досить схожа, оскільки
mv
операція є атомною. Можливо, це зажадає використання rename
системного виклику, і оскільки процеси не можуть бути перервані в режимі ядра, нічого не може перешкоджати цій операції, поки вона не завершиться (успішно чи ні). Знову-таки, змін індексу старого файлу не відбувається: створюється новий, і вже запущені процеси не матимуть цього знання, навіть якщо він був пов'язаний з одним зі старих посилань inode.
За допомогою стратегії 3, крок переміщення нового файлу до існуючого імені видаляє запис каталогу, що веде до старого вмісту, і створює запис каталогу, що веде до нового вмісту. Це робиться за одну атомну операцію, тому ця стратегія має головну перевагу: якщо процес відкриє файл у будь-який час, він побачить або старий вміст, або новий вміст - немає ризику отримати змішаний вміст або файл не існуючі.
Перекомпіляція файлу : при використанні gcc
(і поведінка, ймовірно, схожа на багато інших компіляторів), ви використовуєте стратегію 2. Це можна побачити, запустивши a strace
ваших процесів компілятора:
stat("a.out", {st_mode=S_IFREG|0750, st_size=8511, ...}) = 0
unlink("a.out") = 0
open("a.out", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
chmod("a.out", 0750) = 0
- Компілятор визначає , що файл вже існує через
stat
і lstat
системні виклики.
- Файл від’єднано . Тут, поки він більше не доступний через ім'я
a.out
, його введення та вміст залишаються на диску, доки вони використовуються вже запущеними процесами.
- Створюється новий файл і робиться його виконуваним під назвою
a.out
. Це абсолютно новий inode та абсолютно новий вміст, про який вже запущені процеси не хвилюються.
Тепер, коли мова йде про спільні бібліотеки, буде застосовуватися та ж поведінка. Поки об'єкт бібліотеки використовується процесом, він не буде видалений з диска, незалежно від того, як ви зміните його посилання. Кожного разу, коли щось потрібно завантажувати в пам'ять, ядро буде робити це через inode файлу, і тому ігнорує зміни, які ви внесли до його посилань (наприклад, асоціюючи їх з новими файлами).