epoll dan Windows IO Completion Ports: perbedaan praktis

Pendahuluan


Pada artikel ini, kita akan mencoba memahami bagaimana mekanisme epoll berbeda dari port penyelesaian dalam praktiknya (Windows I / O Completion Port atau IOCP). Ini mungkin menarik untuk arsitek sistem yang mendesain layanan jaringan berkinerja tinggi atau pemrogram porting kode jaringan dari Windows ke Linux atau sebaliknya.

Kedua teknologi ini sangat efektif untuk menangani sejumlah besar koneksi jaringan.

Mereka berbeda dari metode lain dalam hal-hal berikut:

  • Tidak ada batasan (kecuali untuk total sumber daya sistem) pada jumlah total deskriptor yang diamati dan jenis acara
  • Penskalaan berfungsi dengan baik - jika Anda sudah memantau penjelas N, maka beralih ke pemantauan N + 1 akan membutuhkan sedikit waktu dan sumber daya
  • Cukup mudah untuk menggunakan kumpulan utas untuk memproses acara secara paralel
  • Tidak ada gunanya menggunakan koneksi jaringan tunggal. Semua manfaat mulai muncul dengan 1.000+ koneksi

Mengutip semua hal di atas, kedua teknologi ini dirancang untuk mengembangkan layanan jaringan yang memproses banyak koneksi masuk dari klien. Tetapi pada saat yang sama, ada perbedaan yang signifikan antara mereka dan ketika mengembangkan layanan yang sama, penting untuk mengetahuinya.

(Pembaruan: artikel ini adalah terjemahan )


Jenis pemberitahuan


Perbedaan pertama dan paling penting antara epoll dan IOCP adalah bagaimana Anda diberitahu tentang suatu peristiwa.

  • epoll memberi tahu Anda ketika deskriptor siap untuk dapat melakukan sesuatu dengannya - " sekarang Anda dapat mulai membaca data "
  • IOCP memberi tahu Anda ketika operasi yang diminta selesai - " Anda diminta membaca data dan ini dia baca "

Saat menggunakan aplikasi epoll:

  • Memutuskan operasi mana yang ingin dilakukan dengan beberapa deskriptor (baca, tulis, atau keduanya)
  • Setel topeng yang sesuai menggunakan epoll_ctl
  • Panggilan epoll_wait, yang memblokir utas saat ini hingga setidaknya satu peristiwa yang diharapkan terjadi (atau batas waktu habis)
  • Iterate atas peristiwa yang diterima, mengambil pointer ke konteks (dari bidang data.ptr)
  • Memulai pemrosesan peristiwa menurut jenisnya (baca, tulis, atau keduanya operasi)
  • Setelah operasi selesai (apa yang harus terjadi segera), ia terus menunggu data diterima / dikirim

Saat menggunakan aplikasi IOCP:

  • Memulai beberapa operasi (ReadFile atau WriteFile) untuk beberapa deskriptor, menggunakan argumen OVERLAPPED yang tidak kosong. Sistem operasi menambahkan persyaratan untuk melakukan operasi ini ke antriannya, dan fungsi yang dipanggil segera (tanpa menunggu operasi selesai) kembali.
  • Panggilan GetQueuedCompletionStatus () , yang memblokir utas saat ini sampai salah satu dari permintaan yang ditambahkan sebelumnya selesai. Jika beberapa telah selesai, hanya satu yang akan dipilih.
  • Ini memproses notifikasi yang diterima dari penyelesaian operasi menggunakan kunci penyelesaian dan pointer ke OVERLAPPED.
  • Terus menunggu data diterima / dikirim

Perbedaan jenis pemberitahuan memungkinkan (dan sangat sepele) untuk meniru IOCP menggunakan epoll. Misalnya, proyek Wine melakukan hal itu. Namun, melakukan yang sebaliknya tidaklah sesederhana itu. Bahkan jika Anda berhasil, kemungkinan akan mengakibatkan hilangnya kinerja.

Ketersediaan data


Jika Anda berencana untuk membaca data, maka kode Anda harus memiliki semacam buffer di mana Anda berencana untuk membacanya. Jika Anda berencana untuk mengirim data, harus ada buffer dengan data yang siap dikirim.

  • epoll sama sekali tidak khawatir tentang keberadaan buffer ini dan tidak menggunakannya
  • IOCP, buffer ini diperlukan. Inti dari menggunakan IOCP adalah pekerjaan dengan gaya "baca saya 256 byte dari soket ini ke buffer ini". Kami membentuk permintaan seperti itu, memberikannya ke OS, kami sedang menunggu pemberitahuan penyelesaian operasi (dan jangan menyentuh buffer saat ini!)

Layanan jaringan tipikal beroperasi dengan objek koneksi, yang akan mencakup deskriptor dan buffer terkait untuk membaca / menulis data. Biasanya, benda-benda ini hancur ketika soket yang sesuai ditutup. Dan ini memberlakukan beberapa batasan saat menggunakan IOCP.

IOCP bekerja dengan menambahkan permintaan antrian untuk membaca dan menulis data, permintaan ini dieksekusi dalam urutan antrian (mis. Beberapa waktu kemudian). Dalam kedua kasus, buffer yang ditransfer harus terus ada hingga selesainya operasi yang diperlukan. Selain itu, seseorang bahkan tidak dapat memodifikasi data dalam buffer ini sambil menunggu. Ini memberlakukan batasan penting:

  • Anda tidak dapat menggunakan variabel lokal (ditempatkan di tumpukan) sebagai buffer. Buffer harus divalidasi sebelum operasi baca / tulis selesai, dan tumpukan dihancurkan ketika fungsi saat ini keluar
  • Anda tidak dapat merealokasi buffer dengan cepat (misalnya, ternyata Anda perlu mengirim lebih banyak data dan Anda ingin menambah buffer). Anda hanya dapat membuat buffer baru dan permintaan kirim baru
  • Jika Anda menulis sesuatu seperti proxy, ketika data yang sama akan dibaca dan dikirim, Anda harus menggunakan dua buffer terpisah untuknya. Anda tidak dapat meminta OS untuk membaca data dalam buffer dalam satu permintaan, dan dalam permintaan lain kirim data ini di sana
  • Anda perlu berpikir hati-hati tentang bagaimana kelas manajer koneksi Anda akan menghancurkan setiap koneksi tertentu. Anda harus memiliki jaminan penuh bahwa pada saat penghancuran koneksi tidak ada satu permintaan untuk membaca / menulis data menggunakan buffer dari koneksi ini.

Operasi IOCP juga mensyaratkan melewatkan pointer ke struktur OVERLAPPED, yang juga harus terus ada (dan tidak dapat digunakan kembali) sampai penyelesaian operasi yang diharapkan. Ini berarti bahwa jika Anda perlu membaca dan menulis data pada saat yang sama, Anda tidak dapat mewarisi dari struktur OVERLAPPED (sebuah ide yang sering muncul di pikiran). Sebagai gantinya, Anda perlu menyimpan dua struktur OVERLAPPED di kelas terpisah Anda sendiri, melewati salah satunya ke dalam permintaan baca dan yang lainnya ke dalam permintaan tulis.

epoll tidak menggunakan buffer apa pun yang diteruskan dari kode pengguna, jadi semua masalah ini tidak ada hubungannya dengan itu.

Ubah kondisi tunggu


Menambahkan jenis baru dari acara yang diharapkan (misalnya, kami sedang menunggu kesempatan untuk membaca data dari soket, dan sekarang kami juga ingin dapat mengirimkannya) adalah mungkin dan cukup sederhana untuk epoll dan IOCP. epoll memungkinkan Anda untuk mengubah topeng acara yang diharapkan (kapan saja, bahkan dari utas lainnya), dan IOCP memungkinkan Anda untuk memulai operasi lain untuk menunggu jenis acara baru.

Namun mengubah atau menghapus acara yang diharapkan berbeda. epoll masih memungkinkan Anda untuk mengubah kondisinya dengan memanggil epoll_ctl (termasuk dari utas lainnya). IOCP semakin sulit. Jika operasi I / O direncanakan, itu dapat dibatalkan dengan memanggil fungsi CancelIo () . Lebih buruk lagi, hanya utas yang sama yang memulai operasi awal yang dapat memanggil fungsi ini. Semua ide untuk mengatur aliran kontrol terpisah rusak tentang batasan ini. Selain itu, bahkan setelah memanggil CancelIo (), kami tidak dapat memastikan bahwa operasi akan segera dibatalkan (mungkin sudah dalam proses, ia menggunakan struktur OVERLAPPED dan buffer yang diteruskan untuk membaca / menulis). Kita masih harus menunggu sampai operasi selesai (hasilnya akan dikembalikan oleh fungsi GetOverlappedResult ()) dan hanya setelah itu kita dapat membebaskan buffer.

Masalah lain dengan IOCP adalah bahwa begitu suatu operasi telah dijadwalkan untuk dieksekusi, itu tidak lagi dapat diubah. Misalnya, Anda tidak dapat mengubah permintaan ReadFile yang dijadwalkan dan mengatakan bahwa Anda ingin membaca hanya 10 byte, bukan 8192. Anda harus membatalkan operasi saat ini dan memulai yang baru. Ini bukan masalah bagi epoll, yang ketika Anda mulai menunggu, tidak tahu berapa banyak data yang ingin Anda baca saat pemberitahuan tentang kemampuan membaca data datang.

Koneksi non-pemblokiran


Beberapa implementasi layanan jaringan (layanan terkait, FTP, p2p) memerlukan koneksi keluar. Baik epoll dan IOCP mendukung permintaan koneksi non-blocking, tetapi dengan cara yang berbeda.

Saat menggunakan epoll, kode ini umumnya sama dengan untuk pilih atau jajak pendapat. Anda membuat soket yang tidak terblokir, panggil koneksi () untuknya dan tunggu pemberitahuan tentang ketersediaannya untuk ditulis.

Saat menggunakan IOCP, Anda perlu menggunakan fungsi ConnectEx yang terpisah, karena panggilan untuk menyambung () tidak menerima struktur OVERLAPPED, yang berarti tidak dapat membuat pemberitahuan tentang perubahan kondisi soket nanti. Jadi kode inisiasi koneksi tidak hanya akan berbeda dari kode menggunakan epoll, bahkan akan berbeda dari kode Windows menggunakan pilih atau polling. Namun, perubahan bisa dianggap minimal.

Menariknya, terima () bekerja dengan IOCP seperti biasa. Ada fungsi AcceptEx, tetapi perannya sama sekali tidak terkait dengan koneksi yang tidak menghalangi. Ini bukan "penerimaan non-pemblokiran", seperti yang mungkin Anda pikirkan secara analogi dengan connect / ConnectEx.

Pemantauan acara


Seringkali setelah memicu suatu peristiwa, data tambahan datang dengan sangat cepat. Sebagai contoh, kami mengharapkan input dari soket tiba menggunakan epoll atau IOCP, kami mendapatkan acara tentang beberapa byte data pertama, dan di sana, saat kami membacanya, ratusan byte lainnya datang. Bisakah saya membacanya tanpa memulai kembali pemantauan acara?

Menggunakan epoll dimungkinkan. Anda mendapatkan acara "sesuatu sekarang dapat dibaca" - dan Anda membaca semua yang dapat dibaca dari soket (sampai Anda mendapatkan kesalahan EAGAIN). Hal yang sama dengan mengirim data - ketika Anda menerima sinyal bahwa soket siap untuk mengirim data, Anda dapat menulis sesuatu ke dalamnya sampai fungsi tulis mengembalikan EAGAIN.

Dengan IOCP ini tidak akan berfungsi. Jika Anda meminta soket untuk membaca atau mengirim 10 byte data - itu adalah berapa banyak yang akan dibaca / dikirim (bahkan jika lebih banyak sudah bisa dilakukan). Untuk setiap blok berikutnya, Anda perlu membuat permintaan terpisah menggunakan ReadFile atau WriteFile, dan kemudian tunggu sampai dieksekusi. Ini dapat menciptakan tingkat kerumitan tambahan. Perhatikan contoh berikut:

  1. Kelas soket membuat permintaan untuk membaca data dengan memanggil ReadFile. Utas A dan B menunggu hasilnya dengan memanggil GetOverlappedResult ()
  2. Operasi baca selesai, utas A menerima pemberitahuan dan memanggil metode kelas soket untuk memproses data yang diterima
  3. Kelas soket memutuskan bahwa data ini tidak cukup, kita harus mengharapkan yang berikut ini. Ini menempatkan permintaan baca lainnya.
  4. Permintaan ini dieksekusi segera (data telah tiba, OS dapat segera mengirimnya). Stream B menerima pemberitahuan, membaca data, dan meneruskannya ke kelas soket.
  5. Saat ini, fungsi membaca data dalam kelas soket dipanggil dari kedua aliran A dan B, yang mengarah pada risiko korupsi data (tanpa menggunakan objek sinkronisasi), atau ke jeda tambahan (saat menggunakan objek sinkronisasi)

Dengan objek sinkronisasi dalam hal ini umumnya sulit. Nah, jika dia sendirian. Tetapi jika kita memiliki 100.000 koneksi dan masing-masing dari mereka akan memiliki semacam objek sinkronisasi, ini dapat secara serius menekan sumber daya sistem. Dan jika Anda masih menyimpan 2 (dalam kasus pemisahan permintaan pemrosesan untuk membaca dan menulis)? Lebih buruk lagi.

Solusi yang biasa di sini adalah membuat kelas manajer koneksi yang akan bertanggung jawab untuk memanggil ReadFile atau WriteFile untuk kelas koneksi. Ini berfungsi lebih baik, tetapi membuat kode lebih kompleks.

Kesimpulan


Baik epoll dan IOCP cocok (dan digunakan dalam praktiknya) untuk menulis layanan jaringan berkinerja tinggi yang dapat menangani sejumlah besar koneksi. Teknologi itu sendiri berbeda dalam cara mereka menangani acara. Perbedaan-perbedaan ini sangat signifikan sehingga hampir tidak layak untuk mencoba menuliskannya pada beberapa basis umum (jumlah kode yang sama akan minimal). Beberapa kali saya berusaha untuk membawa kedua pendekatan ke beberapa jenis solusi universal - dan setiap kali hasilnya lebih buruk dalam hal kompleksitas, keterbacaan dan dukungan dibandingkan dengan dua implementasi independen. Hasil universal yang diperoleh harus diabaikan setiap kali.

Saat porting kode dari satu platform ke platform lain, biasanya lebih mudah untuk porting kode IOCP untuk menggunakan epoll daripada sebaliknya.

Kiat:

  • Jika tugas Anda adalah mengembangkan layanan jaringan lintas platform, Anda harus mulai dengan implementasi Windows menggunakan IOCP. Setelah semuanya siap dan debugged - tambahkan trioll backend sepele.
  • Anda sebaiknya tidak mencoba menulis kelas umum Connection dan ConnectionMgr yang mengimplementasikan logika epoll dan IOCP secara bersamaan. Ini terlihat buruk dari sudut pandang arsitektur kode dan mengarah ke sekelompok semua jenis #ifdef dengan logika berbeda di dalamnya. Lebih baik membuat kelas dasar dan mewarisi implementasi yang terpisah dari mereka. Di kelas dasar, Anda dapat menyimpan beberapa metode atau data umum, jika ada.
  • Pantau dengan cermat masa objek dari kelas Koneksi (atau apa pun yang Anda sebut kelas tempat buffer untuk data yang diterima / dikirim akan disimpan). Tidak boleh dimusnahkan sampai operasi baca / tulis yang dijadwalkan menggunakan buffernya selesai.

Source: https://habr.com/ru/post/id415403/


All Articles