Bagaimana membuat membaca dan menulis file yang sama di pipa yang sama selalu “gagal”?

9

Katakanlah saya memiliki skrip berikut:

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

Pada baris kunci, saya membaca dan menulis file tmpyang sama yang terkadang gagal.

(Saya membacanya karena kondisi balapan karena proses dalam pipa dieksekusi secara paralel, yang saya tidak mengerti mengapa - masing-masing headperlu mengambil data dari yang sebelumnya, bukan? Ini BUKAN pertanyaan utama saya, tetapi Anda juga bisa menjawabnya.)

Ketika saya menjalankan skrip, itu menghasilkan sekitar 200 baris. Apakah ada cara saya bisa memaksa skrip ini untuk selalu menghasilkan 0 baris (jadi pengalihan I / O tmpselalu disiapkan terlebih dahulu dan data selalu dihancurkan)? Agar lebih jelas, maksud saya mengubah pengaturan sistem, bukan skrip ini.

Terima kasih atas ide Anda.

karlosss
sumber

Jawaban:

2

Jawaban Gilles menjelaskan kondisi balapan. Saya hanya akan menjawab bagian ini:

Apakah ada cara saya bisa memaksa skrip ini untuk selalu menghasilkan 0 baris (sehingga I / O redirection ke tmp selalu disiapkan terlebih dahulu dan data selalu dihancurkan)? Agar jelas, maksud saya mengubah pengaturan sistem

IDK jika alat untuk ini sudah ada, tapi saya punya ide untuk bagaimana seseorang dapat diimplementasikan. (Tetapi perhatikan bahwa ini tidak selalu 0 baris, hanya tester yang berguna yang menangkap balapan sederhana seperti ini dengan mudah, dan beberapa balapan yang lebih rumit. Lihat komentar @Gilles .) Itu tidak akan menjamin bahwa skrip aman , tetapi mungkin menjadi alat yang berguna dalam pengujian, mirip dengan menguji program multi-ulir pada CPU yang berbeda, termasuk CPU non-x86 yang dipesan dengan lemah seperti ARM.

Anda akan menjalankannya sebagai racechecker bash foo.sh

Gunakan fasilitas penelusuran / penyadapan panggilan sistem yang sama strace -fdan ltrace -fgunakan untuk melampirkan ke setiap proses anak. (Di Linux, ini adalah ptracepanggilan sistem yang sama yang digunakan oleh GDB dan debugger lain untuk mengatur breakpoint, langkah tunggal, dan memodifikasi memori / register dari proses lain.)

Instrumen opendan openatsistem panggilan: ketika proses berjalan di bawah alat ini membuat sebuah open(2)system call (atau openat) dengan O_RDONLY, tidur selama mungkin 1/2 atau 1 detik. Biarkan openpanggilan sistem lain (terutama yang termasuk O_TRUNC) dijalankan tanpa penundaan.

Ini harus memungkinkan penulis untuk memenangkan perlombaan di hampir setiap kondisi perlombaan, kecuali beban sistem juga tinggi, atau itu adalah kondisi perlombaan yang rumit di mana pemotongan tidak terjadi sampai setelah beberapa pembacaan lainnya. Jadi variasi acak yang open()s (dan mungkin read()s atau tulis) ditunda akan meningkatkan daya deteksi alat ini, tetapi tentu saja tanpa pengujian untuk jumlah waktu tak terbatas dengan simulator penundaan yang pada akhirnya akan mencakup semua situasi yang mungkin Anda dapat temui di di dunia nyata, Anda tidak dapat memastikan skrip Anda bebas dari balapan kecuali jika Anda membacanya dengan cermat dan membuktikannya.


Anda mungkin akan perlu untuk daftar putih (tidak delay open) untuk file dalam /usr/bindan /usr/libsehingga proses-startup tidak mengambil selamanya. (Tautan dinamis Runtime harus memiliki open()banyak file (lihat strace -eopen /bin/trueatau /bin/lskadang - kadang), meskipun jika induk shell itu sendiri melakukan pemotongan, itu akan baik-baik saja. Tetapi alat ini masih bagus untuk alat ini agar skrip tidak berjalan lambat).

Atau mungkin daftar putih setiap file proses panggilan tidak memiliki izin untuk memotong di tempat pertama. yaitu proses pelacakan dapat membuat access(2)panggilan sistem sebelum benar-benar menangguhkan proses yang ingin open()file.


racecheckeritu sendiri harus ditulis dalam C, bukan di shell, tetapi mungkin bisa menggunakan stracekode sebagai titik awal dan mungkin tidak membutuhkan banyak pekerjaan untuk diimplementasikan.

Anda mungkin bisa mendapatkan fungsionalitas yang sama dengan sistem file FUSE . Mungkin ada contoh FUSE dari sistem file passthrough murni, sehingga Anda dapat menambahkan pemeriksaan ke open()fungsi yang membuatnya menjadi hanya untuk read-only yang terbuka tetapi membiarkan pemotongan segera terjadi.

Peter Cordes
sumber
Ide Anda untuk pemeriksa lomba tidak benar-benar bekerja. Pertama, ada masalah bahwa timeout tidak dapat diandalkan: suatu hari orang lain akan membutuhkan waktu lebih lama dari yang Anda harapkan (ini adalah masalah klasik dengan skrip pembuatan atau pengujian, yang tampaknya berfungsi untuk sementara waktu dan kemudian gagal dengan cara yang sulit untuk debug. ketika beban kerja mengembang dan banyak hal berjalan secara paralel). Tapi di luar ini, yang terbuka Anda akan menambahkan penundaan ke? Untuk mendeteksi sesuatu yang menarik, Anda harus membuat banyak berjalan dengan pola penundaan berbeda dan membandingkan hasilnya.
Gilles 'SANGAT berhenti menjadi jahat'
@Gilles: Benar, penundaan yang cukup pendek tidak menjamin truncate akan memenangkan perlombaan (pada mesin yang sarat muatan seperti yang Anda tunjukkan). Idenya di sini adalah bahwa Anda menggunakan ini untuk menguji skrip Anda beberapa kali, bukan yang Anda gunakan racecheckersepanjang waktu. Dan mungkin Anda ingin membuka-untuk-membaca waktu tidur agar dapat dikonfigurasi untuk kepentingan orang-orang pada mesin yang sangat berat yang ingin membuatnya lebih tinggi, seperti 10 detik. Atau mengaturnya menurunkan, seperti 0,1 detik untuk panjang atau script yang tidak efisien yang membuka kembali file yang banyak .
Peter Cordes
@Gilles: Ide bagus tentang pola keterlambatan yang berbeda, yang mungkin memungkinkan Anda menangkap lebih banyak balapan daripada sekadar hal-hal sederhana dalam pipa yang sama yang "harus jelas (setelah Anda tahu cara kerja shell)" seperti kasus OP. Tapi "mana yang terbuka?" setiap read-only terbuka, dengan daftar putih atau cara lain untuk tidak menunda proses startup.
Peter Cordes
Saya kira Anda sedang memikirkan ras yang lebih kompleks dengan pekerjaan latar belakang yang tidak terpotong sampai setelah beberapa proses lainnya selesai? Ya, variasi acak mungkin diperlukan untuk menangkapnya. Atau mungkin melihat pohon proses dan menunda "awal" membaca lebih banyak, untuk mencoba membalikkan pemesanan yang biasa. Anda bisa membuat alat ini semakin rumit untuk mensimulasikan semakin banyak kemungkinan penataan ulang, tetapi pada titik tertentu Anda masih harus merancang program Anda dengan benar jika Anda melakukan multi-tasking. Pengujian otomatis dapat bermanfaat untuk skrip yang lebih sederhana di mana masalah yang mungkin terjadi lebih terbatas.
Peter Cordes
Ini sangat mirip dengan pengujian kode multi-utas, terutama algoritme tanpa kunci: alasan logis tentang mengapa itu benar sangat penting, serta pengujian, karena Anda tidak dapat mengandalkan pengujian pada set mesin tertentu untuk menghasilkan semua pemesanan ulang yang mungkin menjadi masalah jika Anda belum menutup semua celah. Tetapi seperti halnya pengujian pada arsitektur yang diurutkan secara lemah seperti ARM atau PowerPC adalah ide yang bagus dalam praktiknya, pengujian skrip di bawah sistem yang secara artifisial menunda hal-hal dapat mengekspos beberapa balapan, jadi lebih baik daripada tidak sama sekali. Anda selalu dapat memperkenalkan bug yang tidak ditangkapnya!
Peter Cordes
18

Kenapa ada kondisi balapan

Kedua sisi pipa dieksekusi secara paralel, bukan satu demi satu. Ada cara yang sangat sederhana untuk menunjukkan ini: lari

time sleep 1 | sleep 1

Ini membutuhkan satu detik, bukan dua.

Shell memulai dua proses anak dan menunggu keduanya selesai. Kedua proses ini dijalankan secara paralel: satu-satunya alasan mengapa salah satu dari mereka akan melakukan sinkronisasi dengan yang lain adalah ketika harus menunggu yang lain. Titik sinkronisasi yang paling umum adalah ketika sisi kanan blok menunggu data untuk dibaca pada input standarnya, dan menjadi tidak terblokir ketika sisi kiri menulis lebih banyak data. Kebalikannya juga bisa terjadi, ketika sisi kanan lambat untuk membaca data dan sisi kiri blok dalam operasi penulisan sampai sisi kanan membaca lebih banyak data (ada penyangga di dalam pipa itu sendiri, dikelola oleh kernel, tetapi memiliki ukuran maksimum kecil).

Untuk mengamati titik sinkronisasi, perhatikan perintah berikut ( sh -xmencetak setiap perintah saat dijalankan):

time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'

Bermainlah dengan variasi sampai Anda merasa nyaman dengan apa yang Anda amati.

Diberi perintah majemuk

cat tmp | head -1 > tmp

proses di sebelah kiri melakukan hal berikut (Saya hanya mencantumkan langkah-langkah yang relevan dengan penjelasan saya):

  1. Jalankan program eksternal catdengan argumen tmp.
  2. Terbuka tmpuntuk membaca.
  3. Meskipun belum mencapai akhir file, baca sepotong dari file dan tulis ke output standar.

Proses di sebelah kanan melakukan hal berikut:

  1. Mengarahkan output standar ke tmp, memotong file dalam proses.
  2. Jalankan program eksternal headdengan argumen -1.
  3. Baca satu baris dari input standar dan tulis ke output standar.

Satu-satunya titik sinkronisasi adalah bahwa kanan-3 menunggu kiri-3 untuk memproses satu baris penuh. Tidak ada sinkronisasi antara kiri-2 dan kanan-1, sehingga bisa terjadi dalam urutan apa pun. Urutan apa yang terjadi tidak dapat diprediksi: tergantung pada arsitektur CPU, pada shell, pada kernel, di mana core dari proses yang dijadwalkan, pada apa yang mengganggu CPU terima sekitar waktu itu, dll.

Cara mengubah perilaku

Anda tidak dapat mengubah perilaku dengan mengubah pengaturan sistem. Komputer melakukan apa yang Anda perintahkan. Anda menyuruhnya memotong tmpdan membaca tmpsecara paralel, sehingga ia melakukan dua hal secara paralel.

Ok, ada satu "pengaturan sistem" yang bisa Anda ubah: Anda bisa menggantinya /bin/bashdengan program lain yang bukan bash. Saya harap ini akan berjalan tanpa mengatakan bahwa ini bukan ide yang baik.

Jika Anda ingin pemotongan terjadi sebelum sisi kiri pipa, Anda harus meletakkannya di luar pipa, misalnya:

{ cat tmp | head -1; } >tmp

atau

( exec >tmp; cat tmp | head -1 )

Saya tidak tahu mengapa Anda menginginkan ini. Apa gunanya membaca dari file yang Anda tahu kosong?

Sebaliknya, jika Anda ingin pengalihan output (termasuk pemotongan) terjadi setelah catselesai membaca, maka Anda perlu buffer penuh data dalam memori, misalnya

line=$(cat tmp | head -1)
printf %s "$line" >tmp

atau menulis ke file yang berbeda dan kemudian pindahkan ke tempatnya. Ini biasanya merupakan cara yang kuat untuk melakukan hal-hal dalam skrip, dan memiliki keuntungan bahwa file tersebut ditulis secara penuh sebelum terlihat melalui nama aslinya.

cat tmp | head -1 >new && mv new tmp

The MoreUtils koleksi mencakup program yang tidak hanya itu, disebut sponge.

cat tmp | head -1 | sponge tmp

Cara mendeteksi masalah secara otomatis

Jika tujuan Anda adalah untuk mengambil skrip yang ditulis dengan buruk dan secara otomatis mencari tahu di mana mereka melanggar, maka maaf, hidup tidak sesederhana itu. Analisis Runtime tidak akan andal menemukan masalah karena terkadang catselesai membaca sebelum pemotongan terjadi. Analisis statis pada prinsipnya dapat melakukannya; contoh disederhanakan dalam pertanyaan Anda ditangkap oleh Shellcheck , tetapi mungkin tidak menangkap masalah serupa di skrip yang lebih kompleks.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
Itulah tujuan saya, untuk menentukan apakah naskahnya ditulis dengan baik atau tidak. Jika skrip mungkin menghancurkan data dengan cara ini, saya hanya ingin itu menghancurkannya setiap waktu. Tidak baik mendengar bahwa ini hampir mustahil. Terima kasih kepada Anda, saya sekarang tahu apa masalahnya dan akan mencoba memikirkan solusinya.
karlosss
@karlosss: Hmm, saya bertanya-tanya apakah Anda dapat menggunakan hal-hal penelusuran / penyadapan panggilan sistem yang sama dengan strace(yaitu Linux ptrace) untuk membuat semua openpanggilan sistem-untuk-membaca (dalam semua proses anak) tidur selama setengah detik, jadi ketika balapan dengan pemotongan, pemotongan akan hampir selalu menang.
Peter Cordes
@PeterCordes saya seorang pemula untuk ini, jika Anda dapat mengatur cara untuk mencapai ini dan menuliskannya sebagai jawaban, saya akan menerimanya.
karlosss
@PeterCordes Anda tidak dapat menjamin bahwa pemotongan akan menang dengan penundaan. Ini akan bekerja sebagian besar waktu, tetapi kadang-kadang pada mesin yang sarat dengan script Anda akan gagal dalam cara yang kurang lebih misterius.
Gilles 'SO- stop being evil'
@Gilles: Mari kita bahas ini di bawah jawaban saya.
Peter Cordes