mv --exchange в Linux: атомарная замена двух файлов одной командой

Каждый, кто хоть раз менял местами два файла в Linux, делал это в три шага: первый файл — во временное имя, второй — на его место, временный — на место второго. Между первым и вторым mv один из файлов просто не существует, а если вторая команда упадёт, разбираться с половиной обмена придётся вручную. В GNU coreutils начиная с версии 9.5 есть опция mv --exchange, которая делает то же самое одной командой.

Как работает mv --exchange

Опция говорит mv поменять два пути местами за одну операцию. В Linux под ней используется системный вызов ядра renameat2() с флагом RENAME_EXCHANGE, поэтому обмен атомарный: в любой момент оба имени существуют и указывают на файл. Промежуточного состояния, когда nginx.conf пропал, нет.

Классический трёхшаговый вариант для конфига выглядел так:

mv nginx.conf nginx.conf.tmp
mv nginx.conf.new nginx.conf
mv nginx.conf.tmp nginx.conf.new

С --exchange это одна строка:

mv --exchange nginx.conf nginx.conf.new

Проверка версии и первые опыты

Сначала посмотрите версию coreutils:

mv --version

Нужна 9.5 или новее. Важная деталь: номера версий сравниваются по частям, а не как десятичные дроби, поэтому 9.11 новее, чем 9.5. Второй способ — поискать опцию в справке:

mv --help | grep exchange

Если вывод пустой, опции в вашей сборке нет. Автор статьи проверял на NixOS, где получил mv (GNU coreutils) 9.11.

Проверить обмен можно на двух файлах:

echo "config A" > a.conf
echo "config B" > b.conf
mv --exchange a.conf b.conf
cat a.conf b.conf

Вывод будет config B и config A: содержимое поменялось местами, временный файл не понадобился.

Каталоги и ловушка с -T

С каталогами опция особенно полезна, но здесь легко ошибиться. Автор статьи попробовал так:

mkdir release-old release-new
touch release-old/v1 release-new/v2
mv --exchange release-old release-new

и получил ошибку mv: cannot exchange 'release-old' and 'release-new/release-old': Unknown error -1. Обратите внимание на второй путь в сообщении: release-new/release-old. Когда последний аргумент — существующий каталог, mv считает, что вы хотите переместить что-то внутрь него, как при обычном вызове. Поэтому он пытался обменять release-old с несуществующим путём внутри release-new.

Лечится опцией -T (--no-target-directory), которая велит воспринимать последний аргумент как обычное имя, а не как папку назначения:

mv -T --exchange release-old release-new
ls release-old release-new

После этого release-old содержит v2, а release-new — v1. Обмениваются целые деревья каталогов независимо от числа файлов: меняются только имена.

Где это пригодится на практике

  • Выкатка нового конфига. Новый файл встаёт на место старого, старый остаётся под другим именем — для быстрого отката той же командой.
  • Переключение каталогов релиза. Если новая версия сайта развёрнута в отдельной папке, её можно поменять с рабочей за один шаг, не забыв про -T.
  • Отмена обмена. Повторный запуск той же команды возвращает всё как было.

Ограничения, о которых стоит знать

Один файл — одна файловая система. Обмен работает, только если оба пути лежат на одной ФС. Попытка обменять файл с файлом в tmpfs вроде /dev/shm завершится отказом ядра:

mv --exchange b.conf /dev/shm/x.conf
mv: cannot exchange 'b.conf' and '/dev/shm/x.conf': Unknown error -1

Сообщение то же самое «Unknown error -1», что и в случае с каталогами, поэтому причину надо помнить: ядро не может менять имена между разными файловыми системами. Обычный mv обходит это копированием файла с удалением оригинала, но атомарно так сделать нельзя, поэтому --exchange даже не пытается.

Поддержка файловой системой. Системному вызову нужна поддержка со стороны ФС. Распространённые файловые системы Linux — ext4, Btrfs, XFS и tmpfs — её предоставляют.

Итог

mv --exchange убирает целый класс возни в шелл-скриптах, где ради обмена приходилось заводить временное имя и мириться с окном, когда файла нет. В свежих дистрибутивах вроде NixOS, Fedora или Debian 13 опция уже есть. В следующий раз, когда потянетесь за временным именем, чтобы поменять два объекта местами, попробуйте mv --exchange — и не забудьте -T, если меняете каталоги.

Кстати, если на сервере обновляются дистрибутивы и компоненты, полезно держать под рукой заметки о том, что меняется в свежих выпусках: например, про ядро Linux 7.2 и GNOME 51 в Fedora 45 или про возвращение режима гибернации в Linux Mint 27. А тем, кто работает в терминале, пригодится разбор терминала Ghostty 1.4 — там тоже есть изменения, влияющие на повседневную работу.

Частые вопросы

С какой версии coreutils доступна опция --exchange?

С 9.5. Проверить можно командой mv --version; помните, что 9.11 новее 9.5, потому что номера сравниваются по частям.

Почему mv --exchange выдаёт «Unknown error -1»?

Самая частая причина — пути лежат на разных файловых системах, например один в обычной ФС, другой в tmpfs вроде /dev/shm. Ядро не умеет менять имена между разными ФС, а сообщение об ошибке не помогает понять причину.

Почему обмен двух каталогов не сработал?

Потому что последний аргумент — существующий каталог, и mv по умолчанию считает его папкой назначения. Добавьте -T (--no-target-directory), чтобы последний аргумент воспринимался как обычное имя.

Обмен атомарный?

Да. В Linux используется системный вызов renameat2() с флагом RENAME_EXCHANGE, поэтому оба имени существуют в любой момент времени.

Можно ли обменять файлы на разных дисках или разделах?

Нет. Оба пути должны быть на одной файловой системе, иначе ядро откажет в операции.

Какие файловые системы поддерживают обмен?

Из распространённых — ext4, Btrfs, XFS и tmpfs. Для остальных нужно проверять поддержку отдельно.

Как отменить обмен обратно?

Запустить ту же команду ещё раз: mv --exchange меняет пути местами, поэтому повторный вызов возвращает исходное состояние.

Есть ли эта опция в моём дистрибутиве?

Автор статьи упоминает NixOS, Fedora и Debian 13 как дистрибутивы, где она уже есть. В остальных случаях проверяйте mv --help | grep exchange — если вывод пустой, опции в вашей сборке нет.

Источник: itsfoss.com