Contoh kode IBM, fungsi non-peserta tidak berfungsi di sistem saya

11

Saya sedang belajar re-entrancy dalam pemrograman. Di situs IBM ini (sangat bagus). Saya telah menemukan kode, disalin di bawah. Ini adalah kode pertama yang datang ke situs web.

Kode mencoba menunjukkan masalah yang melibatkan akses bersama ke variabel dalam pengembangan program teks (asinkronisitas) yang tidak linier dengan mencetak dua nilai yang terus berubah dalam "konteks berbahaya".

#include <signal.h>
#include <stdio.h>

struct two_int { int a, b; } data;

void signal_handler(int signum){
   printf ("%d, %d\n", data.a, data.b);
   alarm (1);
}

int main (void){
   static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };

   signal (SIGALRM, signal_handler); 
   data = zeros;
   alarm (1);
   while (1){
       data = zeros;
       data = ones;
   }
}

Masalah muncul ketika saya mencoba menjalankan kode (atau lebih baik, tidak muncul). Saya menggunakan versi gcc 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) dalam konfigurasi default. Output yang salah arah tidak terjadi. Frekuensi dalam mendapatkan nilai pasangan "salah" adalah 0!

Apa yang sedang terjadi? Mengapa tidak ada masalah dalam re-entrancy menggunakan variabel global statis?

Daniel Bandeira
sumber
1
Pastikan semua optimisasi kompiler dinonaktifkan, dan coba lagi
roaima
Saya kira itu ... tapi opsi mana yang akan saya ubah? Saya tidak punya ide. :-(
Daniel Bandeira
5
Ini terlihat seperti pertanyaan pemrograman (stack overflow). Dosisnya sepertinya tidak ditempatkan dengan baik di sini. (Maaf, saya dengan sub-situs yang lebih sedikit; sangat terpotong-potong. Tapi begitulah adanya.)
ctrl-alt-delor
1
Kode masuk ulang yang paling sederhana tidak dapat diubah.
ctrl-alt-delor
Pada saat pertama, saya berpikir bahwa pertanyaannya akan terkait dengan lingkungan gcc dan Linux. Berkembang, misalnya, penjadwalan OS (mengeksekusi lebih banyak teks program sinyal interupsi setelah sebelum memanggil rutin pawang), misalnya.
Daniel Bandeira

Jawaban:

12

Itu tidak benar-benar ulang entrancy ; Anda tidak menjalankan fungsi dua kali di utas yang sama (atau di utas berbeda). Anda bisa mendapatkannya melalui rekursi atau mengirimkan alamat fungsi saat ini sebagai fungsi-pointer callback ke fungsi lain. (Dan itu tidak akan tidak aman karena akan sinkron).

Ini hanyalah perlombaan data vanila UB (Undefined Behavior) antara pengatur sinyal dan utas utama: hanya sig_atomic_tdijamin aman untuk ini . Orang lain mungkin bekerja, seperti dalam kasus Anda di mana objek 8-byte dapat dimuat atau disimpan dengan satu instruksi pada x86-64, dan kompiler kebetulan memilih asm itu. (Seperti yang ditunjukkan oleh jawaban icarus).

Lihat pemrograman MCU - optimasi C ++ O2 rusak sementara loop - pengendali interupsi pada mikrokontroler inti tunggal pada dasarnya adalah hal yang sama dengan pengendali sinyal dalam program berulir tunggal. Dalam hal ini hasil dari UB adalah bahwa beban diangkat dari loop.

Test-case robek Anda benar-benar terjadi karena perlombaan data UB mungkin dikembangkan / diuji dalam mode 32-bit, atau dengan kompiler dumber yang lebih lama yang memuat anggota struct secara terpisah.

Dalam kasus Anda, kompiler dapat mengoptimalkan toko keluar dari infinite loop karena tidak ada program bebas UB yang bisa mengamatinya. databukan _Atomicatauvolatile , dan tidak ada efek samping lain dalam loop. Jadi tidak mungkin pembaca dapat melakukan sinkronisasi dengan penulis ini. Ini sebenarnya terjadi jika Anda mengkompilasi dengan optimasi diaktifkan ( Godbolt menunjukkan loop kosong di bagian bawah utama). Saya juga mengubah struct menjadi dua long long, dan gcc menggunakan toko movdqa16-byte tunggal sebelum loop. (Ini tidak dijamin atom, tetapi dalam praktiknya di hampir semua CPU, dengan asumsi itu disejajarkan, atau pada Intel tidak hanya melewati batas cache-line. Mengapa penugasan integer pada atom variabel sejajar alami pada x86? )

Jadi kompilasi dengan optimasi yang diaktifkan juga akan merusak pengujian Anda, dan menunjukkan nilai yang sama setiap kali. C bukan bahasa rakitan portabel.

volatile struct two_intjuga akan memaksa kompiler untuk tidak mengoptimalkannya, tetapi tidak akan memaksanya untuk memuat / menyimpan seluruh struktur secara atom. (Ini tidak akan berhenti dari melakukannya baik, meskipun.) Perhatikan bahwa volatiletidak tidak menghindari UB data ras, tetapi dalam prakteknya itu cukup untuk komunikasi antar-benang dan bagaimana orang-orang membangun teori atom linting tangan (bersama dengan asm inline) sebelum C11 / C ++ 11, untuk arsitektur CPU normal. Mereka cache-koheren sehingga volatileadalah dalam prakteknya sebagian besar mirip dengan _Atomicdenganmemory_order_relaxed untuk murni-beban dan murni-toko, jika digunakan untuk jenis mempersempit cukup bahwa kompiler akan menggunakan instruksi tunggal sehingga Anda tidak mendapatkan robek. Dan tentu sajavolatiletidak memiliki jaminan dari standar ISO C vs. kode penulisan yang mengkompilasi dengan asm yang sama menggunakan _Atomicdan mo_relaxed.


Jika Anda memiliki fungsi yang dilakukan global_var++;pada intatau long longyang Anda jalankan dari main dan secara tidak sinkron dari penangan sinyal, itu akan menjadi cara untuk menggunakan re-entrancy untuk membuat data-race UB.

Bergantung pada bagaimana itu dikompilasi (ke inc tujuan memori atau menambah, atau untuk memisahkan beban / inc / store) itu akan atomik atau tidak sehubungan dengan penangan sinyal di utas yang sama. Lihat Bisakah num ++ menjadi atom untuk 'int num'? untuk lebih lanjut tentang atomicity pada x86 dan di C ++. (C11 stdatomic.hdan _Atomicatributnya menyediakan fungsionalitas yang setara dengan std::atomic<T>templat C ++ 11 )

Pengecualian atau interupsi lainnya tidak dapat terjadi di tengah instruksi, jadi add tujuan memori adalah atomic wrt. konteks mengaktifkan CPU single-core. Hanya penulis DMA (koheren cache) yang dapat "menginjak" peningkatan dari add [mem], 1tanpa lockawalan pada CPU single-core. Tidak ada core lain yang dapat digunakan untuk menjalankan thread lain.

Jadi mirip dengan kasus sinyal: handler sinyal berjalan bukannya eksekusi normal dari benang yang menangani sinyal, sehingga tidak dapat ditangani di tengah satu instruksi.

Peter Cordes
sumber
2
Saya terdorong untuk menerima jawaban Anda sebagai jawaban terbaik, meskipun jawaban Icaru cukup bagi saya. Konsep yang jelas yang Anda katakan memberi saya seember topik untuk dipelajari sepanjang hari ini (dan selanjutnya). Sebenarnya, saya hampir tidak mendapatkan apa yang Anda tulis dalam dua paragraf pertama pada pandangan pertama. Terima kasih! Jika Anda mempublikasikan artikel di internet tentang komputer dan pemrograman, beri kami tautannya!
Daniel Bandeira
17

Melihat explorer compiler godbolt (setelah menambahkan yang hilang #include <unistd.h>), orang melihat bahwa untuk hampir semua kompiler x86_64 kode yang dihasilkan menggunakan gerakan QWORD untuk memuat onesdan zerosdalam satu instruksi.

        mov     rax, QWORD PTR main::ones[rip]
        mov     QWORD PTR data[rip], rax

Situs IBM mengatakan On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.yang mungkin benar untuk CPU pada tahun 2005 tetapi seperti yang ditunjukkan oleh kode tidak benar sekarang. Mengubah struct untuk memiliki dua panjang daripada dua int akan menunjukkan masalah ini.

Saya sebelumnya menulis bahwa ini adalah "atom" yang malas. Program ini hanya berjalan pada satu cpu. Setiap instruksi akan lengkap dari sudut pandang cpu ini (dengan asumsi tidak ada lagi yang mengubah memori seperti dma).

Jadi pada Ctingkat itu tidak didefinisikan bahwa kompiler akan memilih satu instruksi untuk menulis struct, dan korupsi yang disebutkan dalam makalah IBM dapat terjadi. Kompiler modern yang menargetkan CPU saat ini memang menggunakan satu instruksi. Satu instruksi cukup baik untuk menghindari korupsi untuk satu program threaded.

icarus
sumber
3
Coba ubah tipe data dari intmenjadi long long, dan kompilasi ke 32bit. Pelajarannya adalah Anda tidak pernah tahu kapan akan pecah.
ctrl-alt-delor
2
itu berarti, di mesin saya, penugasan dua nilai ini adalah operasi atom? (mempertimbangkan kompilasi untuk arsitektur x86_64)
Daniel Bandeira
1
long longmasih mengkompilasi ke satu instruksi untuk x86-64: 16-byte movdqa. Kecuali Anda menonaktifkan pengoptimalan, seperti di tautan Godbolt Anda. (Default GCC adalah -O0mode debug, yang penuh dengan noise store / reload dan biasanya tidak menarik untuk dilihat.)
Peter Cordes
Saya mengubah tipe menjadi "lama" setelah membaca semua komentar. Hasilnya menarik: hasil yang menunggu dicapai dan, dengan menyiapkan beberapa penghitung, itu dapat meningkatkan konsepsi lain karena bagaimana laju data yang tidak cocok dipengaruhi oleh sisa kode. Terima kasih atas semua bantuannya!
Daniel Bandeira