Зміна бінарного під час виконання


10

Я часто стикаюся з ситуацією, коли розробляю, де я запускаю бінарний файл, скажімо a.outна задньому плані, як це робить якусь тривалу роботу. Хоча це роблю, я вношу зміни до коду С, який знову створюється a.outта компілюється a.out. Поки що у мене не було проблем з цим. Процес, який запускається, a.outтриває як звичайно, ніколи не виходить з ладу і завжди запускає старий код, з якого він почався.

Однак, скажімо, це a.outбув величезний файл, можливо порівнянний з розміром ОЗУ. Що було б у цьому випадку? І скажіть, що він пов'язаний із спільним файлом об'єкта libblas.so, що робити, якщо я змінив libblas.soпід час виконання? Що б сталося?

Моє головне питання - чи гарантує ОС, що коли я запускаю a.out, тоді оригінальний код завжди буде працювати нормально, відповідно до оригінального двійкового .soфайла , незалежно від розміру бінарних файлів або файлів, на які він посилається, навіть коли ці .oта .soфайли були змінені під час час виконання?

Я знаю, що ці питання стосуються подібних питань: /programming/8506865/when-a-binary-file-runs-does-it-copy-its-entire-binary-data-into-memory -at-раз Що станеться, якщо ви редагуєте сценарій під час виконання? Як можливо зробити оновлення в реальному часі під час роботи програми?

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


На мене, питання, з якими ви зв’язувались (особливо це стосується переповнення стека), вже надають значну допомогу в розумінні цих наслідків (або їх відсутності). Оскільки ядро ​​завантажує вашу програму в текстові регіони / сегменти пам'яті , на неї не повинні впливати зміни, внесені через файлову підсистему.
Джон У. Сміт

@JohnWHSmith На Stackoverflow головна відповідь говорить if they are read-only copies of something already on disc (like an executable, or a shared object file), they just get de-allocated and are reloaded from their source, тож у мене склалося враження, що якщо ваш бінарний файл величезний, то, якщо частина вашого бінарного файлу виходить з оперативної пам'яті, але потім знову потрібна, вона "перезавантажується з джерела" - тому будь-які зміни в .(s)oфайл буде відображений під час виконання. Але, звичайно, я неправильно зрозумів - саме тому я задаю це більш конкретне питання
texasflood

@JohnWHSmith Також друга відповідь говорить, No, it only loads the necessary pages into memory. This is demand paging.тому я був насправді під враженням, що те, про що я попросив, не можна гарантувати.
texasflood

Відповіді:


11

Хоча на перший погляд здавалося, що питання щодо переповнення стека виявляється достатньою, я розумію, з ваших коментарів, чому ви все ще можете сумніватися з цього приводу. Для мене, саме така критична ситуація пов'язана, коли дві підсистеми 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 файлу, і тому ігнорує зміни, які ви внесли до його посилань (наприклад, асоціюючи їх з новими файлами).


Фантастична, детальна відповідь. Це пояснює мою плутанину. Так що я правильно вважаю, що через те, що inode все ще доступний, дані з вихідного бінарного файлу все ще знаходяться на диску, і тому використовувати dfдля визначення кількості вільних байтів на диску неправильно, оскільки він не приймає вставки, які чи були враховані всі посилання файлової системи ? Отже, я повинен використовувати df -i? (Це лише технічна цікавість, мені не потрібно точно знати точне використання диска!)
texasflood

1
Просто для уточнення для майбутніх читачів - моя плутанина полягала в тому, що я думав про виконання, весь бінарний файл буде завантажений в оперативну пам’ять, тому якби оперативна пам’ять була невеликою, то частина двійкового файлу залишила б оперативну пам’ять і доведеться перезавантажуватися з диска - що б викликати проблеми, якщо ви змінили файл. Але у відповіді було зрозуміло, що двійковий файл ніколи насправді не видаляється з диска, навіть якщо ви rmабо mvйого як вкладка до вихідного файлу не буде видалено, поки всі процеси не видалять посилання на цей інод.
texasflood

@texasflood Рівно. Після того, як всі шляхи будуть видалені, жоден новий процес ( dfвключений) не може отримати інформацію про inode. Будь-яка нова інформація, яку ви знайдете, пов’язана з новим файлом та новим inode. Основний момент тут полягає в тому, що підсистема процесів не зацікавлена ​​в цій проблемі, тому поняття управління пам'яттю (пейджинг попиту, заміна процесу, помилки сторінки, ...) абсолютно не мають значення. Це проблема файлової підсистеми, і її опікує файлова підсистема. Підсистема процесу з цим не турбується, це не для чого.
Джон У. Сміт

@texasflood Примітка про df -i: цей інструмент, ймовірно, отримує інформацію з суперблоку fs або його кеша, тобто, він може включати в себе старий бінарний файл (для якого всі посилання були видалені). Це не означає, що нові процеси можуть вільно використовувати ті старі дані.
Джон У. Сміт

2

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

Це, звичайно, має бути підтверджено.


2

Це не завжди буває при заміні .jar-файлу. Ресурси Jar та деякі завантажувачі класів відображення під час виконання не зчитуються з диска, поки програма явно не вимагає інформації.

Це лише проблема, оскільки jar - це просто архів, а не єдиний виконуваний файл, який відображається в пам'яті. Це злегка не зупиняється, але все одно залишається відгалуженням вашого питання, і я щось вистрілив у ногу.

Отже, для виконуваних файлів: так. Для jar файлів: можливо (залежно від реалізації).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.