Zaskoczony zachowaniem CP z hardlinkami

20

Bardzo dobrze rozumiem pojęcie linków twardych i cpkilkakrotnie czytałem strony podręcznika dla podstawowych narzędzi, takich jak --- a nawet najnowsze specyfikacje POSIX. Wciąż byłem zaskoczony obserwowaniem następującego zachowania:

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

W tym momencie johni paulbędzie miał taką samą zawartość iwęzeł (i), i georgebędą się różniły w obu aspektach. Teraz wykonujemy:

$ cp george paul

W tym momencie spodziewałem się georgei będę paulmiał różne numery i-węzłów, ale tę samą treść --- to oczekiwanie zostało spełnione --- ale spodziewałem się również, że będę paulmieć inny numer johni- węzła i johnnadal będę mieć zawartość john. Byłem tam zaskoczony. Okazuje się, że skopiowanie pliku do ścieżki docelowej paulpowoduje również zainstalowanie tego samego pliku (tego samego i-węzła) na wszystkich innych ścieżkach docelowych, które współużytkują pauli-węzeł. Myślałem, że cptworzy nowy plik i przenosi go do miejsca, które poprzednio zajmował stary plik paul. Zamiast tego wydaje się, że otwiera istniejący plik paul, obcina go i piszegeorgezawartość tego istniejącego pliku. W związku z tym wszystkie „inne” pliki z tym samym i-węzłem otrzymują jednocześnie „swoją” treść.

Ok, to jest systematyczne zachowanie i teraz, kiedy wiem, że mogę się tego spodziewać, mogę wymyślić, jak go obejść, lub odpowiednio go wykorzystać. Co mnie zastanawia, gdzie miałem udokumentować to zachowanie? Byłbym zaskoczony, gdyby nie zostało to udokumentowane gdzieś w dokumentach, które już przeglądałem. Ale najwyraźniej mi tego brakowało i nie mogę teraz znaleźć źródła, które omawia takie zachowanie.

dubiousjim
źródło

Odpowiedzi:

4

Po pierwsze, dlaczego tak się dzieje? Jeden powód jest historyczny: tak właśnie zrobiono w Unix First Edition .

Pliki są pobierane w parach; pierwszy jest otwarty do czytania, drugi utworzony tryb 17. Następnie pierwszy jest kopiowany do drugiego.

„Utworzono” odnosi się do creatwywołania systemowego (tego, w którym nie ma znaku e ), który obcina istniejący plik o podanej nazwie, jeśli taki istnieje.

A oto kod źródłowy cpUnix Second Edition (nie mogę znaleźć kodu źródłowego First Edition). Możesz zobaczyć wywołania do openpliku źródłowego i creatdrugiego pliku; i jako ulepszenie do wydania pierwszego, jeśli drugi plik jest istniejącym katalogiem, cptworzy plik w tym katalogu.

Ale możesz zapytać, dlaczego tak się wtedy działo? Odpowiedź na „dlaczego Unix pierwotnie zrobił to w ten sposób” to prawie zawsze prostota. cpotwiera źródło do odczytu i tworzy miejsce docelowe - a wywołanie systemowe w celu utworzenia pliku zastępuje istniejący plik, otwierając go do zapisu, ponieważ pozwala to dzwoniącemu narzucić zawartość pliku o podanej nazwie, niezależnie od tego, czy plik już istniał, czy też nie.

Teraz, gdzie jest to udokumentowane: na stronie podręcznika FreeBSD .

Dla każdego pliku docelowego, który już istnieje, jego zawartość jest zastępowana, jeśli pozwalają na to uprawnienia. Tryb, identyfikator użytkownika i identyfikator grupy pozostają niezmienione, chyba że podano opcję -p.

Sformułowanie to było obecne już w 1990 r. (Kiedy BSD było 4.3BSD). Podobne sformułowanie dotyczy Solaris 10 :

Jeśli plik_docelowy istnieje, cp zastępuje jego zawartość, ale tryb (i ACL, jeśli dotyczy), właściciel i grupa z nim związane nie są zmieniane.

Twoja obudowa jest nawet opisana w podręczniku HP-UX 10 :

Jeśli nowy_plik jest łączem do istniejącego pliku z innymi łączami, zastępuje istniejący plik i zachowuje wszystkie łącza.

POSIX umieszcza go w standardzie. Cytowanie z Single UNIX v2 :

Jeśli plik_dest istnieje, podejmowane są następujące kroki: (…) Deskryptor pliku dla plik_doc zostanie uzyskany poprzez wykonanie akcji równoważnych funkcji open () specyfikacji XSH wywołanej przy użyciu pliku_doc jako argumentu ścieżki oraz dołączenia bitowego LUB O_WRONLY i O_TRUNC jako argument oflag.

Przywołane przeze mnie strony podręcznika i specyfikacja dalej precyzują, że jeśli -fopcja zostanie przekazana, a próba otwarcia / utworzenia pliku docelowego nie powiedzie się (zwykle z powodu braku uprawnień do zapisu pliku), cppróbuje usunąć obiekt docelowy i ponownie utworzyć plik . To zerwałoby twardy link w twoim scenariuszu.

Możesz zgłosić błąd w dokumentacji dotyczący instrukcji GNU coreutils , ponieważ nie dokumentuje tego zachowania. Nawet opis --preserve=links, który w twoim scenariuszu doprowadziłby do usunięcia paulłącza i utworzenia nowego pliku, nie wyjaśnia, co się stanie bez niego --preserve=links. Opis -frodzaju sugeruje, co się dzieje bez niego, ale nie określa tego („Gdy kopiowanie bez tej opcji i istniejącego pliku docelowego nie można otworzyć do zapisu, kopiowanie kończy się niepowodzeniem. Jednak przy użyciu opcji --force,…”).

Gilles „SO- przestań być zły”
źródło
dlaczego mówisz „ponieważ pozwala to dzwoniącemu przejąć na własność nazwę pliku, niezależnie od tego, czy plik już istnieje, czy nie”? CP nie przejmuje na własność wcześniej istniejącego pliku.
jrw32982 obsługuje Monikę
@ jrw32982 Miałem na myśli własność w sensie decydowania o zawartości pliku, a nie własność w znaczeniu metadanych pliku. Przepisałem to zdanie.
Gilles „SO- przestań być zły”
20

cpdokumenty, które zastępują plik docelowy, jeśli plik docelowy jest już obecny. Masz rację, że nie określa ono szczegółowo, co oznacza „nadpisanie”, ale zdecydowanie mówi „nadpisuj”, a nie „zastępuj”. Jeśli chcesz być pedantyczny, możesz argumentować, że „nadpisywanie” jest dokładnie tym, co cpdziała, a oczekiwane zachowanie można by właściwie nazwać „zamień”.

Zauważ też, że gdyby cp„zastąpić” wcześniej istniejące pliki docelowe, można to uznać za zaskakujące lub niepoprawne, prawdopodobnie więcej niż „nadpisywanie”. Na przykład:

  • Jeśli cpnajpierw usuniesz stary plik, a następnie utworzysz nowy, będzie pewien przedział czasu, w którym plik będzie nieobecny, co byłoby zaskakujące.
  • Jeśli cpnajpierw utworzysz plik tymczasowy, a następnie przeniesiesz go na miejsce, prawdopodobnie powinien to udokumentować, ze względu na fakt, że takie pliki tymczasowe o dziwnych nazwach byłyby czasami zauważane ... ale tak nie jest.
  • Jeśli cpnie można utworzyć nowego pliku w tym samym katalogu, co stary plik ze względu na uprawnienia, byłoby to niefortunne (szczególnie, jeśli stary plik został już usunięty).
  • Jeśli plik nie był własnością użytkownika działającego, cpa użytkownik działający cpnie był, nie rootbyłoby możliwe dopasowanie właściciela i uprawnień nowego pliku do uprawnień nowego pliku.
  • Jeśli plik ma fantazyjne atrybuty specjalne, o cpktórych nie wie, to zostałyby utracone w kopii. W dzisiejszych czasach implementacje cppowinny niezawodnie rozumieć takie cechy, jak rozszerzone atrybuty, ale nie zawsze tak było. Są też inne rzeczy, takie jak rozwidlenia zasobów MacOS lub, w przypadku zdalnych systemów plików, w zasadzie wszystko.

Podsumowując: teraz wiesz, co cpnaprawdę robi. Już nigdy nie będziesz zaskoczony! Szczerze mówiąc, myślę, że to samo mogło mi się przydarzyć wiele lat temu.

Celada
źródło
Muszę sprawdzić referencje POSIX, ale w rzeczywistości manstrony dla wersji cpBSD (przynajmniej OSX) i Gnu cpnie są tak jednoznaczne na temat „nadpisywania”. Tego słowa używa się tylko w komentarzach do opcji -ii -n. Strona Gnu jest szczególnie nieinformacyjna, począwszy od strony Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.BSD / Mac, przynajmniej mówiIn the first synopsis form, the cp utility copies the contents of the source_file to the target_file.
dubiousjim
Strona informacyjna Gnu coreutils zaczyna się:‘cp’ copies files (or, optionally, directories). The copy is completely independent of the original.
dubiousjim
2
Widzę, że standard POSIX 2008 określa obserwowane zachowanie; Dodam odpowiedź.
dubiousjim
16

Widzę, że standard POSIX 2013 określa obserwowane zachowanie . To mówi:

  1. Jeśli plik_źródłowy jest typu zwykłego pliku, należy podjąć następujące kroki:

    za. ... jeśli istnieje plik_doc , należy podjąć następujące kroki:

    ja. Jeśli -iopcja jest aktywna, cpnarzędzie wypisze monit o błędzie standardowym i odczyta wiersz ze standardowego wejścia. Jeśli odpowiedź nie jest twierdząca, cpnie należy nic więcej robić z plikiem_źródłowym i przechodzić do pozostałych plików.

    ii. Deskryptor pliku dla plik_docelowy otrzymuje się poprzez wykonywanie czynności odpowiednikiem open()funkcji określonej w objętości systemu interfejsów POSIX.1-2008 nazywa użyciu plik_docelowy jako argument ścieżkę, a iloczynem bitowym włącznie ORz O_WRONLYi O_TRUNCjako oflagu argument.

    iii. Jeśli próba uzyskania deskryptora pliku nie powiedzie się, a -fopcja będzie obowiązywać, podejmiecp próbę usunięcia pliku, wykonując czynności równoważne z unlink()funkcją zdefiniowaną w woluminie interfejsów systemowych POSIX.1-2008, wywoływane przy użyciu pliku dest_file jako argumentu path. Jeśli próba się powiedzie, cpnależy kontynuować od kroku 3b.

    ...

    re. Zawartość pliku_źródłowego zostanie zapisana w deskryptorze pliku. Wszelkie błędy zapisu powodują cpzapisanie komunikatu diagnostycznego do błędu standardowego i przejście do kroku 3e.

    mi. Deskryptor pliku zostanie zamknięty.

dubiousjim
źródło
1
Ciekawy. Tak jak ty, zakładałem, cpże da podobne wyniki mvi zerwie wszelkie twarde linki, których częścią była dest. Ale teraz, gdy o tym myślę, oznaczałoby to, że musiałby konkretnie unlink(2)cel ( cp -f) lub utworzyć tymczasowo inną nazwę, a potem rename(2)to. Prostą implementacją jest po prostu otwarcie pliku do zastąpienia, czego wymaga POSIX. Jest to odpowiednikcat src > dest
Peter Cordes,
2

Jeśli możesz powiedzieć: „skopiowanie pliku do ścieżki docelowej powoduje paul również skopiowanie tego samego pliku (tego samego i-węzła) do wszystkich innych ścieżek docelowych pauli-węzła udziału ”. Przykro mi to powiedzieć, że nie rozumiesz pojęcia twarde linki bardzo dobrze. Jeśli dam jabłko Sir McCartneyowi, dam jabłko Paulowi i jabłko partnerowi Johna Lennona. Ale nie rozdałem trzech jabłek; Dałem jabłko osobie, która ma wiele nazwisk / tytułów / deskryptorów.

Podobnie, kiedy kopiujesz georgedo paul, nie kopiujesz również do john. Zamiast tego kopiujesz georgedane do pliku, którego i-węzeł wskazuje paulpozycja katalogu.

Krok po kroku:   kiedy to zrobisz

echo john > john

utworzyłeś nowy plik (zakładając, że johnw tym katalogu nie było już pliku o nazwie ). Lub, mówiąc ściślej, zakłada to, że nie było już pozycji katalogu o nazwie johnw tym katalogu (ponieważ, ściśle mówiąc, nie ma plików w katalogach; tylko wpisy katalogu, które wskazują na i-węzły). Po tym jak to zrobisz

cp -l john paul

lub

ln john paul

nie utworzyłeś nowego pliku; zamiast tego nadałeś istniejącemu plikowi nową nazwę. Masz teraz plik o dwóch nazwach: johni paul. A kiedy mówisz

cp george paul

nadpisujesz ten plik . Fakt, że ma dwie nazwy, nie ma znaczenia; może mieć 42 nazwy, być może w miejscach, do których nawet nie masz dostępu, a to polecenie nie kopiuje george\ndanych do wszystkich tych nazw (ścieżek); po prostu kopiuje dane do jednego pliku, który ma wiele nazw.

Scott
źródło
1
Dzięki. Tak, zdawałem sobie sprawę z tego, że pisałem to, czego potrzebowałem, do przestraszenia - johni paulzacznij od dwóch ścieżek do tego samego pliku. Ale to był najłatwiejszy sposób na wyrażenie siebie. Nie sądzę, że samo pojęcie twardego linku, właściwie rozumiane, dyktuje jedno z dwóch zachowań dla cp(bez -l).
dubiousjim
Ale dzięki za szturchanie; Próbowałem wyjaśnić sformułowanie.
dubiousjim