Timer sistem di Windows: perubahan besar

Perilaku Penjadwal Windows berubah secara signifikan di Windows 10 2004 tanpa peringatan atau perubahan dokumentasi apa pun. Ini kemungkinan akan merusak beberapa aplikasi. Ini bukan pertama kalinya terjadi , tetapi perubahan ini lebih serius.



Singkatnya, panggilan ke timeBeginPeriod dari satu proses sekarang mempengaruhi proses lain kurang dari sebelumnya, meskipun efeknya masih ada.



Saya pikir perilaku baru pada dasarnya adalah perbaikan, tetapi aneh dan pantas untuk didokumentasikan. Sejujurnya, saya memperingatkan Anda - Saya hanya memiliki hasil eksperimen saya sendiri, jadi saya hanya bisa menebak tentang tujuan dan beberapa efek samping dari perubahan ini. Jika ada temuan saya yang salah, beri tahu saya.



Timer menyela dan alasan keberadaannya



Pertama, sedikit konteks tentang desain sistem operasi. Diharapkan agar program dapat tertidur dan kemudian bangun. Faktanya, ini tidak boleh dilakukan terlalu sering - utas biasanya menunggu acara, bukan pengatur waktu - tetapi terkadang perlu. Jadi, Windows memiliki fungsi Tidur  - berikan durasi tidur yang diinginkan dalam milidetik dan itu akan membangunkan proses:



Tidur (1);



Perlu dipertimbangkan bagaimana ini diterapkan. Idealnya, saat Sleep (1) dipanggil, prosesor akan tidur. Tetapi bagaimana sistem operasi membangunkan utas jika prosesor sedang tidur? Jawabannya adalah interupsi perangkat keras. OS memprogram chip - pengatur waktu perangkat keras yang kemudian memicu interupsi yang membangunkan prosesor dan OS kemudian memulai utas Anda.



Fungsi WaitForSingleObject dan WaitForMultipleObjects juga memiliki nilai waktu tunggu, dan waktu tunggu ini diimplementasikan menggunakan mekanisme yang sama.



Jika ada banyak utas yang menunggu pengatur waktu, maka OS dapat memprogram pengatur waktu perangkat keras untuk waktu individual untuk setiap utas, tetapi ini biasanya mengarah pada fakta bahwa utas bangun secara acak, dan prosesor tidak tidur secara normal. Efisiensi daya CPU sangat bergantung pada waktu tidurnya ( waktu normal 8ms ), dan bangun acak tidak berkontribusi pada hal ini. Jika beberapa utas disinkronkan atau mengumpulkan pengatur waktunya menunggu, sistem menjadi lebih hemat energi.



Ada banyak cara untuk menggabungkan bangun, tetapi mekanisme utama di Windows adalah menghentikan secara global pengatur waktu yang berdetak dengan kecepatan konstan. Ketika utas memanggil Tidur (n) , OS akan menjadwalkan utas untuk memulai segera setelah interupsi timer pertama. Ini berarti bahwa utas mungkin akan bangun sedikit kemudian, tetapi Windows bukanlah OS waktu nyata, itu tidak menjamin waktu bangun tertentu sama sekali (saat ini, inti prosesor mungkin sibuk), jadi cukup normal untuk bangun sedikit kemudian.



Interval antara interupsi Timer tergantung pada versi Windows dan perangkat keras, tetapi pada semua mesin saya itu gagal untuk 15.625ms (1000ms / 64). Artinya jika Anda memanggil Sleep (1)pada beberapa waktu acak, maka proses akan dibangunkan di suatu tempat antara 1.0ms dan 16.625ms di masa mendatang ketika interupsi pengatur waktu global berikutnya dipicu (atau satu kali kemudian jika terlalu dini).



Singkatnya, sifat penundaan pengatur waktu adalah sedemikian rupa (kecuali aktif menunggu prosesor digunakan, dan tolong jangan gunakan ), OS dapat membangunkan utas hanya pada waktu tertentu menggunakan interupsi pengatur waktu, dan Windows menggunakan interupsi reguler.



Beberapa program tidak mengakomodasi latensi yang begitu luas (WPF, SQL Server, Quartz, PowerDirector, Chrome, Go Runtime, banyak game, dll.). Untungnya, mereka dapat memperbaiki masalah dengan fungsi timeBeginPeriodyang memungkinkan program meminta interval yang lebih kecil. Ada juga fungsi NtSetTimerResolution yang memungkinkan interval disetel kurang dari satu milidetik, tetapi ini jarang digunakan dan tidak pernah diperlukan, jadi saya tidak akan menyebutkannya lagi.



Dekade kegilaan



Inilah hal yang gila: timeBeginPeriod dapat dipanggil oleh program apa pun dan itu mengubah interval interupsi timer, dan interupsi timer adalah sumber daya global.



Mari kita bayangkan bahwa proses A berada dalam satu lingkaran dengan panggilan ke Sleep (1) . Ini salah, tapi memang benar, dan secara default ini bangun setiap 15,625ms, atau 64 kali per detik. Kemudian proses B masuk dan memanggil timeBeginPeriod (2) . Ini menyebabkan pengatur waktu menyala lebih sering, dan tiba-tiba proses A terbangun 500 kali per detik, bukan 64 kali per detik. Ini adalah kegilaan! Tapi begitulah cara Windows selalu bekerja.



Pada titik ini, jika proses C muncul dan memanggil timeBeginPeriod (4), itu tidak akan mengubah apa pun - proses A akan terus aktif 500 kali per detik. Dalam situasi seperti itu, bukan panggilan terakhir yang menetapkan aturan, tetapi panggilan dengan interval minimum.



Jadi, panggilan ke timeBeginPeriod dari program yang sedang berjalan dapat menyetel interval interupsi timer global. Jika program ini keluar atau memanggil timeEndPeriod , maka minimum baru akan berlaku. Jika satu program memanggil timeBeginPeriod (1) , ini sekarang menjadi interval interupsi pengatur waktu di seluruh sistem. Jika satu program memanggil timeBeginPeriod (1) dan program lain memanggil timeBeginPeriod (4) , maka interval interupsi pengatur waktu satu milidetik menjadi hukum universal.



Hal ini penting karena laju interupsi pengatur waktu yang tinggi - dan laju penjadwalan utas yang tinggi - dapat menghabiskan daya CPU yang signifikan, seperti yang dibahas di sini .



Salah satu aplikasi yang membutuhkan penjadwalan berbasis timer adalah web browser. Standar JavaScript memiliki fungsi setTimeout yang meminta browser untuk memanggil fungsi JavaScript setelah beberapa milidetik. Chromium menggunakan timer untuk menerapkan ini dan fitur lainnya (pada dasarnya WaitForSingleObject dengan batas waktu, bukan Tidur). Hal ini sering kali membutuhkan peningkatan kecepatan interupsi pengatur waktu. Untuk menjaga masa pakai baterai tetap rendah, Chromium baru-baru ini didesain ulang untuk menjaga laju interupsi pengatur waktu di bawah 125 Hz (interval 8 md) pada daya baterai .



timeGetTime



Fungsi timeGetTime (jangan disamakan dengan GetTickCount) mengembalikan waktu saat ini, diperbarui dengan interupsi pengatur waktu. Prosesor secara historis tidak pandai menjaga waktu yang akurat (jam mereka sengaja diosilasi untuk menghindari berfungsi sebagai pemancar FM, dan karena alasan lain), sehingga CPU sering mengandalkan generator jam terpisah untuk mempertahankan waktu yang akurat. Membaca dari chip ini mahal, itulah sebabnya Windows mempertahankan penghitung milidetik 64-bit yang diperbarui dengan penghitung waktu. Pengatur waktu ini disimpan dalam memori bersama, sehingga proses apa pun dapat dengan murah membaca waktu saat ini dari sana tanpa harus pergi ke jam. timeGetTime memanggil ReadInterruptTick , yang pada dasarnya hanya membaca penghitung 64-bit ini. Sesederhana itu!



Karena penghitung diperbarui oleh penghitung waktu, kita dapat melacaknya dan menemukan frekuensi penghitung waktu.



Realitas baru yang tidak terdokumentasi



Dengan dirilisnya Windows 10 2004 (April 2020), beberapa mekanisme ini telah sedikit berubah, tetapi dengan cara yang sangat membingungkan. Pertama, ada pesan bahwa timeBeginPeriod tidak lagi berfungsi . Faktanya, semuanya ternyata jauh lebih rumit.



Eksperimen pertama memberikan hasil yang beragam. Ketika saya menjalankan program dengan panggilan ke timeBeginPeriod (2) , clockres menunjukkan interval pengatur waktu 2.0ms , tetapi program pengujian terpisah dengan loop Sleep (1) terbangun sekitar 64 kali per detik, bukan 500 kali seperti pada versi Windows sebelumnya.



Eksperimen sains



Kemudian saya menulis beberapa program untuk mempelajari perilaku sistem. Satu program ( change_interval.cpp ) hanya berada dalam satu lingkaran, memanggil timeBeginPeriod dengan interval 1 hingga 15 ms . Dia menahan setiap interval selama empat detik, lalu melanjutkan ke interval berikutnya, dan seterusnya dalam lingkaran. Lima belas baris kode. Mudah.



Program lain ( measure_interval.cpp ) menjalankan beberapa tes untuk memeriksa bagaimana perilakunya berubah ketika change_interval.cpp berubah. Program memonitor tiga parameter.



  1. Dia menanyakan OS apa resolusi timer global saat ini yang menggunakan NtQueryTimerResolution .

  2. timeGetTime, ,  — , .

  3. Sleep(1), . .


@FelixPetriconi menjalankan pengujian untuk saya pada Windows 10 1909 dan saya menjalankan pengujian pada Windows 10 2004. Berikut adalah hasil bebas jitter :







Ini berarti timeBeginPeriod masih menyetel interval timer global di semua versi Windows. Dari hasil timeGetTime () , kita dapat mengatakan bahwa interupsi dipicu pada kecepatan ini pada setidaknya satu inti prosesor, dan waktu diperbarui. Perhatikan juga bahwa 2.0 di baris pertama untuk 1909 juga 2.0 di Windows XP , kemudian 1.0 di Windows 7/8, dan kemudian tampaknya telah kembali ke 2.0 lagi?



Namun, perilaku penjadwalan berubah secara dramatis di Windows 10 2004. Sebelumnya, penundaan untuk Sleep (1)dalam proses apa pun hanya sama dengan interval interupsi timer, kecuali timeBeginPeriod (1), memberikan grafik seperti ini:







Pada Windows 10 2004, hubungan antara timeBeginPeriod dan latensi tidur dalam proses lain (yang tidak memanggil timeBeginPeriod ) tampak aneh:







Bentuk persis dari sisi kiri grafik tidak jelas, tetapi itu pasti berjalan dalam arah yang berlawanan dari yang sebelumnya!



Mengapa?



Efek



Seperti yang ditunjukkan dalam diskusi tentang reddit dan berita peretas, kemungkinan paruh kiri grafik adalah upaya untuk meniru latensi "normal" sedekat mungkin, mengingat ketepatan yang tersedia dari interupsi pengatur waktu global. Artinya, dengan interval interupsi 6 milidetik, penundaan terjadi sekitar 12 ms (dua siklus), dan dengan interval interupsi 7 milidetik, penundaan terjadi sekitar 14 ms (dua siklus). Mengukur penundaan yang sebenarnya, bagaimanapun, menunjukkan bahwa kenyataannya bahkan lebih membingungkan. Dengan pengatur waktu interupsi diatur ke 7 md, latensi Tidur (1) 14 md bahkan bukan hasil yang paling umum:







Beberapa pembaca mungkin menyalahkan gangguan acak pada sistem, tetapi ketika tingkat interupsi pengatur waktu 9 md atau lebih tinggi, bunyinya nol, jadi bukan bisa jadi penjelasan.Coba jalankan sendiri kode yang diperbarui . Interval penghitung waktu dari 4ms hingga 8ms tampaknya sangat kontroversial. Pengukuran interval mungkin harus dilakukan dengan menggunakan QueryPerformanceCounter karena kode saat ini dipengaruhi secara acak oleh perubahan dalam aturan penjadwalan dan perubahan presisi pengatur waktu.



Ini semua sangat aneh, dan saya tidak mengerti baik logika maupun implementasinya. Ini mungkin kesalahan, tapi saya meragukannya. Saya pikir ada logika kompatibilitas mundur yang kompleks di balik ini. Tetapi cara paling efektif untuk menghindari masalah kompatibilitas adalah dengan mendokumentasikan perubahan, sebaiknya sebelumnya, dan di sini pengeditan dilakukan tanpa pemberitahuan apa pun.



Ini tidak akan mempengaruhi kebanyakan program. Jika suatu proses menginginkan interupsi pengatur waktu yang lebih cepat, maka proses itu harus memanggil timeBeginPeriod itu sendiri.... Namun, masalah berikut mungkin terjadi:



  • Program mungkin secara tidak sengaja mengasumsikan bahwa Sleep (1) dan timeGetTime memiliki resolusi yang sama, yang tidak lagi menjadi masalah. Meski, anggapan seperti itu tampaknya tidak mungkin.

  • , .  — Windows System Timer Tool TimerResolution 1.2. «» , . , . , , .

  • , , . , . . , , timeBeginPeriod , , .




Program pengujian change_interval.cpp hanya bekerja jika tidak ada yang meminta laju interupsi pengatur waktu yang lebih tinggi. Karena Chrome dan Visual Studio memiliki kebiasaan melakukan ini, saya harus melakukan sebagian besar eksperimen saya tanpa akses internet dan pengkodean di notepad . Seseorang menyarankan Emacs, tapi mendapatkan terlibat dalam ini diskusi adalah di luar kekuasaan saya.



All Articles