pilih / poll / epoll: perbedaan praktis

Saat merancang aplikasi jaringan berkinerja tinggi dengan soket non-pemblokiran, penting untuk memutuskan metode pemantauan peristiwa jaringan yang akan kita gunakan. Ada beberapa dari mereka, dan masing-masing baik dan buruk dengan caranya sendiri. Memilih metode yang tepat dapat menjadi sangat penting untuk arsitektur aplikasi Anda.

Dalam artikel ini kami akan mempertimbangkan:

  • pilih ()
  • jajak pendapat ()
  • epoll ()
  • libevent

Menggunakan select ()


Yang lama, terbukti selama bertahun-tahun pekerja keras pilih () diciptakan kembali pada masa itu ketika "soket" disebut " soket Berkeley ". Metode ini tidak termasuk dalam spesifikasi pertama dari soket Berkeley itu sendiri, karena pada masa itu masih belum ada konsep I / O yang tidak menghalangi. Tapi di suatu tempat di tahun 80-an dia muncul, dan dengan itu pilih (). Sejak itu, tidak ada yang berubah secara signifikan dalam antarmuka.

Untuk menggunakan select (), pengembang perlu menginisialisasi dan mengisi beberapa struktur fd_set dengan deskriptor dan peristiwa yang perlu dipantau, dan kemudian memanggil select (). Kode khas terlihat seperti ini:

fd_set fd_in, fd_out; struct timeval tv; //   FD_ZERO( &fd_in ); FD_ZERO( &fd_out ); //        sock1 FD_SET( sock1, &fd_in ); //        sock2 FD_SET( sock2, &fd_out ); //       (select   ) int largest_sock = sock1 > sock2 ? sock1 : sock2; //    10  tv.tv_sec = 10; tv.tv_usec = 0; //  select int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv ); //    if ( ret == -1 ) //  else if ( ret == 0 ) // ,    else { if ( FD_ISSET( sock1, &fd_in ) ) //    sock1 if ( FD_ISSET( sock2, &fd_out ) ) //    sock2 } 

Ketika select () dirancang, tidak ada yang berharap bahwa di masa depan kita perlu menulis aplikasi multi-utas yang melayani ribuan koneksi. Select () memiliki beberapa kelemahan signifikan yang membuatnya kurang cocok untuk bekerja pada sistem tersebut. Yang utama adalah:

  • select memodifikasi struktur fd_sets yang diteruskan ke sana, sehingga tidak ada satupun yang dapat digunakan kembali. Bahkan jika Anda tidak perlu mengubah apa pun (misalnya, setelah menerima sepotong data, Anda ingin mendapatkan lebih banyak), struktur fd_sets harus diinisialisasi ulang. Baik, atau salin dari cadangan yang disimpan sebelumnya menggunakan FD_COPY. Dan ini harus dilakukan berulang-ulang, sebelum setiap panggilan pilih.
  • Untuk mengetahui dengan tepat deskriptor mana yang menghasilkan acara, Anda harus melakukan polling secara manual dengan FD_ISSET. Ketika Anda memantau 2000 deskriptor, dan peristiwa itu terjadi hanya untuk salah satu dari mereka (yang, menurut hukum kekejaman, akan menjadi yang terakhir dalam daftar) - Anda akan membuang banyak sumber daya prosesor.
  • Apakah saya baru saja menyebutkan 2.000 deskriptor? Saya senang tentang hal itu. pilih tidak mendukung banyak. Yah, setidaknya di Linux biasa, dengan kernel yang biasa. Jumlah maksimum deskriptor yang diamati secara simultan dibatasi oleh FD_SETSIZE konstan, yang sama dengan 1024 di Linux. Beberapa sistem operasi memungkinkan Anda untuk mengimplementasikan hack dengan menimpa nilai FD_SETSIZE sebelum memasukkan file header sys / select.h, tetapi hack ini bukan bagian dari beberapa standar umum. Linux yang sama akan mengabaikannya.
  • Anda tidak dapat bekerja dengan deskriptor dari set yang dapat diamati dari utas lainnya. Bayangkan utas menjalankan kode di atas. Jadi itu dimulai dan menunggu acara di pilih (). Sekarang bayangkan Anda memiliki utas lain yang memantau keseluruhan beban pada sistem, dan sekarang ia memutuskan bahwa data dari soket sock1 belum tiba terlalu lama dan sudah waktunya untuk memutuskan koneksi. Karena soket ini dapat digunakan kembali untuk melayani klien baru, sebaiknya tutup dengan benar. Tapi utas pertama mengamati deskriptor ini sekarang. Apa yang akan terjadi jika kita menutupnya semua sama? Oh, dokumentasi memiliki jawaban untuk pertanyaan ini dan Anda tidak akan menyukainya: "Jika gagang yang diamati dengan select () ditutup oleh utas lainnya, Anda akan mendapatkan perilaku yang tidak ditentukan."
  • Masalah yang sama muncul ketika mencoba mengirim beberapa data melalui sock1. Kami tidak akan mengirim apa pun sampai pilih selesai pekerjaannya.
  • Pilihan acara yang bisa kita pantau sangat terbatas. Misalnya, untuk menentukan bahwa soket jarak jauh telah ditutup, Anda harus, pertama, memantau peristiwa kedatangan data di atasnya, dan kedua, mencoba membaca data ini (baca akan mengembalikan 0 untuk soket tertutup). Ini masih bisa disebut dapat diterima saat membaca data dari soket (baca 0 - soket ditutup), tetapi bagaimana jika tugas kita saat ini mengirim data ke soket ini dan tidak ada data yang membacanya dari sekarang?
  • pilih menempatkan beban yang tidak perlu pada Anda untuk menghitung "deskriptor terbesar" dan meneruskannya sebagai parameter terpisah

Tentu saja, semua hal di atas bukanlah berita. Pengembang sistem operasi telah lama menyadari masalah ini dan banyak dari mereka diperhitungkan saat merancang metode polling. Pada titik ini, Anda mungkin bertanya, mengapa kita bahkan mempelajari sejarah kuno sekarang, dan adakah alasan saat ini untuk menggunakan pemilihan kuno? Ya, ada dua alasan seperti itu. Bukan fakta bahwa mereka akan berguna bagi Anda kapan-kapan, tetapi mengapa tidak mencari tahu tentang mereka.

Alasan pertama adalah portabilitas. select () telah bersama kami selama satu juta tahun. Tidak peduli apa rimba platform perangkat keras dan perangkat lunak membawa Anda, jika ada jaringan di sana, akan ada pilih. Mungkin tidak ada metode lain, tetapi pilih akan hampir dijamin. Dan jangan berpikir bahwa saya sekarang jatuh dalam kepikunan pikun dan ingat sesuatu seperti kartu punch dan ENIAC, tidak. Tidak ada lagi metode jajak pendapat modern , misalnya, di Windows XP . Tapi pilih.

Alasan kedua lebih eksotis dan terkait dengan fakta bahwa pilih dapat (secara teoritis) bekerja dengan batas waktu urutan satu nanosecond (jika perangkat keras memungkinkan), sementara jajak pendapat dan epoll hanya mendukung akurasi milidetik. Ini seharusnya tidak memainkan peran khusus pada desktop biasa (atau bahkan server), di mana Anda masih tidak memiliki timer akurasi nanosecond perangkat keras. Tetapi masih di dunia ada sistem real-time yang memiliki timer seperti itu. Jadi saya mohon, ketika Anda menulis firmware reaktor nuklir atau roket - jangan terlalu malas untuk mengukur waktu hingga nanodetik. Anda tahu, saya ingin hidup.

Kasus yang dijelaskan di atas mungkin adalah satu-satunya di mana Anda benar-benar tidak punya pilihan apa yang harus digunakan (hanya pilih yang cocok). Namun, jika Anda menulis aplikasi reguler untuk bekerja pada perangkat keras biasa, dan Anda akan beroperasi dengan jumlah soket yang memadai (puluhan, ratusan - dan tidak lebih), maka perbedaan dalam jajak pendapat dan kinerja pilih tidak akan terlihat, sehingga pilihan akan didasarkan pada faktor lain.

Polling dengan polling ()


polling adalah metode polling socket yang lebih baru, dibuat setelah orang mulai mencoba untuk menulis layanan jaringan yang besar dan sarat muatan. Ini dirancang jauh lebih baik dan tidak menderita sebagian besar kelemahan dari metode pilih. Dalam kebanyakan kasus, saat menulis aplikasi modern, Anda akan memilih antara menggunakan polling dan epoll / libevent.

Untuk menggunakan polling, pengembang perlu menginisialisasi anggota struktur pollfd dengan deskriptor dan acara yang dapat diamati, dan kemudian memanggil polling ().
Kode khas terlihat seperti ini:

 //   struct pollfd fds[2]; //  sock1      fds[0].fd = sock1; fds[0].events = POLLIN; //   sock2 -  fds[1].fd = sock2; fds[1].events = POLLOUT; //   10  int ret = poll( &fds, 2, 10000 ); //    if ( ret == -1 ) //  else if ( ret == 0 ) // ,    else { //  ,  revents      if ( pfd[0].revents & POLLIN ) pfd[0].revents = 0; //     sock1 if ( pfd[1].revents & POLLOUT ) pfd[1].revents = 0; //     sock2 } 

Polling dibuat untuk menyelesaikan masalah metode pilih, mari kita lihat bagaimana hasilnya:

  • Tidak ada batasan jumlah deskriptor yang diamati, lebih dari 1024 dapat dipantau
  • Struktur pollfd tidak dimodifikasi, yang memungkinkan untuk menggunakannya kembali di antara panggilan ke polling () - Anda hanya perlu mengatur ulang bidang revents.
  • Acara yang diamati lebih terstruktur. Misalnya, Anda dapat menentukan apakah klien jarak jauh terputus tanpa harus membaca data dari soket.

Kami sudah berbicara tentang kekurangan metode polling: itu tidak tersedia pada beberapa platform, seperti Windows XP. Sejak Vista, itu ada, tetapi disebut WSAPoll. Prototipenya sama, jadi untuk kode bebas platform Anda dapat menulis override, seperti:

 #if defined (WIN32) static inline int poll( struct pollfd *pfd, int nfds, int timeout) { return WSAPoll ( pfd, nfds, timeout ); } #endif 

Nah, keakuratan timeout adalah 1 ms, yang tidak akan cukup jarang. Namun, jajak pendapat memiliki kelemahan lain:

  • Seperti halnya dengan penggunaan pilih, tidak mungkin untuk menentukan deskriptor mana yang menghasilkan peristiwa tanpa sepenuhnya melewati semua struktur yang diamati dan memeriksa bidang revents di dalamnya. Lebih buruk lagi, ini juga diimplementasikan di kernel OS.
  • Seperti halnya pilih, tidak ada cara untuk secara dinamis mengubah set peristiwa yang diamati

Namun, semua hal di atas dapat dianggap relatif tidak signifikan untuk sebagian besar aplikasi klien. Pengecualiannya mungkin hanya protokol P2P, di mana masing-masing klien dapat dikaitkan dengan ribuan lainnya. Masalah-masalah ini dapat diabaikan bahkan oleh sebagian besar aplikasi server. Oleh karena itu, jajak pendapat harus menjadi preferensi default Anda daripada pilih, kecuali salah satu dari dua alasan di atas membatasi Anda.

Ke depan, saya akan mengatakan bahwa jajak pendapat lebih disukai bahkan dibandingkan dengan epoll yang lebih modern (dibahas di bawah) dalam kasus berikut:

  • Anda ingin menulis kode lintas platform (epoll hanya ada di Linux)
  • Anda tidak perlu memonitor lebih dari 1000 soket (epoll tidak akan memberi Anda sesuatu yang signifikan dalam hal ini)
  • Anda perlu memonitor lebih dari 1000 soket, tetapi waktu koneksi dengan masing-masingnya sangat kecil (dalam kasus ini kinerja polling dan epoll akan sangat dekat - keuntungan dari menunggu lebih sedikit peristiwa dalam epoll akan dicoret oleh biaya tambahan untuk menambahkan / menghapusnya)
  • Aplikasi Anda tidak dirancang untuk mengubah acara dari satu utas sementara yang lain menunggu (atau Anda tidak membutuhkannya)

Polling dengan epoll ()


epoll adalah metode terbaru dan terbaik untuk menunggu acara di Linux (dan hanya di Linux). Ya, itu bukan yang โ€œterbaruโ€ yang langsung - sudah menjadi inti sejak tahun 2002. Ini berbeda dari jajak pendapat dan pilih karena menyediakan API untuk menambah / menghapus / memodifikasi daftar deskriptor dan peristiwa yang diamati.

Menggunakan epoll membutuhkan persiapan yang lebih teliti. Pengembang harus:

  • Buat deskriptor epoll dengan menelepon epoll_create
  • Inisialisasi struktur epoll_event dengan peristiwa dan petunjuk yang diperlukan untuk konteks koneksi. "Konteks" di sini dapat berupa apa saja, epoll hanya melewati nilai itu dalam peristiwa yang dikembalikan
  • Panggil epoll_ctl (... EPOLL_CTL_ADD) untuk menambahkan pegangan ke daftar yang bisa diamati
  • Panggil epoll_wait () untuk menunggu acara (kami menunjukkan dengan tepat berapa banyak acara yang ingin kami terima pada suatu waktu, misalnya, 20). Berbeda dengan metode sebelumnya, kami mendapatkan peristiwa ini secara terpisah, dan tidak di properti struktur input. Jika kami mengamati 200 deskriptor dan 5 di antaranya menerima data baru - epoll_wait hanya akan mengembalikan 5 peristiwa. Jika 50 peristiwa terjadi, 20 yang pertama akan dikembalikan kepada kami, dan 30 sisanya akan menunggu panggilan berikutnya, mereka tidak akan hilang.
  • Memproses acara yang diterima. Ini akan menjadi proses yang relatif cepat, karena kita tidak melihat deskriptor di mana tidak ada yang terjadi

Kode khas terlihat seperti ini:

 //   epoll.       ,      //    (    ,   ),        int pollingfd = epoll_create( 0xCAFE ); if ( pollingfd < 0 ) //  //   epoll_event struct epoll_event ev = { 0 }; //     .    ,   // epoll     . , ,       ev.data.ptr = pConnection1; //    ,     ev.events = EPOLLIN | EPOLLONESHOT; //     .        //      epoll_wait -    if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 ) // report error //       20    struct epoll_event pevents[ 20 ]; //  10  int ready = epoll_wait( pollingfd, pevents, 20, 10000 ); //    if ( ret == -1 ) //  else if ( ret == 0 ) // ,    else { //     for ( int i = 0; i < ret; i++ ) { if ( pevents[i].events & EPOLLIN ) { //        ,   Connection * c = (Connection*) pevents[i].data.ptr; c->handleReadEvent(); } } } 

Mari kita mulai dengan kekurangan epoll - mereka jelas dari kode. Metode ini lebih sulit digunakan, Anda perlu menulis lebih banyak kode, itu membuat lebih banyak panggilan sistem.

Keuntungan juga jelas:

  • epoll mengembalikan daftar hanya deskriptor yang peristiwa sebenarnya diamati terjadi. Anda tidak perlu melihat ribuan struktur untuk mencari satu, mungkin satu di mana acara yang diharapkan bekerja.
  • Anda dapat mengaitkan beberapa konteks yang bermakna dengan setiap peristiwa yang diamati. Pada contoh di atas, kami menggunakan pointer ke objek kelas koneksi untuk ini - ini menyelamatkan kami potensi pencarian lain untuk array koneksi.
  • Anda dapat menambah atau menghapus soket dari daftar kapan saja. Anda bahkan dapat memodifikasi acara yang diamati. Semuanya akan berfungsi dengan benar, ini didukung dan didokumentasikan secara resmi.
  • Anda dapat memulai beberapa utas menunggu acara dari antrian yang sama menggunakan epoll_wait. Sesuatu yang sama sekali tidak bisa dilakukan dengan pilih / polling.

Tetapi Anda juga perlu mengingat bahwa epoll bukanlah "polling yang ditingkatkan habis-habisan." Ini memiliki kelemahan dibandingkan dengan polling:

  • Mengubah flag event (misalnya, beralih dari READ ke WRITE) membutuhkan panggilan sistem epoll_ctl tambahan, sedangkan untuk polling Anda hanya mengubah bitmask (sepenuhnya dalam mode pengguna). Beralih 5.000 soket dari baca ke tulis akan membutuhkan 5.000 panggilan sistem dan sakelar konteks untuk epoll, sedangkan untuk polling itu akan menjadi operasi bit sepele dalam satu lingkaran.
  • Untuk setiap koneksi baru, Anda harus memanggil accept () dan epoll_ctl () adalah dua panggilan sistem. Jika Anda menggunakan polling, hanya akan ada satu panggilan. Dengan masa koneksi yang sangat singkat, ini dapat membuat perbedaan.
  • epoll hanya tersedia di Linux. Sistem operasi lain memiliki mekanisme yang serupa, tetapi masih belum sepenuhnya identik. Anda tidak akan bisa menulis kode dengan epoll sehingga itu membangun dan bekerja, misalnya, di FreeBSD.
  • Menulis kode paralel yang sangat susah. Banyak aplikasi tidak memerlukan pendekatan mendasar seperti itu, karena level bebannya mudah diproses menggunakan metode yang lebih sederhana.

Dengan demikian, epoll hanya boleh digunakan ketika semua hal berikut ini benar:

  • Aplikasi Anda menggunakan kumpulan utas untuk menangani koneksi jaringan. Keuntungan dari epoll dalam aplikasi single-threaded akan diabaikan, dan Anda tidak perlu repot-repot dengan implementasi.
  • Anda mengharapkan jumlah koneksi yang relatif besar (dari 1000 ke atas). Pada sejumlah kecil soket yang teramati, epoll tidak akan memberikan peningkatan kinerja, dan jika ada beberapa soket yang sebenarnya, ia bahkan dapat melambat.
  • Koneksi Anda hidup relatif lama. Dalam situasi di mana koneksi baru mentransfer hanya beberapa byte data dan menutup di sana - jajak pendapat akan bekerja lebih cepat, karena akan perlu membuat lebih sedikit panggilan sistem untuk memprosesnya.
  • Anda bermaksud menjalankan kode di Linux dan hanya di Linux.

Jika satu atau lebih item gagal, pertimbangkan untuk menggunakan polling atau libevent.

libevent


libevent adalah pustaka yang membungkus metode pemungutan suara yang tercantum dalam artikel ini (dan juga yang lainnya) dalam API terpadu. Keuntungannya di sini adalah setelah Anda menulis kode, Anda dapat membangun dan menjalankannya pada sistem operasi yang berbeda. Namun demikian, penting untuk memahami bahwa libevent hanyalah pembungkus, di dalamnya semua metode di atas bekerja, dengan semua kelebihan dan kekurangannya. libevent tidak akan memaksa pilih untuk mendengarkan lebih dari 1024 soket, dan epoll tidak akan mengubah daftar acara tanpa panggilan sistem tambahan. Jadi mengetahui teknologi yang mendasarinya masih penting.

Kebutuhan untuk mendukung metode polling yang berbeda membuat API libevent library lebih kompleks. Namun tetap saja, penggunaannya lebih mudah daripada secara manual menulis dua mesin pemilihan acara yang berbeda untuk, misalnya, Linux dan FreeBSD (menggunakan epoll dan kqueue).

Pertimbangkan menggunakan libevent saat menggabungkan dua acara:

  • Anda melihat metode pemilihan dan pemilihan dan mereka jelas tidak bekerja untuk Anda.
  • Anda perlu mendukung banyak OS

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


All Articles