Удивлен поведением cp с жесткими ссылками

20

Я очень хорошо понимаю понятие жестких ссылок и несколько раз читал справочные страницы по основным инструментам, таким как cp- и даже последние спецификации POSIX. Тем не менее я был удивлен, наблюдая следующее поведение:

$ echo john > john
$ cp -l john paul
$ echo george > george

На данный момент johnи paulбудет иметь одинаковый инод (и контент), и georgeбудет отличаться в обоих отношениях. Теперь мы делаем:

$ cp george paul

В этот момент я ожидал georgeи paulиметь разные номера индексных дескрипторов , но то же самое содержание --- это ожидание сбылось --- но я также ожидал , paulчтобы теперь имеют разное количество индексных дескрипторов из john, и johnдо сих пор содержание john. Это где я был удивлен. Оказывается, что копирование файла в путь назначения paulтакже приводит к установке того же файла (того же самого inode) по всем другим путям назначения, которые совместно используют paulinode. Я думал, что cpсоздает новый файл и перемещает его в место, ранее занимаемое старым файлом paul. Вместо этого он, похоже, открывает существующий файл paul, обрезает его и пишетgeorgeсодержание в этот существующий файл. Следовательно, любые «другие» файлы с таким же индексом обновляются «своим» содержимым одновременно.

Хорошо, это систематическое поведение, и теперь, когда я знаю, что ожидать этого, я могу понять, как обойти это или воспользоваться этим, в зависимости от обстоятельств. Что меня озадачивает, когда я должен был увидеть это поведение задокументированным? Я был бы удивлен, если бы это не было документировано где-то в документах, на которые я уже смотрел. Но, видимо, я пропустил это, и теперь не могу найти источник, который обсуждает это поведение.

dubiousjim
источник

Ответы:

4

Во-первых, почему это так? Одна историческая причина: так было в Unix First Edition .

Файлы взяты парами; первый открывается для чтения, второй создает режим 17. Затем первый копируется во второй.

«Создан» относится к creatсистемному вызову (тот, в котором отсутствует класс e ), который усекает существующий файл по заданному имени, если он существует.

А вот исходный код cpUnix Second Edition (я не могу найти исходный код First Edition). Вы можете увидеть вызовы openдля исходного файла и creatдля второго файла; и, как усовершенствование первого издания, если второй файл является существующим каталогом, cpсоздает файл в этом каталоге.

Но вы можете спросить, почему это было сделано так в то время? Ответ на вопрос «почему Unix изначально так поступил» почти всегда прост. cpоткрывает свой источник для чтения и создает его назначение - и системный вызов для создания файла перезаписывает существующий файл, открывая его для записи, потому что это позволяет вызывающей стороне навязывать содержимое файла по заданному имени, независимо от того, существовал ли файл или не.

Теперь о том, где это задокументировано: на странице руководства FreeBSD .

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

Эта формулировка присутствовала по крайней мере еще в 1990 году (тогда, когда BSD был 4.3BSD). На Solaris 10 есть похожая формулировка :

Если целевой_файл существует, cp перезаписывает его содержимое, но режим (и ACL, если применимо), владелец и связанная с ним группа не изменяются.

Ваш случай даже прописан в руководстве HP-UX 10 :

Если new_file является ссылкой на существующий файл с другими ссылками, перезаписывает существующий файл и сохраняет все ссылки.

POSIX переводит его в стандартный. Цитирование из Single UNIX v2 :

Если существует файл dest_file, предпринимаются следующие шаги: (…) Дескриптор файла для файла dest_file будет получен путем выполнения действий, эквивалентных функции open () спецификации XSH, вызываемой с использованием dest_file в качестве аргумента пути, и побитового включения OR для O_WRONLY и O_TRUNC в качестве аргумента офлаг.

Страницы руководства и спецификация, которую я цитировал далее, указывают, что если -fопция пропущена и попытка открыть / создать целевой файл не удалась (как правило, из-за отсутствия разрешения на запись файла), она cpпытается удалить цель и снова создать файл. , Это сломало бы жесткую ссылку в вашем сценарии.

Возможно, вы захотите сообщить об ошибке документации в руководстве GNU coreutils , так как оно не документирует это поведение. Даже описание --preserve=links, которое в вашем сценарии приведет к paulудалению ссылки и созданию нового файла, не дает четкого представления о том, что происходит без --preserve=links. Описание -fтипа подразумевает то, что происходит без него, но не объясняет его («Если копирование без этой опции и существующий конечный файл не может быть открыт для записи, копирование завершается неудачно. Однако с --force,…»).

Жиль "ТАК - перестать быть злым
источник
почему вы говорите "потому что это позволяет вызывающей стороне вступать во владение именем файла, существует ли файл уже или нет"? Cp не становится владельцем уже существующего файла.
jrw32982 поддерживает Монику
@ jrw32982 Я имел в виду владение в смысле решения, что входит в файл, а не владение в смысле метаданных файла. Я переписал это предложение.
Жиль "ТАК - перестань быть злым"
20

cpдокументы, которые перезаписывают файл назначения, если файл назначения уже присутствует. Вы правы в том, что в нем не указано подробно, что означает «перезаписать», но определенно говорится «перезаписать», а не «заменить». Если вы хотите быть педантичным, вы можете утверждать, что «перезапись» - это именно то, что cpнужно, и ожидаемое поведение будет правильно называться «заменить».

Также обратите внимание, что если cp«заменить» уже существующие целевые файлы, это может считаться неожиданным или неправильным, скорее, чем «перезапись». Например:

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

Итак, в заключение: теперь вы знаете, что на cpсамом деле. Вы никогда не будете удивлены этим снова! Честно говоря, я думаю, что то же самое могло случиться и со мной много лет назад.

Celada
источник
Нужно проверить ссылку на POSIX, но на самом деле manстраницы для cpверсий BSD (по крайней мере, OSX) и Gnu cpне так явно говорят о «перезаписи». Это слово используется только в комментариях к опциям -iи -n. Мануальная страница Gnu особенно неинформативна, Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.In the first synopsis form, the cp utility copies the contents of the source_file to the target_file.
поскольку,
Начинается информационная страница Gnu coreutils:‘cp’ copies files (or, optionally, directories). The copy is completely independent of the original.
dubiousjim
2
Я вижу, что стандарт POSIX 2008 действительно определяет наблюдаемое поведение; Я добавлю ответ.
Субъект
16

Я вижу, что стандарт POSIX 2013 действительно определяет наблюдаемое поведение . Это говорит:

  1. Если исходный_файл имеет тип обычный файл, должны быть предприняты следующие шаги:

    а. ... если файл dest_file существует, должны быть предприняты следующие шаги:

    я. Если -iопция включена, cpутилита должна написать подсказку к стандартной ошибке и прочитать строку из стандартного ввода. Если ответ не является положительным, cpбольше ничего не нужно делать с source_file и переходить к оставшимся файлам.

    II. Файл дескриптор для dest_file должен быть получен путем выполнения действия эквивалентно open()функции , определенной в объеме системы Интерфейсы POSIX.1-2008 вызывается с использованием dest_file в качестве аргумента пути, и побитовый включены ORв O_WRONLYи в O_TRUNCкачестве Oflag аргумента.

    III. Если попытка получить файловый дескриптор не удалась и эта -fопция действует, она cpдолжна попытаться удалить файл, выполнив действия, эквивалентные unlink()функции, определенной в томе «Системные интерфейсы» в POSIX.1-2008, который вызывается с использованием dest_file в качестве аргумента пути. Если эта попытка успешна, cpследует продолжить с шага 3b.

    ...

    д. Содержимое source_file должно быть записано в дескриптор файла. Любые ошибки записи должны привести cpк записи диагностического сообщения в стандартную ошибку и перейти к шагу 3e.

    е. Описатель файла должен быть закрыт.

dubiousjim
источник
1
Интересный. Как и вы, я предполагал, cpчто даст аналогичные результаты mvи сломает любые жесткие ссылки, частью которых является dest. Но теперь, когда я думаю об этом, это будет означать, что он должен был бы специально предназначаться unlink(2)для target ( cp -f), или создать временное имя с другим именем, а затем rename(2)это. Простая реализация состоит в том, чтобы просто открыть файл для перезаписи, чего требует POSIX. Это эквивалентноcat src > dest
Питер Кордес
2

Если вы можете сказать: «Копирование файла по пути назначения paul также копирует тот же файл (тот же самый inode) во все другие пути назначения, которые совместно используют paulinode». Мне жаль говорить, что вы не понимаете понятие жесткие ссылки очень хорошо. Если я дам яблоко сэру Маккартни, я дам яблоко Полу, и я дал яблоко партнеру по написанию песен Джона Леннона. Но я не выдал три яблока; Я дал яблоко человеку, у которого есть несколько имен / названий / дескрипторов.

Точно так же, когда вы копируете georgeк paul, вы не также копировать его john. Скорее, вы копируете georgeданные в файл, на индекс которого указывает paulзапись каталога.

Шаг за шагом:   когда вы делаете

echo john > john

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

cp -l john paul

или

ln john paul

вы не создали новый файл; скорее, вы дали существующему файлу новое имя. Теперь у вас есть файл с двумя именами: johnи paul. И когда вы говорите

cp george paul

Вы перезаписываете этот файл . Тот факт, что у него есть два имени, не имеет значения; он может иметь 42 имени, возможно, в местах, к которым вы даже не можете получить доступ, и эта команда не будет копировать george\nданные во все эти имена (пути); это просто копирование данных в один файл с несколькими именами.

Скотт
источник
1
Спасибо. Правильно, я знал о том, что для написания писем мне нужны символы-кавычки, johnи я paulначинал как два пути к одному и тому же файлу. Но это был самый простой способ выразить себя. Я не думаю, что само понятие жесткой связи, правильно понятое, диктует любое из двух вариантов поведения cp(без -l).
Субъект
Но спасибо за подталкивание; Я пытался уточнить формулировку.
Субъект