Terkejut dengan perilaku cp dengan hardlink

20

Saya memahami gagasan tentang hardlink dengan sangat baik, dan telah membaca halaman manual untuk alat dasar seperti cp--- dan bahkan spesifikasi POSIX terbaru --- beberapa kali. Namun saya masih terkejut melihat perilaku berikut:

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

Pada titik ini johndan paulakan memiliki inode (dan konten) yang sama, dan georgeakan berbeda dalam kedua hal. Sekarang kita lakukan:

$ cp george paul

Pada titik ini saya berharap georgedan paulmemiliki nomor inode yang berbeda tetapi konten yang sama --- harapan ini terpenuhi --- tetapi saya juga diharapkan paulsekarang memiliki nomor inode yang berbeda dari john, dan untuk johnmasih memiliki konten john. Di sinilah saya terkejut. Ternyata menyalin file ke jalur tujuan pauljuga memiliki hasil menginstal file yang sama (inode yang sama) di semua jalur tujuan lain yang berbagi paulinode. Saya berpikir untuk cpmembuat file baru dan memindahkannya ke tempat yang sebelumnya ditempati oleh file lama paul. Sebaliknya apa yang tampaknya dilakukan adalah membuka file yang ada paul, memotongnya, dan menulisgeorgeKonten ke dalam file yang ada. Karenanya setiap file "lain" dengan inode yang sama mendapatkan konten "mereka" yang diperbarui secara bersamaan.

Oke, ini adalah perilaku sistematis dan sekarang setelah saya tahu untuk mengharapkannya, saya bisa mencari cara untuk mengatasinya, atau mengambil keuntungan darinya, jika perlu. Apa yang membingungkan saya di mana saya seharusnya melihat perilaku ini didokumentasikan? Saya akan terkejut jika itu tidak didokumentasikan di suatu tempat di dokumen yang sudah saya lihat. Tetapi ternyata saya melewatkannya, dan sekarang tidak dapat menemukan sumber yang membahas perilaku ini.

dubiousjim
sumber

Jawaban:

4

Pertama, mengapa dilakukan dengan cara ini? Salah satu alasannya adalah historis: itulah yang dilakukan di Unix First Edition .

File diambil berpasangan; yang pertama dibuka untuk dibaca, mode yang dibuat kedua 17. Kemudian yang pertama disalin ke yang kedua.

"Created" mengacu pada creatpanggilan sistem (yang terkenal dengan e ), yang memotong file yang ada dengan nama yang diberikan jika ada.

Dan inilah kode sumber cpdi Unix Second Edition (saya tidak dapat menemukan kode sumber Edisi Pertama). Anda dapat melihat panggilan ke openuntuk file sumber dan creatuntuk file kedua; dan, sebagai peningkatan ke Edisi Pertama, jika file kedua adalah direktori yang ada, cpmembuat file di direktori itu.

Tetapi, Anda mungkin bertanya, mengapa hal itu dilakukan pada saat itu? Jawaban untuk "mengapa Unix awalnya melakukannya seperti itu" hampir selalu kesederhanaan. cpmembuka sumbernya untuk membaca dan membuat tujuannya - dan sistem panggilan untuk membuat file menimpa file yang sudah ada dengan membukanya untuk menulis, karena itu memungkinkan penelepon untuk memaksakan konten file dengan nama yang diberikan apakah file tersebut sudah ada atau tidak.

Sekarang, di mana itu didokumentasikan: di halaman manual FreeBSD .

Untuk setiap file tujuan yang sudah ada, isinya ditimpa jika izin mengizinkan. Mode, ID pengguna, dan ID grup tidak berubah kecuali opsi -p ditentukan.

Kata-kata itu hadir setidaknya sejauh 1990 (saat BSD adalah 4.3BSD). Ada kata-kata yang mirip pada Solaris 10 :

Jika target_file ada, cp menimpa isinya, tetapi mode (dan ACL jika berlaku), pemilik, dan grup yang terkait dengannya tidak diubah.

Kasing Anda bahkan dijabarkan dalam manual HP-UX 10 :

Jika new_file adalah tautan ke file yang ada dengan tautan lain, timpa file yang ada dan simpan semua tautan.

POSIX menempatkannya dalam standar. Mengutip dari Single UNIX v2 :

Jika dest_file ada, langkah-langkah berikut diambil: (...) Deskriptor file untuk dest_file akan diperoleh dengan melakukan tindakan yang setara dengan spesifikasi XSH terbuka () fungsi yang disebut menggunakan dest_file sebagai argumen path, dan bitwise inklusif OR dari O_WRONLY dan O_TRUNC sebagai argumen oflag.

Halaman manual dan spesifikasi yang saya kutipkan lebih lanjut menetapkan bahwa jika -fopsi dilewatkan dan upaya untuk membuka / membuat file target gagal (biasanya karena tidak memiliki izin untuk menulis file), cpmencoba untuk menghapus target dan membuat file lagi . Ini akan mematahkan tautan keras dalam skenario Anda.

Anda mungkin ingin melaporkan bug dokumentasi terhadap manual GNU coreutils , karena itu tidak mendokumentasikan perilaku ini. Bahkan deskripsi --preserve=links, yang dalam skenario Anda akan menyebabkan paultautan dihapus dan file baru dibuat, tidak memperjelas apa yang terjadi tanpa itu --preserve=links. Deskripsi -fjenis menyiratkan apa yang terjadi tanpa itu tetapi tidak mengejanya ("Ketika menyalin tanpa opsi ini dan file tujuan yang ada tidak dapat dibuka untuk menulis, salinan gagal. Namun, dengan - force, ...").

Gilles 'SANGAT berhenti menjadi jahat'
sumber
mengapa Anda mengatakan "karena itu memungkinkan penelepon untuk mengambil kepemilikan nama file apakah file tersebut sudah ada atau tidak"? Cp tidak mengambil kepemilikan file yang sudah ada sebelumnya.
jrw32982 mendukung Monica
@ jrw32982 Maksud saya kepemilikan dalam arti memutuskan apa yang masuk ke dalam file, bukan kepemilikan dalam arti file metadata. Saya telah menulis ulang kalimat itu.
Gilles 'SO- stop being evil'
20

cpdokumen yang menimpa file tujuan jika file tujuan sudah ada. Anda benar bahwa itu tidak menentukan secara rinci apa arti "menimpa", tetapi jelas mengatakan "menimpa", bukan "mengganti". Jika Anda ingin menjadi orang yang bertele-tele, Anda dapat berargumen bahwa "menimpa" adalah apa yang cpdilakukannya, dan perilaku yang Anda harapkan akan disebut "ganti".

Juga perhatikan bahwa jika cp"mengganti" file tujuan yang sudah ada sebelumnya, yang mungkin dianggap mengejutkan atau salah, mungkin lebih dari sekadar "menimpa". Sebagai contoh:

  • Jika cppertama kali menghapus file lama dan kemudian membuat yang baru maka akan ada interval waktu di mana file tersebut akan tidak ada, yang akan mengejutkan.
  • Jika cppertama kali membuat file sementara dan kemudian memindahkannya di tempat maka mungkin harus mendokumentasikan ini, karena fakta bahwa file sementara seperti itu dengan nama-nama aneh kadang-kadang akan diperhatikan ... tetapi tidak.
  • Jika cptidak dapat membuat file baru di direktori yang sama dengan file lama karena izin maka ini akan sangat disayangkan (terutama jika sudah menghapus yang lama).
  • Jika file tersebut tidak dimiliki oleh pengguna yang menjalankan cpdan pengguna yang menjalankannya cptidak rootmaka tidak mungkin untuk mencocokkan pemilik & izin dari file baru dengan file yang baru.
  • Jika file memiliki atribut khusus yang cptidak diketahui, maka ini akan hilang dalam salinan. Saat ini implementasi cpseharusnya dapat memahami hal-hal seperti atribut yang diperluas, tetapi tidak selalu demikian. Dan ada hal-hal lain, seperti garpu sumber daya MacOS, atau, untuk sistem file jarak jauh, pada dasarnya apa saja.

Jadi kesimpulannya: sekarang Anda tahu apa yang cpsebenarnya. Anda tidak akan terkejut dengan itu lagi! Jujur, saya pikir hal yang sama mungkin terjadi pada saya juga, bertahun-tahun yang lalu.

Celada
sumber
Harus memeriksa referensi POSIX, tetapi pada kenyataannya manhalaman untuk cppada BSD (setidaknya, OSX) dan versi Gnu cptidak begitu eksplisit tentang "menimpa". Kata itu hanya digunakan di komentar pada opsi -idan -n. Halaman manual Gnu terutama tidak informatif, mulai Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.halaman manual BSD / Mac setidaknya mengatakanIn the first synopsis form, the cp utility copies the contents of the source_file to the target_file.
dubiousjim
Halaman info Gnu coreutils dimulai:‘cp’ copies files (or, optionally, directories). The copy is completely independent of the original.
dubiousjim
2
Saya melihat bahwa standar POSIX 2008 tidak menentukan perilaku yang diamati; Saya akan menambahkan jawaban.
dubiousjim
16

Saya melihat bahwa standar POSIX 2013 tidak menentukan perilaku yang diamati . Ia mengatakan:

  1. Jika source_file adalah tipe file biasa, langkah-langkah berikut harus diambil:

    Sebuah. ... jika dest_file ada, langkah-langkah berikut harus diambil:

    saya. Jika -iopsi ini berlaku, cputilitas akan menulis prompt untuk kesalahan standar dan membaca baris dari input standar. Jika responsnya tidak setuju, cptidak akan melakukan apa-apa lagi dengan source_file dan lanjutkan ke file yang tersisa.

    ii. Deskriptor file untuk dest_file harus diperoleh dengan melakukan tindakan yang setara dengan open()fungsi yang didefinisikan dalam volume Antarmuka Sistem POSIX.1-2008 yang disebut menggunakan dest_file sebagai argumen path, dan bitwise-inclusive ORdari O_WRONLYdan O_TRUNCsebagai argumen oflag .

    aku aku aku. Jika upaya untuk mendapatkan deskriptor file gagal dan -fopsi berlaku, cpharus berusaha untuk menghapus file dengan melakukan tindakan yang setara dengan unlink()fungsi yang didefinisikan dalam volume Antarmuka Sistem POSIX.1-2008 yang disebut menggunakan dest_file sebagai argumen path. Jika upaya ini berhasil, cpakan dilanjutkan dengan langkah 3b.

    ...

    d. Isi source_file akan ditulis ke deskriptor file. Setiap kesalahan penulisan akan menyebabkan cppenulisan pesan diagnostik ke kesalahan standar dan melanjutkan ke langkah 3e.

    e. Deskriptor file akan ditutup.

dubiousjim
sumber
1
Menarik. Seperti Anda, saya berasumsi cpakan memberikan hasil yang serupa mv, dan memutuskan tautan apa pun yang menjadi bagian darinya. Tapi sekarang saya berpikir tentang hal itu, itu berarti harus secara khusus unlink(2)target ( cp -f), atau membuat sementara yang berbeda bernama dan kemudian rename(2). Implementasi yang mudah adalah dengan hanya membuka file untuk ditimpa, yang merupakan persyaratan POSIX. Ini setara dengancat src > dest
Peter Cordes
2

Jika Anda dapat mengatakan, "menyalin file ke jalur tujuan paul juga menyalin file yang sama (inode yang sama) ke semua jalur tujuan lain yang berbagi paulinode.", Saya minta maaf untuk mengatakan bahwa Anda tidak memahami gagasan tentang tautan keras dengan sangat baik. Jika saya memberikan sebuah apel kepada Sir McCartney, saya telah memberikan sebuah apel kepada Paul, dan saya telah memberikan sebuah apel kepada mitra penulis lagu John Lennon. Tapi saya belum membagikan tiga apel; Saya telah memberikan sebuah apel kepada seseorang yang memiliki banyak nama / gelar / deskriptor.

Demikian pula, ketika Anda menyalin georgeke paul, Anda tidak juga menyalin ke john. Sebaliknya, Anda menyalin georgedata ke file yang inode-nya ditunjukkan oleh paulentri direktori.

Langkah demi langkah:   Saat Anda melakukannya

echo john > john

Anda telah membuat file baru (dengan asumsi bahwa belum ada file bernama johndi direktori itu). Atau, untuk berbicara lebih ketat, ini mengasumsikan bahwa belum ada entri direktori dengan nama johndi direktori itu (karena, secara tegas, tidak ada file dalam direktori; hanya entri direktori, yang mengarah ke inode). Setelah kamu lakukan

cp -l john paul

atau

ln john paul

Anda belum membuat file baru; sebaliknya, Anda telah memberi nama baru pada file Anda yang sudah ada. Anda sekarang memiliki file dengan dua nama: johndan paul. Dan ketika Anda mengatakannya

cp george paul

Anda menimpa file itu . Fakta bahwa ia memiliki dua nama tidak relevan; itu bisa memiliki 42 nama, mungkin di tempat-tempat yang Anda bahkan tidak dapat mengakses, dan perintah ini tidak akan menyalin george\ndata ke semua nama (jalur); itu hanya menyalin data ke satu file yang memiliki banyak nama.

Scott
sumber
1
Terima kasih. Benar, saya menyadari karakter menakut-nakuti-diperlukan dari apa yang saya tulis saat saya menulisnya: johndan paulmulai sebagai dua nama path untuk file yang sama. Tetapi itu adalah cara termudah yang bisa saya pikirkan untuk mengekspresikan diri. Saya tidak berpikir gagasan tentang hubungan yang sulit, dipahami dengan benar, menentukan salah satu dari dua perilaku untuk cp(tanpa -l).
dubiousjim
Tapi terima kasih atas dorongannya; Saya sudah mencoba mengklarifikasi kata-katanya.
dubiousjim