منافذ إكمال epoll و Windows IO: الفرق العملي

مقدمة


في هذه المقالة ، سنحاول فهم كيفية اختلاف آلية epoll عن منافذ الإكمال عمليًا (منفذ إكمال I / O لـ Windows أو IOCP). قد يكون هذا مثيرًا للاهتمام لمهندسي النظام الذين يقومون بتصميم خدمات الشبكة عالية الأداء أو المبرمجين الذين ينقلون رمز الشبكة من Windows إلى Linux أو العكس.

كل من هذه التقنيات فعالة للغاية في التعامل مع عدد كبير من اتصالات الشبكة.

تختلف عن الطرق الأخرى في النقاط التالية:

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

لإعادة صياغة كل ما سبق ، تم تصميم كل من هذه التقنيات لتطوير خدمات الشبكة التي تعالج العديد من الاتصالات الواردة من العملاء. ولكن في الوقت نفسه ، هناك فرق كبير بينهما ، وعند تطوير نفس الخدمات ، من المهم معرفة ذلك.

(التحديث: هذه المقالة ترجمة )


نوع الإخطارات


الاختلاف الأول والأهم بين epoll و IOCP هو كيفية إعلامك بحدث ما.

  • يخبرك epoll عندما يكون الواصف جاهزًا للقيام بشيء باستخدامه - " الآن يمكنك البدء في قراءة البيانات "
  • يخبرك IOCP عند اكتمال العملية المطلوبة - " لقد طلبت قراءة البيانات وهنا يتم قراءتها "

عند استخدام تطبيق epoll:

  • تحديد العملية التي تريد تنفيذها باستخدام بعض الواصفات (القراءة أو الكتابة أو كليهما)
  • يضبط القناع المناسب باستخدام epoll_ctl
  • استدعاء epoll_wait ، الذي يمنع سلسلة المحادثات الحالية حتى يحدث حدث متوقع واحد على الأقل (أو انتهاء المهلة)
  • يتكرر فوق الأحداث المستلمة ، ويأخذ مؤشرًا إلى السياق (من حقل data.ptr)
  • يبدأ معالجة الأحداث وفقًا لنوعها (القراءة أو الكتابة أو كلتا العمليتين)
  • بعد اكتمال العملية (ما يجب أن يحدث على الفور) ، تستمر في انتظار استلام / إرسال البيانات

عند استخدام تطبيق IOCP:

  • يبدأ بعض العمليات (ReadFile أو WriteFile) لبعض الواصفات ، باستخدام وسيطة OVERLAPPED غير الفارغة. يضيف نظام التشغيل متطلبات تنفيذ هذه العملية لنفسه في قائمة الانتظار ، ويتم إرجاع الوظيفة المطلوبة على الفور (دون انتظار اكتمال العملية).
  • استدعاءات GetQueuedCompletionStatus () ، التي تحظر سلسلة المحادثات الحالية حتى يكتمل أحد الطلبات المضافة مسبقًا بالضبط. إذا اكتمل العديد ، فسيتم اختيار واحد منهم فقط.
  • يقوم بمعالجة الإخطار الذي تم تلقيه بشأن اكتمال العملية باستخدام مفتاح الإكمال ومؤشر للتجاوز.
  • يستمر في انتظار تلقي / إرسال البيانات

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

توفر البيانات


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

  • لا تشعر epoll على الإطلاق بالقلق من وجود هذه المخازن المؤقتة ولا تستخدمها بأي شكل من الأشكال
  • هناك حاجة إلى IOCP هذه المخازن المؤقتة. الهدف الرئيسي من استخدام IOCP هو العمل بأسلوب "قراءة 256 بايت من هذا المقبس في هذا المخزن المؤقت". لقد شكلنا مثل هذا الطلب ، وأعطيناه لنظام التشغيل ، ونحن ننتظر إخطارًا بإكمال العملية (ولا تلمس المخزن المؤقت في هذا الوقت!)

تعمل خدمة الشبكة النموذجية مع كائنات الاتصال ، والتي ستتضمن واصفات ومخازن مرتبطة بها لقراءة / كتابة البيانات. عادة ، يتم تدمير هذه الكائنات عندما يتم إغلاق مأخذ التوصيل المقابل. وهذا يفرض بعض القيود عند استخدام IOCP.

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

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

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

لا تستخدم epoll أي مخازن مؤقتة تم تمريرها إليها من كود المستخدم ، لذلك كل هذه المشاكل لا علاقة لها بها.

تغيير شروط الانتظار


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

ومع ذلك ، يختلف تغيير الأحداث المتوقعة أو حذفها. لا يزال يسمح لك epoll بتعديل الحالة عن طريق استدعاء epoll_ctl (بما في ذلك من سلاسل الرسائل الأخرى). IOCP تزداد صعوبة. إذا تم التخطيط لعملية I / O ، يمكن إلغاؤها عن طريق استدعاء وظيفة CancelIo () . الأسوأ من ذلك ، يمكن فقط استدعاء نفس مؤشر الترابط الذي بدأ العملية الأولية هذه الوظيفة. كل أفكار تنظيم تدفق تحكم منفصل مكسورة حول هذا القيد. بالإضافة إلى ذلك ، حتى بعد استدعاء CancelIo () ، لا يمكننا التأكد من أن العملية سيتم إلغاؤها على الفور (قد تكون قيد التقدم بالفعل ، فهي تستخدم بنية OVERLAPPED والمخزن المؤقت الذي تم تمريره للقراءة / الكتابة). لا يزال يتعين علينا الانتظار حتى اكتمال العملية (سيتم إرجاع نتيجتها بواسطة الدالة GetOverlappedResult ()) وبعد ذلك فقط يمكننا تحرير المخزن المؤقت.

مشكلة أخرى مع IOCP هي أنه بمجرد جدولة عملية للتنفيذ ، لا يمكن تغييرها. على سبيل المثال ، لا يمكنك تغيير طلب ReadFile المجدول وقول أنك تريد قراءة 10 بايت فقط ، وليس 8192. تحتاج إلى إلغاء العملية الحالية وبدء عملية جديدة. هذه ليست مشكلة لـ epoll ، التي عند بدء الانتظار ، ليس لديها فكرة عن مقدار البيانات التي تريد قراءتها في الوقت الذي يأتي فيه الإخطار حول القدرة على قراءة البيانات.

اتصال غير مانع


تتطلب بعض تطبيقات خدمات الشبكة (الخدمات ذات الصلة ، FTP ، p2p) اتصالات صادرة. يدعم كل من epoll و IOCP طلب اتصال غير قابل للحظر ، ولكن بطرق مختلفة.

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

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

من المثير للاهتمام ، قبول () يعمل مع IOCP كالمعتاد. توجد وظيفة AcceptEx ، ولكن دورها غير مرتبط تمامًا باتصال لا يؤدي إلى الحظر. هذا ليس "قبولًا غير قابل للحظر" ، كما قد تفكر بالقياس مع connect / ConnectEx.

مراقبة الأحداث


غالبًا ما تأتي البيانات الإضافية بعد إطلاق الحدث بسرعة كبيرة. على سبيل المثال ، كنا نتوقع وصول مدخلات من المقبس باستخدام epoll أو IOCP ، حصلنا على حدث حول أول بايتات قليلة من البيانات ، وهناك ، بينما نقرأها ، جاءت مائة بايت أخرى. هل يمكنني قراءتها دون إعادة تشغيل مراقبة الأحداث؟

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

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

  1. أنشأت فئة مأخذ التوصيل طلبًا لقراءة البيانات عن طريق استدعاء ReadFile. ينتظر مؤشر الترابط A و B النتيجة عن طريق استدعاء GetOverlappedResult ()
  2. اكتملت عملية القراءة ، وتلقى مؤشر الترابط A إشعارًا واستدعى طريقة فئة مأخذ التوصيل لمعالجة البيانات المستلمة
  3. قررت فئة المقبس أن هذه البيانات ليست كافية ، يجب أن نتوقع ما يلي. يضع طلب قراءة آخر.
  4. يتم تنفيذ هذا الطلب على الفور (وصلت البيانات بالفعل ، يمكن لنظام التشغيل إرسالها على الفور). يتلقى التدفق B إشعارًا ويقرأ البيانات ويمررها إلى فئة المقابس.
  5. في الوقت الحالي ، يتم استدعاء وظيفة قراءة البيانات في فئة المقبس من كل من التدفق A و B ، مما يؤدي إما إلى خطر تلف البيانات (بدون استخدام كائنات المزامنة) ، أو إلى فترات توقف إضافية (عند استخدام كائنات المزامنة)

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

الحل المعتاد هنا هو إنشاء فئة إدارة اتصال تكون مسؤولة عن استدعاء ReadFile أو WriteFile لفئة الاتصال. يعمل هذا بشكل أفضل ، ولكنه يجعل الشفرة أكثر تعقيدًا.

الاستنتاجات


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

عند نقل التعليمات البرمجية من نظام أساسي إلى آخر ، يكون من الأسهل عادةً إدخال رمز IOCP لاستخدام epoll من العكس.

نصائح:

  • إذا كانت مهمتك هي تطوير خدمة شبكة عبر الأنظمة الأساسية ، يجب أن تبدأ بتطبيق Windows باستخدام IOCP. بمجرد أن يصبح كل شيء جاهزًا ومُصححًا - أضف خلفية خلفية بسيطة.
  • يجب ألا تحاول كتابة الفئات العامة Connection و ConnectionMgr التي تطبق منطق epoll و IOCP في نفس الوقت. يبدو سيئًا من وجهة نظر بنية الكود ويؤدي إلى مجموعة من جميع أنواع #ifdef مع منطق مختلف داخلها. من الأفضل إنشاء فئات أساسية وترث عمليات التنفيذ المنفصلة عنها. في الفئات الأساسية ، يمكنك الاحتفاظ ببعض الطرق أو البيانات العامة ، إن وجدت.
  • راقب عن كثب عمر كائنات فئة الاتصال (أو أيًا كان ما تطلق عليه الفئة حيث سيتم تخزين المخازن المؤقتة للبيانات المستلمة / المرسلة). لا يجب تدميره حتى تكتمل عمليات القراءة / الكتابة المجدولة باستخدام المخازن المؤقتة الخاصة به.

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


All Articles