حدد / استطلاع / epoll: اختلاف عملي

عند تصميم تطبيقات شبكة عالية الأداء باستخدام مآخذ غير مانعة للحظر ، من المهم تحديد طريقة مراقبة أحداث الشبكة التي سنستخدمها. هناك العديد منها ، وكل منها جيد وسيئ بطريقته الخاصة. يمكن أن يكون اختيار الطريقة الصحيحة أمرًا بالغ الأهمية لبنية تطبيقك.

سننظر في هذه المقالة في:

  • حدد ()
  • استطلاع ()
  • epoll ()
  • libevent

باستخدام select ()


لقد تم إنشاء العامل الثابت الثابت () الذي تم إثباته على مر السنين في تلك الأيام عندما كانت "المقابس" تسمى " مقابس بيركلي ". لم يتم تضمين هذه الطريقة في المواصفات الأولى لمآخذ بيركلي نفسها ، لأنه في تلك الأيام لم يكن هناك حتى الآن مفهوم عدم حظر الإدخال / الإخراج. ولكن ظهرت في مكان ما في الثمانينيات ، ومعها حددت (). منذ ذلك الحين ، لم يتغير شيء بشكل كبير في واجهته.

لاستخدام select () ، يحتاج المطور إلى تهيئة العديد من هياكل fd_set وتعبئتها بالواصفات والأحداث التي تحتاج إلى المراقبة ، ثم استدعاء select (). يبدو الرمز النموذجي شيئًا مثل هذا:

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 } 

عندما تم تصميم select () ، لم يتوقع أحد أننا سنحتاج في المستقبل إلى كتابة تطبيقات متعددة الخيوط تخدم آلاف الاتصالات. لدى Select () عدة عيوب مهمة تجعله غير مناسب للعمل على هذه الأنظمة. أهمها:

  • حدد تعديل هياكل fd_sets التي تم تمريرها إليه ، بحيث لا يمكن إعادة استخدام أي منها. حتى إذا لم تكن بحاجة إلى تغيير أي شيء (على سبيل المثال ، بعد تلقي جزء من البيانات ، فأنت تريد الحصول على المزيد) ، يجب إعادة تهيئة هياكل fd_sets. حسنًا ، أو انسخ من نسخة احتياطية محفوظة مسبقًا باستخدام FD_COPY. ويجب القيام بذلك مرارًا وتكرارًا قبل كل مكالمة محددة.
  • لمعرفة الوصف الذي أنشأ الحدث بالضبط ، يجب عليك استقصائهم يدويًا باستخدام FD_ISSET. عندما تراقب 2000 واصف ، وحدث الحدث لأحدهم فقط (والذي ، وفقًا لقانون اللئيم ، سيكون الأخير في القائمة) - سوف تضيع الكثير من موارد المعالج.
  • هل ذكرت 2000 واصف فقط؟ أنا متحمس لذلك. حدد لا يدعم ذلك كثيرا. حسنًا ، على الأقل في نظام لينكس العادي ، بالنواة المعتادة. الحد الأقصى لعدد الواصفات التي تمت ملاحظتها في نفس الوقت محدود بواسطة ثابت FD_SETSIZE ، وهو يساوي بدقة 1024 في Linux. تسمح لك بعض أنظمة التشغيل بتنفيذ اختراق من خلال تجاوز قيمة FD_SETSIZE قبل تضمين ملف رأس sys / select.h ، ولكن هذا الاختراق ليس جزءًا من بعض المعايير المشتركة. سوف يتجاهلها نفس لينكس.
  • لا يمكنك العمل مع واصفات من مجموعة يمكن ملاحظتها من مؤشر ترابط آخر. تخيل خيط تنفيذ التعليمات البرمجية أعلاه. لذلك بدأت وتنتظر الأحداث في تحديدها (). تخيل الآن أن لديك مؤشر ترابط آخر يراقب الحمل الكلي على النظام ، والآن قرر أن البيانات من مقبس sock1 لم تصل لفترة طويلة وأن الوقت قد حان لقطع الاتصال. نظرًا لأنه يمكن إعادة استخدام هذا المقبس لخدمة عملاء جدد ، فسيكون من الجيد إغلاقه بشكل صحيح. لكن الخيط الأول يلاحظ هذا الوصف في الوقت الحالي. ماذا سيحدث إذا أغلقناها بنفس الطريقة؟ أوه ، تحتوي الوثائق على إجابة على هذا السؤال ولن تعجبك: "إذا تم إغلاق المقبض الذي لوحظ مع تحديد () بمؤشر ترابط آخر ، فسوف تحصل على سلوك غير محدد."
  • تظهر نفس المشكلة عند محاولة إرسال بعض البيانات عبر sock1. لن نرسل أي شيء حتى انتهاء تحديد عمله.
  • إن اختيار الأحداث التي يمكننا مراقبتها محدود للغاية. على سبيل المثال ، لتحديد أنه تم إغلاق مأخذ التوصيل البعيد ، يجب عليك أولاً مراقبة أحداث وصول البيانات عليه ، وثانياً ، محاولة قراءة هذه البيانات (ستقرأ القراءة 0 للمأخذ المغلق). لا يزال يمكن تسميتها مقبولة عند قراءة البيانات من مأخذ التوصيل (اقرأ 0 - تم إغلاق مأخذ التوصيل) ، ولكن ماذا لو كانت مهمتنا الحالية في الوقت الحالي هي إرسال البيانات إلى هذا المقبس ولا تحتاج إلى قراءة البيانات منه الآن؟
  • تحديد يضع عبئا غير ضروري عليك لحساب "أكبر واصف" وتمريره كمعلمة منفصلة

بالطبع ، كل ما سبق ليس أي أخبار. لطالما كان مطورو أنظمة التشغيل على علم بهذه المشاكل ، وقد تم أخذ العديد منها في الاعتبار عند تصميم طريقة الاستطلاع. في هذه المرحلة ، قد تسأل ، لماذا ندرس التاريخ القديم الآن ، وهل هناك أسباب اليوم لاستخدام الاختيار القديم؟ نعم ، هناك سببان من هذا القبيل. ليس حقيقة أنها ستكون مفيدة لك في وقت ما ، ولكن لماذا لا تعرف عنها.

السبب الأول هو قابلية النقل. select () معنا منذ مليون سنة. بغض النظر عن غابة الأجهزة والبرامج الأساسية التي تجلب لك ، إذا كانت هناك شبكة هناك ، فسيكون هناك تحديد. قد لا تكون هناك أي طرق أخرى ، ولكن الاختيار مضمون تقريبًا. ولا أعتقد أنني أقع الآن في شيخوخة الشيخوخة وأتذكر شيئًا مثل بطاقات اللكم و ENIAC ، لا. لا توجد طريقة استطلاع حديثة أكثر ، على سبيل المثال ، في Windows XP . لكن الاختيار.

السبب الثاني هو أكثر غرابة ويتعلق بحقيقة أن التحديد يمكن أن يعمل (نظريًا) مع مهلة ترتيب نانوثانية واحدة (إذا سمحت الأجهزة) ، في حين أن الاستطلاع و epoll يدعمان دقة المللي ثانية فقط. لا يجب أن يلعب هذا دورًا خاصًا على أجهزة سطح المكتب العادية (أو حتى الخوادم) ، حيث لا يزال لديك موقت دقة نانو ثانية للأجهزة. ولكن لا يزال في العالم هناك أنظمة في الوقت الفعلي تحتوي على مثل هذه الموقتات. لذا أتوسل إليك ، عندما تكتب البرامج الثابتة لمفاعل نووي أو صاروخ - لا تكن كسولًا جدًا في قياس الوقت بالثواني النانوية. تعلم ، أريد أن أعيش.

من المحتمل أن تكون الحالة الموضحة أعلاه هي الحالة الوحيدة التي ليس لديك فيها حقًا خيار ما تستخدمه (التحديد فقط هو المناسب). ومع ذلك ، إذا كنت تكتب طلبًا عاديًا للعمل على الأجهزة العادية ، وستعمل بعدد مناسب من المقابس (عشرات ، مئات - وليس أكثر) ، فلن يكون الفرق في الاستطلاع واختيار الأداء ملحوظًا ، لذلك سيعتمد الاختيار على عوامل أخرى.

استطلاع مع استطلاع ()


الاستطلاع هو طريقة أحدث لاستقصاء المقابس ، تم إنشاؤها بعد أن بدأ الناس في محاولة كتابة خدمات شبكة كبيرة ومحملة بشكل كبير. تم تصميمه بشكل أفضل ولا يعاني من معظم عيوب الطريقة المحددة. في معظم الحالات ، عند كتابة التطبيقات الحديثة ، ستختار بين استخدام الاستطلاع و epoll / libevent.

لاستخدام الاستطلاع ، يحتاج المطور إلى تهيئة أعضاء هيكل الاستطلاعات باستخدام واصفات وأحداث يمكن ملاحظتها ، ثم استدعاء الاستطلاع ().
يبدو الرمز النموذجي كما يلي:

 //   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 } 

تم إنشاء الاستطلاع لحل مشاكل الطريقة المحددة ، دعنا نرى كيف تم ذلك:

  • لا يوجد حد لعدد الواصفات التي تمت ملاحظتها ؛ يمكن مراقبة أكثر من 1024
  • لم يتم تعديل بنية pollfd ، مما يجعل من الممكن إعادة استخدامه بين مكالمات الاستطلاع () - تحتاج فقط إلى إعادة تعيين حقل Revents.
  • الأحداث المرصودة منظمة بشكل أفضل. على سبيل المثال ، يمكنك تحديد ما إذا كان العميل البعيد غير متصل دون الحاجة إلى قراءة البيانات من مأخذ التوصيل.

لقد تحدثنا بالفعل عن أوجه القصور في طريقة الاستطلاع: فهي غير متاحة في بعض الأنظمة الأساسية ، مثل Windows XP. منذ نظام Vista ، يوجد ، ولكن يسمى WSAPoll. النموذج الأولي هو نفسه ، لذلك بالنسبة للكود المستقل عن النظام الأساسي ، يمكنك كتابة تجاوز ، مثل:

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

حسنًا ، دقة المهلات 1 مللي ثانية ، والتي لن تكون كافية في حالات نادرة جدًا. ومع ذلك ، فإن الاستطلاع له عيوب أخرى:

  • كما هو الحال مع استخدام التحديد ، من المستحيل تحديد أي واصفات ولدت الأحداث دون تمريرة كاملة عبر جميع الهياكل المرصودة والتحقق من حقول العوائد فيها. والأسوأ من ذلك ، يتم تطبيقه أيضًا في نواة نظام التشغيل.
  • كما هو الحال مع التحديد ، لا توجد طريقة لتغيير مجموعة الأحداث الملحوظة ديناميكيًا

ومع ذلك ، يمكن اعتبار كل ما سبق غير مهم نسبيًا لمعظم تطبيقات العملاء. ربما يكون الاستثناء هو بروتوكولات P2P فقط ، حيث يمكن ربط كل عميل بآلاف العملاء الآخرين. يمكن تجاهل هذه المشاكل حتى من قبل معظم تطبيقات الخادم. لذلك ، يجب أن يكون الاستطلاع هو تفضيلك الافتراضي على التحديد ، ما لم يحدك أحد السببين المذكورين أعلاه.

واستشرافا للمستقبل ، سأقول أن الاستطلاع هو الأفضل حتى بالمقارنة مع epoll الأكثر حداثة (الموضح أدناه) في الحالات التالية:

  • تريد كتابة رمز عبر النظام الأساسي (epoll هو فقط على Linux)
  • لا تحتاج إلى مراقبة أكثر من 1000 مآخذ (لن يمنحك epoll أي شيء مهم في هذه الحالة)
  • تحتاج إلى مراقبة أكثر من 1000 مآخذ ، ولكن وقت الاتصال مع كل منها صغير جدًا (في هذه الحالات سيكون أداء الاستطلاع و epoll قريبًا جدًا - سيتم شطب الربح من انتظار عدد أقل من الأحداث في epoll من خلال زيادة إضافتها / إزالتها)
  • التطبيق الخاص بك غير مصمم لتغيير الأحداث من مؤشر ترابط بينما ينتظره الآخرون (أو لا تحتاجه)

الاستطلاع مع epoll ()


epoll هو أحدث وأفضل طريقة لانتظار الأحداث على Linux (وفقط على Linux). حسنًا ، ليس الأمر أن "الأحدث" مباشر - فقد كان في صميمه منذ عام 2002. وهو يختلف عن الاستطلاع وحدد أنه يوفر واجهة برمجة تطبيقات لإضافة / إزالة / تعديل قائمة الواصفات والأحداث الملحوظة.

يتطلب استخدام epoll استعدادات أكثر شمولًا. يجب على المطور:

  • قم بإنشاء واصف epoll عن طريق استدعاء epoll_create
  • قم بتهيئة بنية epoll_event بالأحداث والمؤشرات الضرورية لسياقات الاتصال. يمكن أن يكون "السياق" هنا أي شيء ، ويمرر epoll تلك القيمة في الأحداث التي تم إرجاعها
  • استدعاء epoll_ctl (... EPOLL_CTL_ADD) لإضافة مؤشر إلى قائمة الملاحظات
  • اتصل بـ epoll_wait () لانتظار الأحداث (نوضح بالضبط عدد الأحداث التي نريد تلقيها في المرة الواحدة ، على سبيل المثال ، 20). على عكس الطرق السابقة ، نحصل على هذه الأحداث بشكل منفصل ، وليس في خصائص بنيات الإدخال. إذا لاحظنا 200 واصفًا ، وتلقى 5 منهم بيانات جديدة - فلن يعرض epoll_wait سوى 5 أحداث. في حالة حدوث 50 حدثًا ، سيتم إرجاع أول 20 حدثًا إلينا ، بينما ينتظر 30 الآخرون المكالمة التالية ، فلن يتم فقدهم
  • تلقى عملية الأحداث. ستكون هذه معالجة سريعة نسبيًا ، لأننا لا ننظر إلى الواصفات التي لم يحدث فيها شيء

يبدو الرمز النموذجي كما يلي:

 //   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(); } } } 

لنبدأ بعيوب epoll - فهي واضحة من الكود. هذه الطريقة أكثر صعوبة في الاستخدام ، وتحتاج إلى كتابة المزيد من التعليمات البرمجية ، وإجراء المزيد من مكالمات النظام.

المزايا واضحة أيضًا:

  • ترجع epoll قائمة فقط بالواصفات التي حدثت لها الأحداث المرصودة بالفعل. لا تحتاج إلى إلقاء نظرة على آلاف الهياكل بحثًا عن واحد ، ربما هو الذي يعمل فيه الحدث المتوقع.
  • يمكنك ربط سياق محدد بكل حدث مرصود. في المثال أعلاه ، استخدمنا مؤشرًا لكائن من فئة الاتصال لهذا - وهذا وفر لنا بحثًا محتملًا آخر لمجموعة من الاتصالات.
  • يمكنك إضافة أو إزالة مآخذ التوصيل من القائمة في أي وقت. يمكنك حتى تعديل الأحداث الملحوظة. كل شيء سيعمل بشكل صحيح ، وهذا مدعوم وموثق رسميًا.
  • يمكنك بدء سلاسل رسائل متعددة في انتظار الأحداث من نفس قائمة الانتظار باستخدام epoll_wait. شيء لا يمكن القيام به بأي حال من الأحوال مع تحديد / استطلاع.

ولكن عليك أيضًا أن تتذكر أن epoll ليس "استطلاعًا شاملاً محسنًا". لها عيوب مقارنة بالاستطلاع:

  • يتطلب تغيير علامات الأحداث (على سبيل المثال ، التبديل من READ إلى WRITE) استدعاء نظام epoll_ctl إضافي ، بينما في الاستطلاع يمكنك فقط تغيير قناع البت (تمامًا في وضع المستخدم). سيتطلب تحويل 5000 مآخذ من القراءة إلى الكتابة 5000 مكالمة للنظام ومبدلات السياق لـ epoll ، بينما بالنسبة للاستطلاع ، ستكون عملية بت بسيطة في حلقة.
  • لكل اتصال جديد ، عليك الاتصال بقبول () و epoll_ctl () مكالمتين للنظام. إذا كنت تستخدم الاستطلاع ، فسيكون هناك مكالمة واحدة فقط. مع عمر اتصال قصير جدًا ، يمكن أن يحدث هذا فرقًا.
  • يتوفر epoll فقط على Linux. أنظمة التشغيل الأخرى لديها آليات مماثلة ، لكنها لا تزال غير متطابقة تمامًا. لن تكون قادرًا على كتابة التعليمات البرمجية باستخدام epoll بحيث يبني ويعمل ، على سبيل المثال ، على FreeBSD.
  • من الصعب كتابة رمز متوازي عالي التحميل. لا تحتاج العديد من التطبيقات إلى مثل هذا النهج الأساسي ، حيث يتم معالجة مستوى حمولتها بسهولة باستخدام طرق أبسط.

وبالتالي ، يجب استخدام epoll فقط عندما يكون كل ما يلي صحيحًا:

  • يستخدم التطبيق الخاص بك تجمع مؤشر ترابط لمعالجة اتصالات الشبكة. سيكون الربح من epoll في تطبيق ذو مؤشر ترابط ضئيلًا ، ويجب ألا تهتم بالتنفيذ.
  • تتوقع عددًا كبيرًا نسبيًا من الاتصالات (من 1000 وما فوق). على عدد صغير من المقابس التي تمت ملاحظتها ، لن تعطي epoll مكاسب في الأداء ، وإذا كان هناك عدد قليل من المقابس ، فقد يتباطأ.
  • اتصالاتك تعيش طويلا نسبيا. في الحالة التي ينقل فيها اتصال جديد بضع بايتات من البيانات ويغلق عندها مباشرةً - سيعمل الاستطلاع بشكل أسرع ، لأنه سيحتاج إلى إجراء عدد أقل من استدعاءات النظام لمعالجته.
  • تنوي تشغيل التعليمات البرمجية على Linux وفقط على Linux.

إذا فشل عنصر أو أكثر من العناصر ، ففكر في استخدام الاستطلاع أو libevent.

libevent


libevent هي مكتبة تلتف حول طرق الاستقصاء المدرجة في هذه المقالة (بالإضافة إلى البعض الآخر) في واجهة برمجة تطبيقات موحدة. الميزة هنا هي أنه بمجرد كتابة الرمز ، يمكنك بناءه وتشغيله على أنظمة تشغيل مختلفة. ومع ذلك ، من المهم أن نفهم أن libevent هو مجرد غلاف ، تعمل فيه جميع الأساليب المذكورة أعلاه ، بكل مزاياها وعيوبها. لن يجبر libevent على التحديد للاستماع إلى أكثر من 1024 مآخذ ، ولن يقوم epoll بتعديل قائمة الأحداث بدون استدعاء نظام إضافي. لذا فإن معرفة التقنيات الأساسية لا تزال مهمة.

الحاجة لدعم طرق اقتراع مختلفة تجعل مكتبة libevent API أكثر تعقيدًا. ولكن لا يزال استخدامه أسهل من كتابة محركين مختلفين لاختيار الحدث يدويًا ، على سبيل المثال ، Linux و FreeBSD (باستخدام epoll و kqueue).

ضع في اعتبارك استخدام libevent عند دمج حدثين:

  • لقد نظرت إلى طرق الاختيار والاستطلاع ، وهي بالتأكيد لم تنجح معك.
  • تحتاج إلى دعم أنظمة تشغيل متعددة

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


All Articles