Почему можно переместить работающую программу в Ubuntu?

24

Я только что понял, что могу переместить работающую активную программу в другой каталог. По моему опыту, это было невозможно в MacOs или Windows. Как это работает в Ubuntu?

Изменить: Я думал, что это не возможно на Mac, но, видимо, это возможно, как подтверждают комментарии. Это возможно только в Windows. Спасибо за все ответы.

n0.ob
источник
2
В значительной степени межсайтовый обман: stackoverflow.com/a/196910/1394393 .
jpmc26
1
Вы не можете rename(2)запустить исполняемый файл на OS X? Что происходит, вы получаете EBUSYили что-то? Почему это не работает? Страница man rename (2) не документирует ETXTBUSYэтот системный вызов, а только говорит о EBUSYвозможности переименования каталогов, поэтому я не знал, что система POSIX может даже запретить переименование исполняемых файлов.
Питер Кордес
3
Приложения macOS также можно перемещать во время работы, но не в мусор. Я полагаю, что после этого некоторые приложения могут ошибаться, например, если они хранят URL-адреса файлов для своих двоичных или связанных ресурсов где-то в виде переменной, а не генерируют такой URL-адрес через NSBundle et al. Я подозреваю, что это соответствие macOS POSIX.
Константино Царухас,
1
Это на самом деле работает так, как намеревается Linux, вы должны знать, что вы делаете. : P
userDepth
2
Я думаю, что другой способ думать об этом, почему это невозможно? То, что Windows не позволяет вам, не обязательно означает, что это принципиально невозможно из-за того, как работают процессы или что-то в этом роде.
Томас

Ответы:

32

Позвольте мне сломать это.

Когда вы запускаете исполняемый файл, выполняется последовательность системных вызовов, прежде всего fork()и execve():

  • fork()создает дочерний процесс вызывающего процесса, который (в основном) является точной копией родительского процесса, причем оба по-прежнему используют один и тот же исполняемый файл (используя страницы памяти копирования при записи, поэтому он эффективен). Он возвращает дважды: в родительском он возвращает дочерний PID. В дочернем процесс возвращает 0. Обычно дочерний процесс вызывает execve сразу же:

  • execve()принимает полный путь к исполняемому файлу в качестве аргумента и заменяет вызывающий процесс исполняемым файлом. В этот момент вновь созданный процесс получает свое собственное виртуальное адресное пространство, то есть виртуальную память, и выполнение начинается в его точке входа (в состоянии, определяемом правилами платформы ABI для свежих процессов).

На этом этапе загрузчик ELF ядра отобразил сегменты текста и данных исполняемого файла в память, как если бы он использовал mmap()системный вызов (с общими сопоставлениями только для чтения и частными сопоставлениями чтения и записи соответственно). BSS также отображается как будто с MAP_ANONYMOUS. (Кстати, я игнорирую динамическое связывание здесь для простоты: динамический компоновщик open()и mmap()все динамические библиотеки перед переходом к точке входа основного исполняемого файла.)

Только несколько страниц загружаются в память с диска до того, как вновь созданный exec () запустит собственный код. Другие страницы по требованию распределяются по мере необходимости, если / когда процесс касается этих частей своего виртуального адресного пространства. (Предварительная загрузка любых страниц кода или данных перед началом выполнения кода пользовательского пространства - это просто оптимизация производительности.)


Исполняемый файл идентифицируется индексом на нижнем уровне. После того, как файл начал выполняться, ядро ​​сохраняет содержимое файла без изменений по ссылке на inode, а не по имени файла, как для открытых файловых дескрипторов или отображений памяти с файловой поддержкой. Таким образом, вы можете легко переместить исполняемый файл в другое место файловой системы или даже в другую файловую систему. В качестве примечания, чтобы проверить различные характеристики процесса, вы можете заглянуть в /proc/PIDкаталог (PID - это идентификатор процесса данного процесса). Вы даже можете открыть исполняемый файл /proc/PID/exe, даже если он был удален с диска.


Теперь давайте раскопаем движение:

Когда вы перемещаете файл в пределах одной и той же файловой системы, выполняется системный вызов rename(), который просто переименовывает файл в другое имя, его индекс остается неизменным.

В то время как между двумя разными файловыми системами происходят две вещи:

  • Содержимое файла сначала копируется в новое местоположение, read()иwrite()

  • После этого файл отсоединяется от исходного каталога с помощью, unlink()и, очевидно, файл получит новый inode в новой файловой системе.

rmна самом деле просто unlink()-ing заданный файл из дерева каталогов, поэтому, имея разрешение на запись в каталог, вы получите достаточное право удалить любой файл из этого каталога.

А теперь представьте себе, что происходит, когда вы перемещаете файлы между двумя файловыми системами и у вас нет прав доступа к unlink()файлу из исходного кода?

Ну, файл сначала будет скопирован в место назначения ( read(), write()), а затем unlink()потерпит неудачу из-за недостаточного разрешения. Итак, файл останется в обеих файловых системах !!

heemayl
источник
5
Вы несколько путаете виртуальную и физическую память. Ваше описание того, как программа загружается в физическую память, является неточным. Системный вызов exec вовсе не копирует различные разделы исполняемого файла в физическую память, а загружает только тот, который необходим для запуска процесса. После этого требуемые страницы загружаются по запросу, возможно, через долгое время. Байты исполняемого файла являются частью виртуальной памяти процесса и могут быть прочитаны и, возможно, прочитаны снова в течение всей жизни процесса.
Jlliagre
@jlliagre Отредактировано, я надеюсь, теперь это прояснилось. Спасибо.
Heemayl
6
Заявление «процесс больше не использует файловую систему» ​​все еще сомнительно.
Jlliagre
2
Базовое понимание того, что данный файл в файловой системе не идентифицируется напрямую по имени файла, должно быть намного понятнее.
Турбьёрн Равн Андерсен
2
Все еще есть неточности в вашем обновлении. В mmapи unmapсистемные вызовы не используются для загрузки и выгрузки страниц по требованию, страницы загружаются ядром при доступе к их генерировать ошибку страницы, страницы выгружаются из памяти , когда операционная система чувствует RAM будет лучше использовать для чего - то еще. В этих операциях загрузки / выгрузки системный вызов не задействован.
Jlliagre
14

Ну, это довольно просто. Давайте возьмем исполняемый файл с именем / usr / local / bin / whoopdeedoo. Это только ссылка на так называемый inode (базовая структура файлов в файловых системах Unix). Это индекс, который помечается как «используемый».

Теперь, когда вы удаляете или перемещаете файл / usr / local / whoopdeedoo, единственное, что перемещается (или стирается), - это ссылка на индекс. Сам инод остается неизменным. Это в основном это.

Я должен это проверить, но я верю, что вы можете сделать это и в файловых системах Mac OS X.

Windows использует другой подход. Зачем? Кто знает...? Я не знаком с внутренностями NTFS. Теоретически, все файловые системы, которые используют ссылки на интенциональные структуры для имен файлов, должны быть в состоянии сделать это.

Признаюсь, я слишком упростил, но прочитал раздел «Последствия» в Википедии, который намного лучше меня работает.

jawtheshark
источник
1
Хорошо, если вы используете ярлык в Windows для запуска исполняемого файла, вы можете также стереть ярлык, если вы хотите сравнить его, может быть? = 3
Рэй
2
Нет, это было бы похоже на стирание символической ссылки. Где-то в других комментариях говорится, что такое поведение обусловлено устаревшей поддержкой файловых систем FAT. Это звучит как вероятная причина.
Jawtheshark
1
Это не имеет ничего общего конкретно с дескрипторами. NTFS использует записи MFT для отслеживания состояния файла, а FAT для этого использует записи каталога, но Linux по-прежнему работает с этими файловыми системами аналогично - с точки зрения пользователя.
Руслан
13

Одна вещь, которая кажется отсутствующей во всех других ответах, состоит в том, что: как только файл открыт и программа содержит открытый дескриптор файла, файл не будет удален из системы, пока этот дескриптор файла не будет закрыт.

Попытки удалить указанный inode будут отложены до тех пор, пока файл не будет закрыт: переименование в той же или другой файловой системе не может повлиять на открытый файл, независимо от поведения переименования, а также явного удаления или перезаписи файла новым. Единственный способ, которым вы можете испортить файл - это явно открыть его inode и связываться с содержимым, а не с операциями над каталогом, такими как переименование / удаление файла.

Более того, когда ядро ​​выполняет файл, оно сохраняет ссылку на исполняемый файл, и это снова предотвратит любую его модификацию во время выполнения.

Таким образом, в конце концов, даже если кажется, что вы можете удалять / перемещать файлы, из которых состоит запущенная программа, фактически содержимое этих файлов сохраняется в памяти до завершения программы.

Bakuriu
источник
1
Это неправильно. execve()не возвращает никакого FD, он просто выполняет программу. Так, например, если вы бежите , tail -f /foo.logто их это FD ( /proc/PID/fd/<fd_num>) , связанный с tailдля , foo.logно не для самого исполняемого файла, а tailне на его родителей , а также. Это верно и для отдельных исполняемых файлов.
Heemayl
@heemayl Я не упомянул, execveпоэтому не понимаю, насколько это актуально. Как только ядро ​​начинает выполнять файл, попытка заменить файл не изменит программу, которую ядро ​​собирается загрузить, отрисовывая точечный спор. Если вы хотите «обновить» исполняемый файл во время его работы, вы можете позвонить execveв какой-то момент, чтобы заставить ядро ​​перечитать файл, но я не понимаю, как это имеет значение. Дело в том, что удаление «запущенного исполняемого файла» на самом деле не вызывает удаления данных до тех пор, пока исполняемый файл не остановится.
Бакуриу
Я говорю об этой части, если программа состоит из одного исполняемого файла, как только вы начинаете выполнение, программа будет работать нормально независимо от каких-либо изменений в каталоге: переименование в той же или другой файловой системе не может повлиять на открытый обработчик , вы обязательно говорите о execve()и FD, когда в этом случае не участвует FD.
Heemayl
2
Вам не нужен дескриптор файла для того, чтобы иметь ссылку на файл - достаточно отображения страниц.
Саймон Рихтер
1
Unix не имеет "файловых дескрипторов". open()возвращает дескриптор файла , о котором здесь говорит Химейл execve(). Да, запущенный процесс имеет ссылку на свой исполняемый файл, но это не дескриптор файла. Возможно, даже если бы он munmap()отредактировал все свои сопоставления своего исполняемого файла, он все равно имел бы ссылку (отраженную в / proc / self / exe), которая не давала освобождать индекс. (Это было бы возможно без сбоев, если бы он делал это из библиотечной функции, которая никогда не возвращалась.) Кстати, усечение или изменение используемого исполняемого файла может дать вам ETXTBUSY, но может работать.
Питер Кордес
7

В файловой системе Linux, когда вы перемещаете файл, пока он не пересекает границы файловой системы (читай: остается на том же диске / разделе), все, что вы изменяете, - это индекс ..(родительский каталог) и новый каталог , Фактические данные вообще не перемещаются на диске, только указатель, чтобы файловая система знала, где их найти.

Вот почему операции перемещения выполняются так быстро и, вероятно, почему нет проблем с перемещением работающей программы, поскольку вы на самом деле не перемещаете саму программу.

I_GNU_it_all_along
источник
Похоже, ваш ответ подразумевает, что перенос исполняемого двоичного файла в другую файловую систему повлияет на выполнение процессов, запускаемых из этого двоичного файла.
Jlliagre
6

Это возможно, потому что перемещение программы не влияет на запущенные процессы, запускаемые при ее запуске.

После запуска программы ее биты на диске защищены от перезаписи, но нет необходимости защищать файл для переименования, перемещения в другое место в той же файловой системе, что эквивалентно переименованию файла или перемещению в другую файловую систему, что эквивалентно копированию файла в другом месте и удалению его.

Удаление используемого файла: либо из-за того, что у процесса открыт дескриптор файла, либо из-за того, что процесс выполняет его, не удаляет данные файла, на которые ссылается индекс файла, а только удаляет запись каталога, т. е. путь, с которого может быть достигнут индекс.

Обратите внимание, что запуск программы не загружает все сразу в (физическую) память. Напротив, загружается только строгий минимум, необходимый для запуска процесса. Затем требуемые страницы загружаются по требованию в течение всей жизни процесса. это называется поиском по запросу. Если имеется нехватка ОЗУ, ОС может освободить ОЗУ, содержащее эти страницы, поэтому вполне возможно, что процесс может несколько раз загрузить одну и ту же страницу из исполняемого inode.

Первоначально причина, по которой это было невозможно с Windows, скорее всего связана с тем, что базовая файловая система (FAT) не поддерживала концепцию разделения записей каталога по сравнению с inode. Это ограничение больше не присутствовало в NTFS, но дизайн ОС сохранялся в течение длительного времени, что приводило к неприятному ограничению необходимости перезагрузки при установке новой версии двоичного файла, что больше не относится к последним версиям Windows.

jlliagre
источник
1
Я считаю, что более новые версии Windows могут заменить используемые двоичные файлы без перезагрузки.
Турбьёрн Равн Андерсен
@ ThorbjørnRavnAndersen Интересно, почему все обновления все еще требуют перезапуска :(
Брайам
1
@Braiam Они не. Присмотритесь. Даже если двоичные файлы могут быть обновлены, ядро ​​не может (насколько мне известно) и требует перезагрузки для замены на более новую версию. Это справедливо для большинства ядер операционной системы. Более умные люди, чем я, написали kpatch для Linux, который может исправлять ядро ​​Linux во время работы - см. En.wikipedia.org/wiki/Kpatch
Торбьерн Равн Андерсен
@ ThorbjørnRavnAndersen Я имел в виду «все обновления Windows»
Брайам
@ Брайам, да, я тоже. Пожалуйста, присмотрись поближе.
Турбьерн Равн Андерсен
4

По сути, в Unix и его аналогах имя файла (включая ведущий к нему путь к каталогу) используется для связывания / поиска файла при его открытии (выполнение файла - это один из способов его открытия). После этого момента личность файла (через его "inode") устанавливается и больше не подвергается сомнению. Вы можете удалить файл, переименовать его, изменить его разрешения. Пока у любого процесса или пути к файлу есть дескриптор этого файла / индекса, он будет зависеть, как канал между процессами (фактически, в исторической UNIX канал был безымянным индексом с размером, который только что помещался в «Прямые блоки» ссылки на дисковое хранилище в inode, что-то вроде 10 блоков).

Если у вас есть средство просмотра PDF, открытое для файла PDF, вы можете удалить этот файл и открыть новый файл с тем же именем, и, пока старый просмотрщик открыт, он все равно будет нормально обращаться к старому файлу (если он не просматривает активно). файловая система, чтобы заметить, когда файл исчезает под своим первоначальным именем).

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

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

user584745
источник